Магазин на Laravel 7, часть 13. Панель управления, обрезка изображения и валидация данных
17.10.2020
Теги: Laravel • MySQL • PHP • Web-разработка • БазаДанных • ИнтернетМагазин • КаталогТоваров • ПанельУправления • Практика • Форма • Фреймворк • ШаблонСайта
Загрузка изображения
Ну вот, теперь можно вернуться к методам store()
и update()
контроллера и организовать загрузку и дальнейшее хранение изображения для категории каталога. Первым делом выполняем в консоли artisan-команду, которая создаст символическую ссылку.
> php artisan storage:link
Для начала просто сохраним изображение на диск и запишем имя файла изображения в базу данных:
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|regex:~^[-_a-z0-9]+$~i', 'image' => 'mimes:jpeg,jpg,png|max:5000' ]); /* * Проверка пройдена, создаем категорию */ $file = $request->file('image'); if ($file) { // был загружен файл изображения $path = $file->store('catalog/category/source', 'public'); $base = basename($path); } $data = $request->all(); $data['image'] = $base ?? null; $category = Category::create($data); return redirect() ->route('admin.category.show', ['category' => $category->id]) ->with('success', 'Новая категория успешно создана'); } /* ... */ }
class CategoryController extends Controller { /* ... */ 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|regex:~^[-_a-z0-9]+$~i', 'image' => 'mimes:jpeg,jpg,png|max:5000' ]); /* * Проверка пройдена, обновляем категорию */ if ($request->remove) { // если надо удалить изображение $old = $category->image; if ($old) { Storage::disk('public')->delete('catalog/category/source/' . $old); } } $file = $request->file('image'); if ($file) { // был загружен файл изображения $path = $file->store('catalog/category/source', 'public'); $base = basename($path); // удаляем старый файл изображения $old = $category->image; if ($old) { Storage::disk('public')->delete('catalog/category/source/' . $old); } } $data = $request->all(); $data['image'] = $base ?? null; $category->update($data); return redirect() ->route('admin.category.show', ['category' => $category->id]) ->with('success', 'Категория была успешно исправлена'); } /* ... */ }
Обрезка изображения
Изображение может быть слишком большим и не подходить для использования на сайте из-за неподходящих пропорций. Мы будем создавать из исходного изображения еще два — размером 600x300px и размером 300x150px. Для этого установим пакет intervention/image
с помощью composer
.
> composer require intervention/image
Открываем файл config/app.php
и добавляем следующие строки:
return [ /* ... */ 'providers' => [ /* ... */ Intervention\Image\ImageServiceProvider::class, ], 'aliases' => [ /* ... */ 'Image' => Intervention\Image\Facades\Image::class, ] /* ... */ ]
Поскольку обрезать и сохранять изображения нам придется еще и для брендов и товаров — создадим отдельный класс ImageSaver
в директории app/Helpers
.
namespace App\Helpers; use Illuminate\Http\Request; use Illuminate\Support\Facades\Storage; use Intervention\Image\Facades\Image; class ImageSaver { /** * Сохраняет изображение при создании или редактировании категории, * бренда или товара; создает два уменьшенных изображения. * * @param \Illuminate\Http\Request $request — объект HTTP-запроса * @param \App\Models\Item $item — модель категории, бренда или товара * @param string $dir — директория, куда будем сохранять изображение * @return string|null — имя файла изображения для сохранения в БД */ public function upload($request, $item, $dir) { $name = $item->image ?? null; if ($item && $request->remove) { // если надо удалить изображение $this->remove($item, $dir); $name = null; } $source = $request->file('image'); if ($source) { // если было загружено изображение // перед загрузкой нового изображения удаляем старое if ($item && $item->image) { $this->remove($item, $dir); } $ext = $source->extension(); // сохраняем загруженное изображение без всяких изменений $path = $source->store('catalog/'.$dir.'/source', 'public'); $path = Storage::disk('public')->path($path); // абсолютный путь $name = basename($path); // имя файла // создаем уменьшенное изображение 600x300px, качество 100% $dst = 'catalog/'.$dir.'/image/'; $this->resize($path, $dst, 600, 300, $ext); // создаем уменьшенное изображение 300x150px, качество 100% $dst = 'catalog/'.$dir.'/thumb/'; $this->resize($path, $dst, 300, 150, $ext); } return $name; } /** * Создает уменьшенную копию изображения * * @param string $src — путь к исходному изображению * @param string $dst — куда сохранять уменьшенное * @param integer $width — ширина в пикселях * @param integer $height — высота в пикселях * @param string $ext — расширение уменьшенного */ private function resize($src, $dst, $width, $height, $ext) { // создаем уменьшенное изображение width x height, качество 100% $image = Image::make($src) ->heighten($height) ->resizeCanvas($width, $height, 'center', false, 'eeeeee') ->encode($ext, 100); // сохраняем это изображение под тем же именем, что исходное $name = basename($src); Storage::disk('public')->put($dst . $name, $image); $image->destroy(); } /** * Удаляет изображение при удалении категории, бренда или товара * * @param \App\Models\Item $item — модель категории, бренда или товара * @param string $dir — директория, в которой находится изображение */ public function remove($item, $dir) { $old = $item->image; if ($old) { Storage::disk('public')->delete('catalog/'.$dir.'/source/' . $old); Storage::disk('public')->delete('catalog/'.$dir.'/image/' . $old); Storage::disk('public')->delete('catalog/'.$dir.'/thumb/' . $old); } } }
Внедряем зависимость в класс контроллера, чтобы иметь доступ к созданному классу и изменяем код методов store()
и update()
, избавляясь от всего лишнего.
namespace App\Http\Controllers\Admin; use App\Helpers\ImageSaver; use App\Http\Controllers\Controller; use App\Models\Category; use Illuminate\Http\Request; class CategoryController extends Controller { private $imageSaver; public function __construct(ImageSaver $imageSaver) { $this->imageSaver = $imageSaver; } /** * Показывает список всех категорий * * @return \Illuminate\Http\Response * @throws \Illuminate\Contracts\Container\BindingResolutionException */ public function index() { $roots = Category::roots(); return view('admin.category.index', compact('roots')); } /** * Показывает форму для создания категории * * @return \Illuminate\Http\Response */ public function create() { // для возможности выбора родителя $parents = Category::roots(); return view('admin.category.create', compact('parents')); } /** * Сохраняет новую категорию в базу данных * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function store(Request $request) { /* * Проверяем данные формы создания категории */ $this->validate($request, [ 'parent_id' => 'integer', 'name' => 'required|max:100', 'slug' => 'required|max:100|unique:categories,slug|regex:~^[-_a-z0-9]+$~i', 'image' => 'mimes:jpeg,jpg,png|max:5000' ]); /* * Проверка пройдена, создаем категорию */ $data = $request->all(); $data['image'] = $this->imageSaver->upload($request, null, 'category'); $category = Category::create($data); return redirect() ->route('admin.category.show', ['category' => $category->id]) ->with('success', 'Новая категория успешно создана'); } /** * Показывает страницу категории * * @param \App\Models\Category $category * @return \Illuminate\Http\Response */ public function show(Category $category) { return view('admin.category.show', compact('category')); } /** * Показывает форму для редактирования категории * * @param \App\Models\Category $category * @return \Illuminate\Http\Response */ public function edit(Category $category) { // для возможности выбора родителя $parents = Category::roots(); return view('admin.category.edit',compact('category', 'parents')); } /** * Обновляет категорию каталога * * @param \Illuminate\Http\Request $request * @param \App\Models\Category $category * @return \Illuminate\Http\Response */ 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|regex:~^[-_a-z0-9]+$~i', 'image' => 'mimes:jpeg,jpg,png|max:5000' ]); /* * Проверка пройдена, обновляем категорию */ $data = $request->all(); $data['image'] = $this->imageSaver->upload($request, $category, 'category'); $category->update($data); return redirect() ->route('admin.category.show', ['category' => $category->id]) ->with('success', 'Категория была успешно исправлена'); } /** * Удаляет категорию каталога * * @param \App\Models\Category $category * @return \Illuminate\Http\Response */ public function destroy(Category $category) { if ($category->children->count()) { $errors[] = 'Нельзя удалить категорию с дочерними категориями'; } if ($category->products->count()) { $errors[] = 'Нельзя удалить категорию, которая содержит товары'; } if (!empty($errors)) { return back()->withErrors($errors); } $this->imageSaver->remove($category, 'category'); $category->delete(); return redirect() ->route('admin.category.index') ->with('success', 'Категория каталога успешно удалена'); } }
Структура директорий, где мы сохраняем исходное изображение и его уменьшенные копии:
[storage] [app] [public] доступна из веб через символическую ссылку [catalog] [category] [source] bt8QPiw96zrwl65f7eKOXJ8yao1r8e4VOHQtpQff.jpeg .......... [image] bt8QPiw96zrwl65f7eKOXJ8yao1r8e4VOHQtpQff.jpeg .......... [thumb] bt8QPiw96zrwl65f7eKOXJ8yao1r8e4VOHQtpQff.jpeg .......... [brand] [source] fd9QPiw96zrwl65f7eKODJ8yao1r8e4VOHQtpSda.jpeg .......... [image] fd9QPiw96zrwl65f7eKODJ8yao1r8e4VOHQtpSda.jpeg .......... [thumb] fd9QPiw96zrwl65f7eKODJ8yao1r8e4VOHQtpSda.jpeg .......... [product] [source] ey7QPiw96zrwl65f7eKOYJ8yao1r8e4VOHQtpFvs.jpeg .......... [image] ey7QPiw96zrwl65f7eKOYJ8yao1r8e4VOHQtpFvs.jpeg .......... [thumb] ey7QPiw96zrwl65f7eKOYJ8yao1r8e4VOHQtpFvs.jpeg ..........
И осталось только показать загруженное изображение в шаблоне show.blade.php
:
@extends('layout.admin') @section('content') <h1>Просмотр категории</h1> <div class="row"> <div class="col-md-6"> <p><strong>Название:</strong> {{ $category->name }}</p> <p><strong>ЧПУ (англ):</strong> {{ $category->slug }}</p> <p><strong>Краткое описание</strong></p> @isset($category->content) <p>{{ $category->content }}</p> @else <p>Описание отсутствует</p> @endisset </div> <div class="col-md-6"> @php if ($category->image) { // $url = url('storage/catalog/category/image/' . $category->image); $url = Storage::disk('public')->url('catalog/category/image/' . $category->image); } else { // $url = url('storage/catalog/category/image/default.jpg'); $url = Storage::disk('public')->url('catalog/category/image/default.jpg'); } @endphp <img src="{{ $url }}" alt="" class="img-fluid"> </div> </div> @if ($category->children->count()) <p><strong>Дочерние категории</strong></p> <!-- Здесь таблица дочерних категорий --> @else <p>Нет дочерних категорий</p> @endif <a href="{{ route('admin.category.edit', ['category' => $category->id]) }}" class="btn btn-success"> Редактировать категорию </a> <form method="post" class="d-inline" action="{{ route('admin.category.destroy', ['category' => $category->id]) }}"> @csrf @method('DELETE') <button type="submit" class="btn btn-danger"> Удалить категорию </button> </form> @endsection
Валидация в отдельном классе
Сейчас валидация данных формы добавления и редактирования категории у нас внутри методов store()
и update()
. Давайте создадим отдельный класс и всю логику валидации переместим в него.
> php artisan make:request CategoryCatalogRequest
namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class CategoryCatalogRequest extends FormRequest { /** * Determine if the user is authorized to make this request. * * @return bool */ public function authorize() { return true; } /** * Get the validation rules that apply to the request. * * @return array */ public function rules() { switch ($this->method()) { case 'POST': return [ 'parent_id' => 'integer', 'name' => 'required|max:100', 'slug' => 'required|max:100|unique:categories,slug|regex:~^[-_a-z0-9]+$~i', 'image' => 'mimes:jpeg,jpg,png|max:5000' ]; case 'PUT': case 'PATCH': // получаем объект модели категории из маршрута: admin/category/{category} $model = $this->route('category'); // из объекта модели получаем уникальный идентификатор для валидации $id = $model->id; return [ '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|regex:~^[-_a-z0-9]+$~i', 'image' => 'mimes:jpeg,jpg,png|max:5000' ]; } } }
И будем использовать этот класс в контроллере для проверки данных формы:
namespace App\Http\Controllers\Admin; use App\Helpers\ImageSaver; use App\Http\Controllers\Controller; use App\Http\Requests\CategoryCatalogRequest; use App\Models\Category; class CategoryController extends Controller { /* ... */ public function store(CategoryCatalogRequest $request) { $data = $request->all(); $data['image'] = $this->imageSaver->upload($request, null, 'category'); $category = Category::create($data); return redirect() ->route('admin.category.show', ['category' => $category->id]) ->with('success', 'Новая категория успешно создана'); } /* ... */ public function update(CategoryCatalogRequest $request, Category $category) { $data = $request->all(); $data['image'] = $this->imageSaver->upload($request, $category, 'category'); $category->update($data); return redirect() ->route('admin.category.show', ['category' => $category->id]) ->with('success', 'Категория была успешно исправлена'); } /* ... */ }
Сообщения об ошибках
Давайте приведем в порядок сообщения об ошибках — для этого открываем на редактирование файл validation.php
в директории resources/lang/ru
.
return [ /* ... */ 'custom' => [ 'name' => [ 'required' => 'Поле «:attribute» обязательно для заполнения', 'max' => 'Поле «:attribute» должно быть не больше :max символов', ], 'email' => [ 'required' => 'Поле «:attribute» обязательно для заполнения', 'max' => 'Поле «:attribute» должно быть не больше :max символов', ], 'phone' => [ 'required' => 'Поле «:attribute» обязательно для заполнения', 'max' => 'Поле «:attribute» должно быть не больше :max символов', ], 'address' => [ 'required' => 'Поле «:attribute» обязательно для заполнения', 'max' => 'Поле «:attribute» должно быть не больше :max символов', ], 'slug' => [ 'required' => 'Поле «:attribute» обязательно для заполнения', 'unique' => 'Поле «:attribute» должно быть уникальным значением', 'regex' => 'Поле «:attribute» допускает только буквы, цифры, «-» и «_»', 'max' => 'Поле «:attribute» должно быть не больше :max символов', ], ], 'attributes' => [ 'name' => 'Имя, Фамилия', 'slug' => 'ЧПУ (англ)', 'email' => 'Адрес почты', 'password' => 'Пароль', 'password_confirmation' => 'Подтверждение пароля', 'address' => 'Адрес доставки', 'phone' => 'Номер телефона', /* ... */ ], ];
- Магазин на Laravel 7, часть 12. Панель управления, создание и редактирование категорий
- Магазин на Laravel 7, часть 24. Фильтр товаров категории по цене, новинкам и лидерам продаж
- Магазин на Laravel 7, часть 18. Панель управления, пользователи и CRUD страниц сайта
- Магазин на Laravel 7, часть 17. Панель управления, работа с заказами, изменение статуса
- Магазин на Laravel 7, часть 16. Панель управления, CRUD-операции для товаров каталога
- Магазин на Laravel 7, часть 15. Панель управления, добавление и редактирование брендов
- Магазин на Laravel 7, часть 10. Форма оформления, сохранение заказа в базу данных
Поиск: Laravel • MySQL • PHP • Web-разработка • База данных • Интернет магазин • Каталог товаров • Панель управления • Практика • Форма • Фреймворк • Шаблон сайта • Валидация