Магазин на Laravel 7, часть 20. Показ отдельной страницы и верхнее меню всех страниц

31.10.2020

Теги: LaravelMySQLPHPWeb-разработкаБазаДанныхИнтернетМагазинКаталогТоваровМенюНавигацияПрактикаФреймворкШаблонСайта

Показ страницы

Давайте создадим контроллер для показа страницы сайта в публичной части. У этого контроллера будет только одно действие, а следовательно — только один метод. Создать заготовку такого контроллера можно с помощью artisan-команды.

> php artisan make:controller PageController --invokable
namespace App\Http\Controllers;

use App\Models\Page;
use Illuminate\Http\Request;

class PageController extends Controller {
    /**
     * Handle the incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param App\Models\Page $page
     * @return \Illuminate\Http\Response
     */
    public function __invoke(Request $request, Page $page) {
        return view('page', compact('page'));
    }
}

Добавим маршрут в файл routes/web.php:

Route::get('page/{page}', 'PageController')->name('page.show');

Но тут сразу возникает проблема. Laravel будет пытаться получить модель по уникальному идентификатору страницы. Это задается в методе getRouteKeyName() родительского класса модели Illuminate\Database\Eloquent\Model.

abstract class Model implements ... {
    /**
     * The primary key for the model.
     *
     * @var string
     */
    protected $primaryKey = 'id';

    /**
     * Get the route key for the model.
     *
     * @return string
     */
    public function getRouteKeyName() {
        return $this->getKeyName();
    }

    /**
     * Get the primary key for the model.
     *
     * @return string
     */
    public function getKeyName() {
        return $this->primaryKey;
    }

    /**
     * Retrieve the model for a bound value.
     *
     * @param  mixed  $value
     * @param  string|null  $field
     * @return \Illuminate\Database\Eloquent\Model|null
     */
    public function resolveRouteBinding($value, $field = null) {
        return $this->where($field ?? $this->getRouteKeyName(), $value)->first();
    }
}

Поэтому для этого маршрута надо явно указать, что получать модель для внедрения в контроллер нужно по уникальному slug.

Route::get('/page/{page:slug}', 'PageController')->name('page.show');

И осталось только создать шаблон views/page/show.blade.php:

@extends('layout.site')

@section('content')
    <div class="card">
        <div class="card-header">
            <h1>{{ $page->name }}</h1>
        </div>
        <div class="card-body">
            {!! $page->content  !!}
        </div>
        <div class="card-footer">
            Добавлена: {{ $page->created_at->format('d.m.Y H:i') }}
        </div>
    </div>
@endsection

Небольшое отступление

По поводу привязки модели к маршруту. Мы можем в модели задать значение переменной $primaryKey, чтобы указать Laravel, по какому полю таблицы искать запись в таблице БД. Но, в панели управления нужно получать страницу по идентификатору, а в публичной части — по уникальному slug. Так что нам это не подходит.

Есть как минимум три варианта решения этой проблемы. Первый — переопределить метод модели getRouteKeyName(). Второй — переопределить метод модели resolveRouteBinding(). Третий — внести изменения в метод boot() сервис-провайдера RouteServiceProvider. Подробно это описано в документации, см. здесь.

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Route;

class Page extends Model {
    /**
     * Если мы в панели управления — страница будет получена из
     * БД по id, если в публичной части сайта — то по slug
     *
     * @return string
     */
    public function getRouteKeyName() {
        $current = Route::currentRouteName();
        if ('page.show' == $current) {
            return 'slug'; // мы в публичной части сайта
        }
        return 'id'; // мы в панели управления
    }
    /* ... */
}
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Route;

class Page extends Model {
    /**
     * Если мы в панели управления — страница будет получена из
     * БД по id, если в публичной части сайта — то по slug
     *
     * @param  mixed  $value
     * @param  string|null  $field
     * @return \Illuminate\Database\Eloquent\Model|null
     */
    public function resolveRouteBinding($value, $field = null) {
        $current = Route::currentRouteName();
        if ('page.show' == $current) {
            // мы в публичной части сайта
            return $this->whereSlug($value)->firstOrFail();
        }
        // мы в панели управления
        return $this->findOrFail($value);
    }
    /* ... */
}
namespace App\Providers;

use App\Models\Page;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;

class RouteServiceProvider extends ServiceProvider {
    /* ... */
    public function boot() {

        parent::boot();

        /*
         * Если мы в панели управления — страница будет получена из
         * БД по id, если в публичной части сайта — то по slug
         */
        Route::bind('page', function($value) {
            $current = Route::currentRouteName();
            if ('page.show' == $current) { // публичная часть сайта
                return Page::whereSlug($value)->firstOrFail();
            }
            // панель управления сайта
            return Page::findOrFail($value);
        });
    }
    /* ... */
}

Верхнее меню

Нам нужно получать от модели все страницы и показывать ссылки на эти страницы в верхнем меню.

<ul class="navbar-nav mr-auto">
    <li class="nav-item">
        <a class="nav-link" href="{{ route('catalog.index') }}">Каталог</a>
    </li>
    <li class="nav-item">
        <a class="nav-link" href="#">Доставка</a>
    </li>
    <li class="nav-item dropdown">
        <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown"
           role="button" data-toggle="dropdown" aria-haspopup="true"
           aria-expanded="false">
            Оплата (dropdown)
        </a>
        <div class="dropdown-menu" aria-labelledby="navbarDropdown">
            <a class="dropdown-item" href="#">Оплата (navlink)</a>
            <div class="dropdown-divider"></div>
            <a class="dropdown-item" href="#">Первая дочерняя (оплата)</a>
            <a class="dropdown-item" href="#">Вторая дочерняя (оплата)</a>
        </div>
    </li>
    <li class="nav-item">
        <a class="nav-link" href="#">Контакты</a>
    </li>
