Магазин на Laravel 7, часть 20. Показ отдельной страницы и верхнее меню всех страниц
31.10.2020
Теги: Laravel • MySQL • PHP • Web-разработка • БазаДанных • ИнтернетМагазин • КаталогТоваров • Меню • Навигация • Практика • Фреймворк • ШаблонСайта
Показ страницы
Давайте создадим контроллер для показа страницы сайта в публичной части. У этого контроллера будет только одно действие, а следовательно — только один метод. Создать заготовку такого контроллера можно с помощью 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 7, часть 24. Фильтр товаров категории по цене, новинкам и лидерам продаж
- Магазин на Laravel 7, часть 23. Главная страница сайта, новинки, лидеры продаж и распродажа
- Магазин на Laravel 7, часть 21. Добавляем профили и используем их при оформлении заказа
- Магазин на Laravel 7, часть 18. Панель управления, пользователи и CRUD страниц сайта
- Магазин на Laravel 7, часть 17. Панель управления, работа с заказами, изменение статуса
- Магазин на Laravel 7, часть 16. Панель управления, CRUD-операции для товаров каталога
- Магазин на Laravel 7, часть 15. Панель управления, добавление и редактирование брендов
Поиск: Laravel • MySQL • PHP • Web-разработка • База данных • Интернет магазин • Каталог товаров • Меню • Навигация • Практика • Фреймворк • Шаблон сайта