Магазин на Laravel 7, часть 12. Панель управления, создание и редактирование категорий
13.10.2020
Теги: Laravel • MySQL • PHP • Web-разработка • БазаДанных • ИнтернетМагазин • КаталогТоваров • ПанельУправления • Практика • Форма • Фреймворк • ШаблонСайта
Методы create() и edit()
Реализуем еще два метода контроллера — create()
и edit()
— для создания новой категории каталога и для редактирования существующей.
class CategoryController extends Controller { /* ... */ public function create() { // для возможности выбора родителя $parents = Category::roots(); return view('admin.category.create', compact('parents')); } /* ... */ public function edit(Category $category) { // для возможности выбора родителя $parents = Category::roots(); return view('admin.category.edit', compact('category', 'parents')); } /* ... */ }
Два новых шаблона
И создадим два шаблона в директории views/admin/category
— это файлы create.blade.php
и edit.blade.php
.
@extends('layout.admin') @section('content') <h1>Создание новой категории</h1> <form method="post" action="{{ route('admin.category.store') }}" enctype="multipart/form-data"> @csrf <div class="form-group"> <input type="text" class="form-control" name="name" placeholder="Наименование" required maxlength="100" value="{{ old('name') ?? '' }}"> </div> <div class="form-group"> <input type="text" class="form-control" name="slug" placeholder="ЧПУ (на англ.)" required maxlength="100" value="{{ old('slug') ?? '' }}"> </div> <div class="form-group"> <select name="parent_id" class="form-control" title="Родитель"> <option value="0">Без родителя</option> @if (count($parents)) @include('admin.category.part.branch', ['items' => $parents, 'level' => -1]) @endif </select> </div> <div class="form-group"> <textarea class="form-control" name="content" placeholder="Краткое описание" maxlength="200" rows="3">{{ old('content') ?? '' }}</textarea> </div> <div class="form-group"> <input type="file" class="form-control-file" name="image" accept="image/png, image/jpeg"> </div> <div class="form-group"> <button type="submit" class="btn btn-primary">Сохранить</button> </div> </form> @endsection
@extends('layout.admin') @section('content') <h1>Редактирование категории</h1> <form method="post" enctype="multipart/form-data" action="{{ route('admin.category.update', ['category' => $category->id]) }}"> @method('PUT') @csrf <div class="form-group"> <input type="text" class="form-control" name="name" placeholder="Наименование" required maxlength="100" value="{{ old('name') ?? $category->name }}"> </div> <div class="form-group"> <input type="text" class="form-control" name="title" placeholder="ЧПУ (на англ.)" required maxlength="100" value="{{ old('slug') ?? $category->slug }}"> </div> <div class="form-group"> <select name="parent_id" class="form-control" title="Родитель"> <option value="0">Без родителя</option> @if (count($parents)) @include('admin.category.part.branch', ['items' => $parents, 'level' => -1]) @endif </select> </div> <div class="form-group"> <textarea class="form-control" name="content" placeholder="Краткое описание" maxlength="200" rows="3">{{ old('content') ?? $category->content }}</textarea> </div> <div class="form-group"> <input type="file" class="form-control-file" name="image" accept="image/png, image/jpeg"> </div> @isset($category->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> </form> @endsection
Для показа списка выбора родителя, мы рекурсивно подключаем шаблон views/admin/catgory/part/branch.blade.php
:
@php $level++ @endphp @foreach($items as $item) <option value="{{ $item->id }}"> @if ($level) {!! str_repeat(' ', $level) !!} @endif {{ $item->name }} </option> @if ($item->children->count()) @include('admin.category.part.branch', ['items' => $item->children, 'level' => $level]) @endif @endforeach
Оптимизация шаблонов
Не слишком удачно, что у нас две почти одинаковые формы в двух шаблонах. Давайте создадим еще один шаблон form.blade.php
в директории view/admin/category/part
.
@csrf <div class="form-group"> <input type="text" class="form-control" name="name" placeholder="Наименование" required maxlength="100" value="{{ old('name') ?? $category->name ?? '' }}"> </div> <div class="form-group"> <input type="text" class="form-control" name="slug" placeholder="ЧПУ (на англ.)" required maxlength="100" value="{{ old('slug') ?? $category->slug ?? '' }}"> </div> <div class="form-group"> @php $parent_id = old('parent_id') ?? $category->parent_id ?? 0; @endphp <select name="parent_id" class="form-control" title="Родитель"> <option value="0">Без родителя</option> @if (count($parents)) @include('admin.category.part.branch', ['items' => $parents, 'level' => -1]) @endif </select> </div> <div class="form-group"> <textarea class="form-control" name="content" placeholder="Краткое описание" maxlength="200" rows="3">{{ old('content') ?? $category->content ?? '' }}</textarea> </div> <div class="form-group"> <input type="file" class="form-control-file" name="image" accept="image/png, image/jpeg"> </div> @isset($category->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>
@php $level++ @endphp @foreach($items as $item) <option value="{{ $item->id }}" @if ($item->id == $parent_id) selected @endif> @if ($level) {!! str_repeat(' ', $level) !!} @endif {{ $item->name }} </option> @if ($item->children->count()) @include('admin.category.part.branch', ['items' => $item->children, 'level' => $level]) @endif @endforeach
Тогда два шаблона для создания новой категории и редактирования существующей будут совсем маленькими:
@extends('layout.admin') @section('content') <h1>Создание новой категории</h1> <form method="post" action="{{ route('admin.category.store') }}" enctype="multipart/form-data"> @include('admin.category.part.form') </form> @endsection
@extends('layout.admin') @section('content') <h1>Редактирование категории</h1> <form method="post" enctype="multipart/form-data" action="{{ route('admin.category.update', ['category' => $category->id]) }}"> @method('PUT') @include('admin.category.part.form') </form> @endsection
Методы store() и update()
Поскольку мы планируем использовать «mass assignment», нужно добавить свойство $fillable
в модель Category
:
class Category extends Model { protected $fillable = [ 'parent_id', 'name', 'slug', 'content', 'image', ]; /* ... */ }
А методы store()
и update()
будут достаточно простыми, пока не займемся загрузкой и обрезкой изображений.
class CategoryController extends Controller { /* ... */ public function store(Request $request) { // проверяем данные формы создания категории $this->validate($request, [ 'parent_id' => 'integer', 'name' => 'required|max:100', 'slug' => 'required|max:100|unique:categories,slug|alpha_dash', 'image' => 'mimes:jpeg,jpg,png|max:5000' ]); // проверка пройдена, сохраняем категорию $category = Category::create($request->all()); return redirect() ->route('admin.category.show', ['category' => $category->id]) ->with('success', 'Новая категория успешно создана'); } /* ... */ public function update(Request $request, Category $category) { // проверяем данные формы редактирования категории $id = $category->id; $this->validate($request, [ 'parent_id' => 'integer', 'name' => 'required|max:100', /* * Проверка на уникальность slug, исключая эту категорию по идентифкатору: * 1. categories — таблица базы данных, где проверяется уникальность * 2. slug — имя колонки, уникальность значения которой проверяется * 3. значение, по которому из проверки исключается запись таблицы БД * 4. поле, по которому из проверки исключается запись таблицы БД * Для проверки будет использован такой SQL-запрос к базе данныхЖ * SELECT COUNT(*) FROM `categories` WHERE `slug` = '...' AND `id` <> 17 */ 'slug' => 'required|max:100|unique:categories,slug,'.$id.',id|alpha_dash', 'image' => 'mimes:jpeg,jpg,png|max:5000' ]); // проверка пройдена, обновляем категорию $category->update($request->all()); return redirect() ->route('admin.category.show', ['category' => $category->id]) ->with('success', 'Категория была успешно исправлена'); } /* ... */ }
Нехорошо заставлять администратора заполнять поле «ЧПУ (англ)» — давайте сами заполнять это поле на основе названия категории. Для этого в файл public/js/admin.js
добавляем следующий код.
jQuery(document).ready(function($) { $('input[name="name"]').on('input', function() { var map = { 'А': 'A', 'Б': 'B', 'В': 'V', 'Г': 'G', 'Д': 'D', 'Е': 'E', 'Ё': 'Yo', 'Ж': 'Zh', 'З': 'Z', 'И': 'I', 'Й': 'J', 'К': 'K', 'Л': 'L', 'М': 'M', 'Н': 'N', 'О': 'O', 'П': 'P', 'Р': 'R', 'С': 'S', 'Т': 'T', 'У': 'U', 'Ф': 'F', 'Х': 'H', 'Ц': 'C', 'Ч': 'Ch', 'Ш': 'Sh', 'Щ': 'Sh', 'Ъ': '', 'Ы': 'Y', 'Ь': '', 'Э': 'E', 'Ю': 'Yu', 'Я': 'Ya', 'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'yo', 'ж': 'zh', 'з': 'z', 'и': 'i', 'й': 'j', 'к': 'k', 'л': 'l', 'м': 'm', 'н': 'n', 'о': 'o', 'п': 'p', 'р': 'r', 'с': 's', 'т': 't', 'у': 'u', 'ф': 'f', 'х': 'h', 'ц': 'c', 'ч': 'ch', 'ш': 'sh', 'щ': 'sh', 'ъ': '', 'ы': 'y', 'ь': '', 'э': 'e', 'ю': 'yu', 'я': 'ya', }; var text = $(this).val(); for (var k in map) { text = text.replace(RegExp(k, 'g'), map[k]); } text = text.replace(/[^- _a-zA-Z0-9]/g, ''); text = text.replace(/\s+/g, '-'); text = text.replace(/-+/g, '-'); $('input[name="slug"]').val(text); }); });
Небольшое отступление
Прежде, чем двигаться дальше, давайте посмотрим, как в Laravel организовано хранение и загрузка файлов. Это нам потребуется, чтобы организовать загрузку и хранение изображений для категорий, брендов и товаров.
Файловое хранилище
Сразу после установки Laravel доступны диски local
и public
, использующие драйвер local
. Для диска local
место хранения — директория storage/app
, для диска public
место хранения — директория storage/app/public
. Диск local
является диском по умолчанию, это задается в файле настроек config/filesystems.php
.
return [ /* ... */ 'default' => env('FILESYSTEM_DRIVER', 'local'), /* ... */ 'disks' => [ 'local' => [ 'driver' => 'local', 'root' => storage_path('app'), ], 'public' => [ 'driver' => 'local', 'root' => storage_path('app/public'), 'url' => env('APP_URL').'/storage', 'visibility' => 'public', ], ], /* ... */ 'links' => [ public_path('storage') => storage_path('app/public'), ], /* ... */ ];
Чтобы сделать файлы диска public
доступными через веб, надо создать символьную ссылку из public/storage
на storage/app/public
. Директория public
проекта Laravel — является корневой директорией сервера, поэтому файл storage/app/public/image.jpg
будет доступен через веб как http://server.com/storage/image.jpg
.
> php artisan storage:link
Какие символьные ссылки создавать — задается в файле конфигурации, см. выше. Когда файл сохранён на диске и создана символьная ссылка, можно создать URL к файлу с помощью хелпера asset()
или метода url()
фасада Storage
:
<img src="{{ asset('storage/images/image.jpg') }}" alt="" />
<img src="{{ Storage::disk('public')->url('images/image.jpg') }}" alt="" />
При использовании диска local
при вызове метода Storage::url()
будет возвращен URL вида /storage/images/image.jpg
.
При использовании драйвера local
все файловые операции выполняются относительно директории root
, определенной в конфигурационном файле. Для диска local
директория root
— это storage/app
, для диска public
директория root
— это storage/app/public
.
// файл будет сохранен в storage/app/data/file.txt Storage::disk('local')->put('data/file.txt', 'Some file content');
Загрузка файлов
В Laravel очень просто сохранять загружаемые файлы методом store()
на экземпляре загружаемого файла:
namespace App\Http\Controllers; use Illuminate\Http\Request; use App\Http\Controllers\Controller; class UserAvatarController extends Controller { /** * Обновление аватара пользователя. */ public function update(Request $request) { // будет сохранен как storage/app/avatars/L6ceL...xzXFw.jpeg $path = $request->file('avatar')->store('avatars'); return $path; } }
Мы указываем только директорию avatars
, а имя файла будет сформировано автоматически. Метод вернёт путь к файлу, поэтому можно сохранить в БД весь путь, включая сгенерированное имя. Файл будет сохранен на диск по умолчанию (и не будет доступен из веб), но можно указать диск вторым аргументом метода store()
.
// будет сохранен как storage/app/public/avatars/L6ceL...xzXFw.jpeg и будет // доступен из веб как http://server.com/storage/avatars/L6ceL...xzXFw.jpeg $path = $request->file('avatar')->store('avatars', 'public');
Чтобы задать свое имя файла и (опционально) диск для сохранения, можно использовать метод storeAs()
:
// будет использован диск по умолчанию $path = $request->file('avatar')->storeAs( 'avatars', // директория, куда сохранять $request->user()->id // имя файла );
// явное указание диска для сохранения $path = $request->file('avatar')->storeAs( 'avatars', // директория, куда сохранять $request->user()->id, // имя файла 'public' // диск, куда сохранять );
- Магазин на Laravel 7, часть 13. Панель управления, обрезка изображения и валидация данных
- Магазин на Laravel 7, часть 24. Фильтр товаров категории по цене, новинкам и лидерам продаж
- Магазин на Laravel 7, часть 18. Панель управления, пользователи и CRUD страниц сайта
- Магазин на Laravel 7, часть 17. Панель управления, работа с заказами, изменение статуса
- Магазин на Laravel 7, часть 16. Панель управления, CRUD-операции для товаров каталога
- Магазин на Laravel 7, часть 15. Панель управления, добавление и редактирование брендов
- Магазин на Laravel 7, часть 10. Форма оформления, сохранение заказа в базу данных
Поиск: Laravel • MySQL • PHP • Web-разработка • База данных • Интернет магазин • Каталог товаров • Панель управления • Практика • Форма • Фреймворк • Шаблон сайта