Магазин на Laravel 7, часть 22. Рефакторинг кода, работа над каталогом товаров и корзиной
04.11.2020
Теги: Laravel • MySQL • PHP • Web-разработка • БазаДанных • ИнтернетМагазин • КаталогТоваров • Корзина • Практика • ШаблонСайта
Рефакторинг кода
Фреймворк Laravel имеет в своём арсенале много полезных функций, и одна из них — привязка модели к маршруту (Route Model Binding). Привязка модели к маршруту — это механизм внедрения экземпляра модели по ключу маршрута. Звучит сложно, но на самом деле все просто. Мы уже использовали привязку в панели управления много раз.
Сейчас наш контроллер CatalogController
, который отвечает за страницы каталога товаров, выглядит так:
namespace App\Http\Controllers; use App\Models\Brand; use App\Models\Category; use App\Models\Product; use Illuminate\Http\Request; class CatalogController extends Controller { public function index() { $roots = Category::where('parent_id', 0)->get(); return view('catalog.index', compact('roots')); } public function category($slug) { $category = Category::where('slug', $slug)->firstOrFail(); return view('catalog.category', compact('category')); } public function brand($slug) { $brand = Brand::where('slug', $slug)->firstOrFail(); return view('catalog.brand', compact('brand')); } public function product($slug) { $product = Product::where('slug', $slug)->firstOrFail(); return view('catalog.product', compact('product')); } }
И у нас есть маршруты в файле routes/web.php
:
Route::get('/catalog/index', 'CatalogController@index')->name('catalog.index'); Route::get('/catalog/category/{slug}', 'CatalogController@category')->name('catalog.category'); Route::get('/catalog/brand/{slug}', 'CatalogController@brand')->name('catalog.brand'); Route::get('/catalog/product/{slug}', 'CatalogController@product')->name('catalog.product');
Мы можем упростить контроллер, чтобы не самим получать экземпляр модели категории, бренда и товара, а доверить это Laravel. Фреймворк будет внедрять экземпляры моделей в методы контроллера, если мы используем «type hinting».
namespace App\Http\Controllers; use App\Models\Brand; use App\Models\Category; use App\Models\Product; use Illuminate\Http\Request; class CatalogController extends Controller { public function index() { $roots = Category::where('parent_id', 0)->get(); return view('catalog.index', compact('roots')); } public function category(Category $category) { return view('catalog.category', compact('category')); } public function brand(Brand $brand) { return view('catalog.brand', compact('brand')); } public function product(Product $product) { return view('catalog.product', compact('product')); } }
Важно, что название параметра /catalog/brand/{brand}
должно соответствовать имени переменной аргумента метода brand(Brand $brand)
. Только так фреймворк будет понимать, что надо создать экземпляр модели App\Models\Brand
и внедрить его в метод brand()
.
Route::get('/catalog/brand/{brand}', 'CatalogController@brand')->name('catalog.brand');
class CatalogController extends Controller {
public function brand(Brand $brand) {
return view('catalog.brand', compact('brand'));
}
}
Давайте так и сделаем:
Route::get('/catalog/index', 'CatalogController@index')->name('catalog.index'); Route::get('/catalog/category/{category}', 'CatalogController@category')->name('catalog.category'); Route::get('/catalog/brand/{brand}', 'CatalogController@brand')->name('catalog.brand'); Route::get('/catalog/product/{product}', 'CatalogController@product')->name('catalog.product');
Фреймворк по умолчанию берёт параметр {brand}
из маршрута и ищет запись в таблице БД brands
по полю id
. Чтобы изменить поле, по которому идет поиск записи в таблице БД, можно поступить так:
Route::get('/catalog/index', 'CatalogController@index')->name('catalog.index'); Route::get('/catalog/category/{category:slug}', 'CatalogController@category')->name('catalog.category'); Route::get('/catalog/brand/{brand:slug}', 'CatalogController@brand')->name('catalog.brand'); Route::get('/catalog/product/{product:slug}', 'CatalogController@product')->name('catalog.product');
Теперь поиск записи будет по уникальному полю slug
. Но осталось еще исправить имя параметра в шаблонах — заменить slug
на category
, brand
или product
. Сильно жалею, что не сделал с самого начала, так что теперь много муторной работы. Надо найти все вызовы функции route()
и заменить имя параметра.
<a href="{{ route('catalog.brand', ['slug' => $brand->slug]) }}">{{ $brand->name }}</a> <a href="{{ route('catalog.brand', ['brand' => $brand->slug]) }}">{{ $brand->name }}</a>
<a href="{{ route('catalog.category', ['slug' => $category->slug]) }}">{{ $category->name }}</a> <a href="{{ route('catalog.category', ['category' => $category->slug]) }}">{{ $category->name }}</a>
<a href="{{ route('catalog.product', ['slug' => $product->slug]) }}">{{ $product->name }}</a> <a href="{{ route('catalog.product', ['product' => $product->slug]) }}">{{ $product->name }}</a>
Еще лучше — вообще не указывать имя параметра, чтобы больше с этим никогда не сталкиваться, если возникнет необходимость изменить имя параметра маршрута.
<a href="{{ route('catalog.brand', [$brand->slug]) }}">{{ $brand->name }}</a>
<a href="{{ route('catalog.category', [$category->slug]) }}">{{ $category->name }}</a>
<a href="{{ route('catalog.product', [$product->slug]) }}">{{ $product->name }}</a>
Работа над каталогом
У нас каталог в публичной части пока что толком не реализован, есть лишь некоторые общие черты — как это должно работать. Например, нет постраничной навигации, не показываются дочерние категории (что затрудняет навигацию), нет хлебных крошек и так далее.
1. Дочерние категории
Для каждой категории каталога будем показывать дочерние категории для удобства навигации. Кроме того, для каждой категории будем показывать товары не только этой категории, но и товары, принадлежащие потомкам этой категории. Для этого изменим метод category()
контроллера CatalogController
.
class CatalogController extends Controller { public function category(Category $category) { // получаем всех потомков этой категории $descendants = $category->getAllChildren($category->id); $descendants[] = $category->id; // товары этой категории и всех потомков $products = Product::whereIn('category_id', $descendants)->paginate(6); return view('catalog.category', compact('category', 'products')); } }
Шаблон для показа категории views/catalog/category.blade.php
:
@extends('layout.site', ['title' => $category->name]) @section('content') <h1>{{ $category->name }}</h1> <p>{{ $category->content }}</p> <div class="row"> @foreach ($category->children as $child) @include('catalog.part.category', ['category' => $child]) @endforeach </div> <h5 class="bg-info text-white p-2 mb-4">Товары раздела</h5> <div class="row"> @foreach ($products as $product) @include('catalog.part.product', ['product' => $product]) @endforeach </div> {{ $products->links() }} @endsection
Вспомогательный шаблон views/catalog/part/category.blade.php
:
<div class="col-md-4 mb-4"> <div class="card list-item"> <div class="card-header"> <h3 class="mb-0">{{ $category->name }}</h3> </div> <div class="card-body p-0"> @if ($category->image) @php $url = url('storage/catalog/category/thumb/' . $category->image) @endphp <img src="{{ $url }}" class="img-fluid" alt=""> @else <img src="https://via.placeholder.com/300x150" class="img-fluid" alt=""> @endif </div> <div class="card-footer"> <a href="{{ route('catalog.category', ['category' => $category->slug]) }}" class="btn btn-dark">Товары раздела</a> </div> </div> </div>
Вспомогательный шаблон views/catalog/part/product.blade.php
:
<div class="col-md-4 mb-4"> <div class="card list-item"> <div class="card-header"> <h3 class="mb-0">{{ $product->name }}</h3> </div> <div class="card-body p-0"> @if ($product->image) @php($url = url('storage/catalog/product/thumb/' . $product->image)) <img src="{{ $url }}" class="img-fluid" alt=""> @else <img src="https://via.placeholder.com/300x150" class="img-fluid" alt=""> @endif </div> <div class="card-footer"> <!-- Форма для добавления товара в корзину --> <form action="{{ route('basket.add', ['id' => $product->id]) }}" method="post" class="d-inline"> @csrf <button type="submit" class="btn btn-success">В корзину</button> </form> <a href="{{ route('catalog.product', ['product' => $product->slug]) }}" class="btn btn-dark float-right">Смотреть</a> </div> </div> </div>
2. Главная страница каталога
Кроме корневых категорий будем показывать популярные бренды. Для этого изменим метод index()
контроллера CatalogController
.
class CatalogController extends Controller { public function index() { // корневые категории $roots = Category::where('parent_id', 0)->get(); // популярные бренды $brands = Brand::popular(); return view('catalog.index', compact('roots', 'brands')); } }
Шаблон для показа главной страницы каталога views/catalog/category.blade.php
:
@extends('layout.site', ['title' => 'Каталог товаров']) @section('content') <h1>Каталог товаров</h1> <p> Lorem ipsum dolor sit amet, consectetur adipisicing elit. Atque ducimus, eligendi exercitationem expedita, iure iusto laborum magnam qui quidem repellat similique tempora tempore ullam! Deserunt doloremque impedit quis repudiandae voluptas. </p> <h2 class="mb-4">Разделы каталога</h2> <div class="row"> @foreach ($roots as $root) @include('catalog.part.category', ['category' => $root]) @endforeach </div> <h2 class="mb-4">Популярные бренды</h2> <div class="row"> @foreach ($brands as $brand) @include('catalog.part.brand', ['brand' => $brand]) @endforeach </div> @endsection
Вспомогательный шаблон views/catalog/part/brand.blade.php
:
<div class="col-md-4 mb-4"> <div class="card list-item"> <div class="card-header px-1"> <h3 class="mb-0">{{ $brand->name }}</h3> </div> <div class="card-body p-0"> @if ($brand->image) @php($url = url('storage/catalog/brand/thumb/' . $brand->image)) <img src="{{ $url }}" class="img-fluid" alt=""> @else <img src="https://via.placeholder.com/300x150" class="img-fluid" alt=""> @endif </div> <div class="card-footer px-1"> <a href="{{ route('catalog.brand', ['brand' => $brand->slug]) }}" class="btn btn-dark">Товары бренда</a> </div> </div> </div>
3. Товары бренда
Добавим постраничную навигацию для просмотра товаров бренда. Для этого изменим метод brand()
контроллера CatalogController
.
class CatalogController extends Controller { public function brand(Brand $brand) { $products = $brand->products()->paginate(6); return view('catalog.brand', compact('brand', 'products')); } }
Шаблон для показа товаров бренда views/catalog/brand.blade.php
:
@extends('layout.site', ['title' => $brand->name]) @section('content') <h1>{{ $brand->name }}</h1> <p>{{ $brand->content }}</p> <h5 class="bg-info text-white p-1 mb-4">Товары бренда</h5> <div class="row"> @foreach ($products as $product) @include('catalog.part.product', ['product' => $product]) @endforeach </div> {{ $products->links() }} @endsection
Добавление в корзину
Мы сейчас добавляем товар в корзину с перезагрузкой страницы, что выглядит совсем уж архаично. Давайте это изменим и будем отправлять на сервер ajax-запрос. Для этого во всех формах добавления в корзину добавим css-класс add-to-basket
.
<!-- Форма для добавления товара в корзину --> <form action="{{ route('basket.add', ['id' => $product->id]) }}" method="post" class="form-inline add-to-basket"> @csrf <label for="input-quantity">Количество</label> <input type="text" name="quantity" id="input-quantity" value="1" class="form-control mx-2 w-25"> <button type="submit" class="btn btn-success">Добавить в корзину</button> </form>
<!-- Форма для добавления товара в корзину --> <form action="{{ route('basket.add', ['id' => $product->id]) }}" method="post" class="d-inline add-to-basket"> @csrf <button type="submit" class="btn btn-success">В корзину</button> </form>
Теперь редактируем файл js-скрипта site.js
:
jQuery(document).ready(function($) { /* * Общие настройки ajax-запросов, отправка на сервер csrf-токена */ $.ajaxSetup({ headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') } }); /* * Раскрытие и скрытие пунктов меню каталога в левой колонке */ $('#catalog-sidebar > ul ul').hide(); $('#catalog-sidebar .badge').on('click', function () { /* ... */ }); /* * Получение данных профиля пользователя при оформлении заказа */ $('form#profiles button[type="submit"]').hide(); // при выборе профиля отправляем ajax-запрос, чтобы получить данные $('form#profiles select').change(function () { /* ... */ }); /* * Добавление товара в корзину с помощью ajax-запроса без перезагрузки */ $('form.add-to-basket').submit(function (e) { // отменяем отправку формы стандартным способом e.preventDefault(); // получаем данные этой формы добавления в корзину var $form = $(this); var data = new FormData($form[0]); $.ajax({ url: $form.attr('action'), data: data, processData: false, contentType: false, type: 'POST', dataType: 'HTML', beforeSend: function () { var spinner = ' <span class="spinner-border spinner-border-sm"></span>'; $form.find('button').append(spinner); }, success: function(html) { $form.find('.spinner-border').remove(); $('#top-basket').html(html); } }); }); });
И изменяем метод add()
контроллера BasketController
, чтобы он мог обрабатывать и ajax-запросы.
class BasketController extends Controller { /** * Добавляет товар с идентификатором $id в корзину */ public function add(Request $request, $id) { $quantity = $request->input('quantity') ?? 1; $this->basket->increase($id, $quantity); if ( ! $request->ajax()) { // выполняем редирект обратно на ту страницу, // где была нажата кнопка «В корзину» return back(); } // в случае ajax-запроса возвращаем html-код корзины в правом // верхнем углу, чтобы заменить исходный html-код, потому что // теперь количество позиций будет другим $positions = $this->basket->products->count(); return view('basket.part.basket', compact('positions')); } }
Нам еще потребуется малюсенький шаблон views/basket/part/basket.blade.php
:
<a class="nav-link @if ($positions) text-success @endif" href="{{ route('basket.index') }}"> Корзина @if ($positions) ({{ $positions }}) @endif </a>
Этот фрагмент html-кода будет отправляться клиенту при ajax-запросе. А наш js-код этот фрагмент html-кода будет вставлять внутрь элемента #top-basket
. Как нетрудно догадаться, идентификатор #top-basket
надо прописать в layout-шаблоне site.blade.php
.
<!-- Этот блок расположен справа --> <ul class="navbar-nav ml-auto"> <li class="nav-item" id="top-basket"> <a class="nav-link @if ($positions) text-success @endif" href="{{ route('basket.index') }}"> Корзина @if ($positions) ({{ $positions }}) @endif </a> </li> @guest <!-- ..... --> @else <!-- ..... --> @endif </ul>
- Магазин на Laravel 7, часть 10. Форма оформления, сохранение заказа в базу данных
- Магазин на Laravel 7, часть 6. Изменение количества товара, удаление товара из корзины
- Магазин на Laravel 7, часть 25. Поиск по каталогу товаров, деплой проекта на хостинг TimeWeb
- Магазин на Laravel 7, часть 24. Фильтр товаров категории по цене, новинкам и лидерам продаж
- Магазин на Laravel 7, часть 23. Главная страница сайта, новинки, лидеры продаж и распродажа
- Магазин на Laravel 7, часть 21. Добавляем профили и используем их при оформлении заказа
- Магазин на Laravel 7, часть 20. Показ отдельной страницы и верхнее меню всех страниц
Поиск: Laravel • MySQL • PHP • Web-разработка • База данных • Интернет магазин • Каталог товаров • Корзина • Практика • Шаблон сайта