</ul>

В Bootstrap сделано так, что элемент панели Navbar может быть либо ссылкой, либо выпадающим списком. Но не может быть одновременно и тем и другим. Поэтому, если у страницы есть дочерние страницы, то первым элементом выпадающего списка будет ссылка на страницу первого уровня, а дальше — ссылки на дочерние страницы.

В классе ComposerServiceProvider будем передавать переменную pages в шаблон layout.part.pages:

class ComposerServiceProvider extends ServiceProvider {
    /* ... */
    public function boot() {
        View::composer('layout.part.roots', function($view) {
            $view->with(['items' => Category::all()]);
        });
        View::composer('layout.part.brands', function($view) {
            $view->with(['items' => Brand::popular()]);
        });
        View::composer('layout.site', function($view) {
            $view->with(['positions' => Basket::getCount()]);
        });
        View::composer('layout.part.pages', function($view) {
            $view->with(['pages' => Page::all()]);
        });
    }
}

В layout-шаблоне подключим шаблон layout.part.pages с помощью директивы @include():

<!-- Основная часть меню -->
<div class="collapse navbar-collapse" id="navbar-example">
    <!-- Этот блок расположен слева -->
    <ul class="navbar-nav mr-auto">
        <li class="nav-item">
            <a class="nav-link" href="{{ route('catalog.index') }}">Каталог</a>
        </li>
        @include('layout.part.pages')
    </ul>

    <!-- Этот блок расположен посередине -->
    <form class="form-inline my-2 my-lg-0">
        <!-- ..... -->
    </form>

    <!-- Этот блок расположен справа -->
    <ul class="navbar-nav ml-auto">
        <!-- ..... -->
    </ul>
</div>

И создадим шаблон views/layout/part/pages.blade.php, который будет показывать ссылки на все страницы сайта. Шаблон не будем делать рекурсивным, потому как нет задачи сделать неограниченную глубину вложенности для страниц.

@foreach ($pages->where('parent_id', 0) as $page)
    @if (count($pages->where('parent_id', $page->id)))
        <li class="nav-item dropdown">
            <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown"
               role="button" data-toggle="dropdown" aria-haspopup="true"
               aria-expanded="false">
                {{ $page->name }}
            </a>
            <div class="dropdown-menu" aria-labelledby="navbarDropdown">
                <a class="dropdown-item" href="{{ route('page.show', ['page' => $page->slug]) }}">
                    {{ $page->name }}
                </a>
                <div class="dropdown-divider"></div>
                @foreach ($pages->where('parent_id', $page->id) as $child)
                    <a class="dropdown-item" href="{{ route('page.show', ['page' => $child->slug]) }}">
                        {{ $child->name }}
                    </a>
                @endforeach
            </div>
        </li>
    @else
        <li class="nav-item">
            <a class="nav-link" href="{{ route('page.show', ['page' => $page->slug]) }}">
                {{ $page->name }}
            </a>
        </li>
    @endif
@endforeach

Заголовки страниц

Совсем про них забыл, так что давайте это исправим. В layout-шаблоне зададим значение по умолчанию для заголовка и будем передавать из дочерних шаблонов актуальное значение заголовка.

<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>{{ $title ?? 'Интернет-магазин' }}</title>
    <link rel="stylesheet" href="{{ asset('css/app.css') }}">
    <link rel="stylesheet" href="https://pro.fontawesome.com/releases/v5.10.0/css/all.css">
    <link rel="stylesheet" href="{{ asset('css/site.css') }}">
    <script src="{{ asset('js/app.js') }}"></script>
    <script src="{{ asset('js/site.js') }}"></script>
</head>

Шаблон страницы сайта views/page/show.blade.php:

@extends('layout.site', ['title' => $page->name])

@section('content')
    <!-- ..... -->
@endsection

Шаблон категории каталога views/catalog/category.blade.php:

@extends('layout.site', ['title' => $category->name])

@section('content')
    <!-- ..... -->
@endsection

Шаблон товара каталога views/catalog/product.blade.php:

@extends('layout.site', ['title' => $product->name])

@section('content')
    <!-- ..... -->
@endsection

Шаблон бренда каталога views/catalog/brand.blade.php:

@extends('layout.site', ['title' => $brand->name])

@section('content')
    <!-- ..... -->
@endsection

Шаблон корзины покупателя views/basket/index.blade.php:

@extends('layout.site', ['title' => 'Ваша корзина'])

@section('content')
    <!-- ..... -->
@endsection

Шаблон страницы оформления заказа views/basket/checkout.blade.php:

@extends('layout.site', ['title' => 'Оформить заказ'])

@section('content')
    <!-- ..... -->
@endsection

Шаблон успешного оформления заказа views/basket/success.blade.php:

@extends('layout.site', ['title' => 'Заказ размещен'])

@section('content')
    <!-- ..... -->
@endsection

Шаблон страницы регистрации views/auth/register.blade.php:

@extends('layout.site', ['title' => 'Регистрация на сайте'])

@section('content')
    <!-- ..... -->
@endsection

Шаблон страницы входа в ЛК views/auth/login.blade.php:

@extends('layout.site', ['title' => 'Вход в личный кабинет'])

@section('content')
    <!-- ..... -->
@endsection

Поиск: Laravel • MySQL • PHP • Web-разработка • База данных • Интернет магазин • Каталог товаров • Меню • Навигация • Практика • Фреймворк • Шаблон сайта

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.