Магазин на Laravel 7, часть 16. Панель управления, CRUD-операции для товаров каталога
24.10.2020
Теги: Laravel • MySQL • PHP • Web-разработка • БазаДанных • ИнтернетМагазин • КаталогТоваров • ПанельУправления • Практика • Фреймворк • ШаблонСайта
С категориями и брендами разобрались, настала очередь товаров каталога. Нам опять нужен ресурсный контроллер, который создадим с помощью artisan-команды. Еще потребуется семь маршртутов, отвечающих за CRUD-операции над товарами и свойство $fillable
для модели, поскольку будем использовать «mass assignment». Ну и шаблоны для просмотра списка, отдельного товара, страницы добавления и редактирования товара.
Создаем ресурсный контроллер:
> php artisan make:controller Admin/ProductController --resource --model=Models/Product
namespace App\Http\Controllers\Admin; use App\Helpers\ImageSaver; use App\Http\Controllers\Controller; use App\Models\Brand; use App\Models\Category; use App\Models\Product; use Illuminate\Http\Request; class ProductController extends Controller { private $imageSaver; public function __construct(ImageSaver $imageSaver) { $this->imageSaver = $imageSaver; } /** * Показывает список всех товаров каталога * * @return \Illuminate\Http\Response */ public function index() { $products = Product::paginate(5); return view('admin.product.index', compact('products')); } /** * Показывает форму для создания товара * * @return \Illuminate\Http\Response */ public function create() { // все категории для возможности выбора родителя $items = Category::all(); // все бренды для возмозжности выбора подходящего $brands = Brand::all(); return view('admin.product.create', compact('items', 'brands')); } /** * Сохраняет новый товар в базу данных * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function store(Request $request) { $data = $request->all(); $data['image'] = $this->imageSaver->upload($request, null, 'product'); $product = Product::create($data); return redirect() ->route('admin.product.show', ['product' => $product->id]) ->with('success', 'Новый товар успешно создан'); } /** * Показывает страницу товара каталога * * @param \App\Models\Product $product * @return \Illuminate\Http\Response */ public function show(Product $product) { return view('admin.product.show', compact('product')); } /** * Показывает форму для редактирования товара * * @param \App\Models\Product $product * @return \Illuminate\Http\Response */ public function edit(Product $product) { // все категории для возможности выбора родителя $items = Category::all(); // все бренды для возмозжности выбора подходящего $brands = Brand::all(); return view('admin.product.edit', compact('product', 'items', 'brands')); } /** * Обновляет товар каталога в базе данных * * @param \Illuminate\Http\Request $request * @param \App\Models\Product $product * @return \Illuminate\Http\Response */ public function update(Request $request, Product $product) { $data = $request->all(); $data['image'] = $this->imageSaver->upload($request, $product, 'product'); $product->update($data); return redirect() ->route('admin.product.show', ['product' => $product->id]) ->with('success', 'Товар был успешно обновлен'); } /** * Удаляет товар каталога из базы данных * * @param \App\Models\Product $product * @return \Illuminate\Http\Response */ public function destroy(Product $product) { $this->imageSaver->remove($product, 'product'); $product->delete(); return redirect() ->route('admin.category.index') ->with('success', 'Товар каталога успешно удален'); } }
Добавляем свойство $fillable
в модель:
class Category extends Model { protected $fillable = [ 'parent_id', 'name', 'slug', 'content', 'image', ]; /* ... */ }
Прописываем маршруты в файле web.php
:
Route::group([ 'as' => 'admin.', // имя маршрута, например admin.index 'prefix' => 'admin', // префикс маршрута, например admin/index 'namespace' => 'Admin', // пространство имен контроллера 'middleware' => ['auth', 'admin'] // один или несколько посредников ], function () { // главная страница панели управления Route::get('index', 'IndexController')->name('index'); // CRUD-операции над категориями каталога Route::resource('category', 'CategoryController'); // CRUD-операции над брендами каталога Route::resource('brand', 'BrandController'); // CRUD-операции над товарами каталога Route::resource('product', 'ProductController'); });
Создаем шаблон views/admin/product/index.blade.php
:
@extends('layout.admin', ['title' => 'Все товары каталога']) @section('content') <h1>Все товары</h1> <a href="{{ route('admin.product.create') }}" class="btn btn-success mb-4"> Создать товар </a> <table class="table table-bordered"> <tr> <th width="30%">Наименование</th> <th width="65%">Описание</th> <th><i class="fas fa-edit"></i></th> <th><i class="fas fa-trash-alt"></i></th> </tr> @foreach ($products as $product) <tr> <td> <a href="{{ route('admin.product.show', ['product' => $product->id]) }}"> {{ $product->name }} </a> </td> <td>{{ iconv_substr($product->content, 0, 150) }}</td> <td> <a href="{{ route('admin.product.edit', ['product' => $product->id]) }}"> <i class="far fa-edit"></i> </a> </td> <td> <form action="{{ route('admin.product.destroy', ['product' => $product->id]) }}" method="post" onsubmit="return confirm('Удалить этот товар?')"> @csrf @method('DELETE') <button type="submit" class="m-0 p-0 border-0 bg-transparent"> <i class="far fa-trash-alt text-danger"></i> </button> </form> </td> </tr> @endforeach </table> {{ $products->links() }} @endsection
Создаем шаблон views/admin/product/show.blade.php
:
@extends('layout.admin', ['title' => 'Просмотр товара']) @section('content') <h1>Просмотр товара</h1> <div class="row"> <div class="col-md-6"> <p><strong>Название:</strong> {{ $product->name }}</p> <p><strong>ЧПУ (англ):</strong> {{ $product->slug }}</p> <p><strong>Бренд:</strong> {{ $product->brand->name }}</p> <p><strong>Категория:</strong> {{ $product->category->name }}</p> </div> <div class="col-md-6"> @php if ($product->image) { $url = url('storage/catalog/product/image/' . $product->image); } else { $url = url('storage/catalog/product/image/default.jpg'); } @endphp <img src="{{ $url }}" alt="" class="img-fluid"> </div> </div> <div class="row"> <div class="col-12"> <p><strong>Описание</strong></p> @isset($product->content) <p>{{ $product->content }}</p> @else <p>Описание отсутствует</p> @endisset <a href="{{ route('admin.product.edit', ['product' => $product->id]) }}" class="btn btn-success"> Редактировать товар </a> <form method="post" class="d-inline" onsubmit="return confirm('Удалить этот товар?')" action="{{ route('admin.product.destroy', ['product' => $product->id]) }}"> @csrf @method('DELETE') <button type="submit" class="btn btn-danger"> Удалить товар </button> </form> </div> </div> @endsection
Создаем шаблон views/admin/product/create.blade.php
:
@extends('layout.admin', ['title' => 'Создание товара']) @section('content') <h1>Создание нового товара</h1> <form method="post" action="{{ route('admin.product.store') }}" enctype="multipart/form-data"> @include('admin.product.part.form') </form> @endsection
Создаем шаблон views/admin/product/edit.blade.php
:
@extends('layout.admin', ['title' => 'Редактирование товара']) @section('content') <h1>Редактирование товара</h1> <form method="post" enctype="multipart/form-data" action="{{ route('admin.product.update', ['product' => $product->id]) }}"> @method('PUT') @include('admin.product.part.form') </form> @endsection
Создаем шаблон views/admin/product/part/form.blade.php
:
@csrf <div class="form-group"> <input type="text" class="form-control" name="name" placeholder="Наименование" required maxlength="100" value="{{ old('name') ?? $product->name ?? '' }}"> </div> <div class="form-group"> <input type="text" class="form-control" name="slug" placeholder="ЧПУ (на англ.)" required maxlength="100" value="{{ old('slug') ?? $product->slug ?? '' }}"> </div> <div class="form-group"> @php $category_id = old('category_id') ?? $product->category_id ?? 0; @endphp <select name="category_id" class="form-control" title="Категория"> <option value="0">Выберите</option> @if (count($items)) @include('admin.product.part.branch', ['level' => -1, 'parent' => 0]) @endif </select> </div> <div class="form-group"> @php $brand_id = old('brand_id') ?? $product->brand_id ?? 0; @endphp <select name="brand_id" class="form-control" title="Бренд"> <option value="0">Выберите</option> @foreach($brands as $brand) <option value="{{ $brand->id }}" @if ($brand->id == $brand_id) selected @endif> {{ $brand->name }} </option> @endforeach </select> </div> <div class="form-group"> <textarea class="form-control" name="content" placeholder="Описание" rows="4">{{ old('content') ?? $product->content ?? '' }}</textarea> </div> <div class="form-group"> <input type="file" class="form-control-file" name="image" accept="image/png, image/jpeg"> </div> @isset($product->image) <div class="form-group form-check"> <input type="checkbox" class="form-check-input" name="remove" id="remove"> <label class="form-check-label" for="remove"> Удалить загруженное изображение </label> </div> @endisset <div class="form-group"> <button type="submit" class="btn btn-primary">Сохранить</button> </div>
Создаем шаблон views/admin/product/part/branch.blade.php
:
@php $level++ @endphp @foreach ($items->where('parent_id', $parent) as $item) <option value="{{ $item->id }}" @if ($item->id == $category_id) selected @endif> @if ($level) {!! str_repeat(' ', $level) !!} @endif {{ $item->name }} </option> @if (count($items->where('parent_id', $parent))) @include('admin.product.part.branch', ['level' => $level, 'parent' => $item->id]) @endif @endforeach
Работать в админке сейчас не очень удобно, потому что список товаров может содержать сотни и тысячи элементов. Так что найти нужный для редактирования товар довольно проблематично — нужна какая-то навигация. Давайте на странице списка товаров будем показывать корневые категории. И создадим еще один метод в контроллере, который будет показывать товары выбранной категории. Таким образом, у нас будет навигация, которая позволит перейти внутрь корневой категории, оттуда перейти в дочернюю категорию этой корневой и так далее.
class ProductController extends Controller { /* ... */ public function index() { // корневые категории для возможности навигации $roots = Category::where('parent_id', 0)->get(); $products = Product::paginate(5); return view('admin.product.index', compact('products', 'roots')); } /** * Показывает товары выбранной категории * * @return \Illuminate\Http\Response */ public function category(Category $category) { $products = $category->products()->paginate(5); return view('admin.product.category', compact('category', 'products')); } /* ... */ }
Еще один маршрут для просмотра товаров категории:
Route::group([ 'as' => 'admin.', // имя маршрута, например admin.index 'prefix' => 'admin', // префикс маршрута, например admin/index 'namespace' => 'Admin', // пространство имен контроллера 'middleware' => ['auth', 'admin'] // один или несколько посредников ], function () { // главная страница панели управления Route::get('index', 'IndexController')->name('index'); // CRUD-операции над категориями каталога Route::resource('category', 'CategoryController'); // CRUD-операции над брендами каталога Route::resource('brand', 'BrandController'); // CRUD-операции над товарами каталога Route::resource('product', 'ProductController'); // доп.маршрут для просмотра товаров категории Route::get('product/category/{category}', 'ProductController@category') ->name('product.category'); });
Редактируем шаблон views/admin/product/index.blade.php
:
@extends('layout.admin', ['title' => 'Все товары каталога']) @section('content') <h1>Все товары</h1> <!-- Корневые категории для возможности навигации --> <ul> @foreach ($roots as $root) <li> <a href="{{ route('admin.product.category', ['category' => $root->id]) }}"> {{ $root->name }} </a> </li> @endforeach </ul> <a href="{{ route('admin.product.create') }}" class="btn btn-success mb-4"> Создать товар </a> <table class="table table-bordered"> <!-- ..... --> </table> {{ $products->links() }} @endsection
Создаем шаблон views/admin/product/category.blade.php
:
@extends('layout.admin', ['title' => 'Товары категории']) @section('content') <h1>{{ $category->name }}</h1> <!-- Дочерние категории для возможности навигации --> <ul> @foreach ($category->children as $child) <li> <a href="{{ route('admin.product.category', ['category' => $child->id]) }}"> {{ $child->name }} </a> </li> @endforeach </ul> <a href="{{ route('admin.product.create') }}" class="btn btn-success mb-4"> Создать товар </a> <!-- Список товаров выбранной категории --> @if (count($products)) <table class="table table-bordered"> <tr> <th width="30%">Наименование</th> <th width="65%">Описание</th> <th><i class="fas fa-edit"></i></th> <th><i class="fas fa-trash-alt"></i></th> </tr> @foreach ($products as $product) <tr> <td> <a href="{{ route('admin.product.show', ['product' => $product->id]) }}"> {{ $product->name }} </a> </td> <td>{{ iconv_substr($product->content, 0, 150) }}</td> <td> <a href="{{ route('admin.product.edit', ['product' => $product->id]) }}"> <i class="far fa-edit"></i> </a> </td> <td> <form action="{{ route('admin.product.destroy', ['product' => $product->id]) }}" method="post"> @csrf @method('DELETE') <button type="submit" class="m-0 p-0 border-0 bg-transparent"> <i class="far fa-trash-alt text-danger"></i> </button> </form> </td> </tr> @endforeach </table> {{ $products->links() }} @else <p>Нет товаров в этой категории</p> @endif @endsection
Создаем класс для валидации:
> php artisan make:request ProductCatalogRequest
namespace App\Http\Requests; class ProductCatalogRequest extends CatalogRequest { /** * С какой сущностью сейчас работаем (товар каталога) * @var array */ protected $entity = [ 'name' => 'product', 'table' => 'products' ]; public function authorize() { return parent::authorize(); } public function rules() { return parent::rules(); } /** * Объединяет дефолтные правила и правила, специфичные для товара * для проверки данных при добавлении нового товара */ protected function createItem() { $rules = [ 'category_id' => [ 'required', 'integer', 'min:1' ], 'brand_id' => [ 'required', 'integer', 'min:1' ], ]; return array_merge(parent::createItem(), $rules); } /** * Объединяет дефолтные правила и правила, специфичные для товара * для проверки данных при обновлении существующего товара */ protected function updateItem() { $rules = [ 'category_id' => [ 'required', 'integer', 'min:1' ], 'brand_id' => [ 'required', 'integer', 'min:1' ], ]; return array_merge(parent::updateItem(), $rules); } }
Изменяем «type hinting» в контроллере:
namespace App\Http\Controllers\Admin; use App\Helpers\ImageSaver; use App\Http\Controllers\Controller; use App\Http\Requests\ProductCatalogRequest; use App\Models\Brand; use App\Models\Category; use App\Models\Product; class ProductController extends Controller { /* ... */ public function store(ProductCatalogRequest $request) { /* ... */ } /* ... */ public function update(ProductCatalogRequest $request, Brand $brand) { /* ... */ } /* ... */ }
Для товара обязательно должны быть заполнены поля «Категория» и «Бренд». Но сообщение об ошибке совершенно не информативно.
Зададим свои сообщения в файле lang/ru/validation.php
, если не выбраны категория и/или бренд:
return [ 'custom' => [ /* ... */ 'category_id' => [ 'required' => 'Поле «:attribute» обязательно для заполнения', 'integer' => 'Поле «:attribute» должно быть целым положительным числом', 'min' => 'Поле «:attribute» обязательно для заполнения', ], 'brand_id' => [ 'required' => 'Поле «:attribute» обязательно для заполнения', 'integer' => 'Поле «:attribute» должно быть целым положительным числом', 'min' => 'Поле «:attribute» обязательно для заполнения', ], ], 'attributes' => [ 'name' => 'Имя, Фамилия', 'slug' => 'ЧПУ (англ)', 'email' => 'Адрес почты', 'password' => 'Пароль', 'password_confirmation' => 'Подтверждение пароля', 'address' => 'Адрес доставки', 'phone' => 'Номер телефона', /* ... */ 'parent_id' => 'Родитель', 'category_id' => 'Категория', 'brand_id' => 'Бренд', ], ]
- Магазин на Laravel 7, часть 18. Панель управления, пользователи и CRUD страниц сайта
- Магазин на Laravel 7, часть 17. Панель управления, работа с заказами, изменение статуса
- Магазин на Laravel 7, часть 15. Панель управления, добавление и редактирование брендов
- Магазин на Laravel 7, часть 13. Панель управления, обрезка изображения и валидация данных
- Магазин на Laravel 7, часть 12. Панель управления, создание и редактирование категорий
- Магазин на Laravel 7, часть 24. Фильтр товаров категории по цене, новинкам и лидерам продаж
- Магазин на Laravel 7, часть 23. Главная страница сайта, новинки, лидеры продаж и распродажа
Поиск: Laravel • MySQL • PHP • Web-разработка • База данных • Интернет магазин • Каталог товаров • Панель управления • Практика • Фреймворк • Шаблон сайта