Магазин на Laravel 7, часть 14. Панель управления, доп.проверка для родителя категории

18.10.2020

Теги: LaravelMySQLPHPWeb-разработкаБазаДанныхИнтернетМагазинКаталогТоваровПанельУправленияПрактикаФреймворк

Осталась еще одна проблема — нет проверки, что при редактировании категории в качестве родителя не будет выбана эта же категория или один из ее потомков. Здесь простыми правилами валидации не обойтись — потребуется класс, который реализует интерфейс Illuminate\Contracts\Validation\Rule. Давайте создадим такой класс с помощью artisan-команды.

> php artisan make:rule CategoryParent
namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;

class CategoryParent implements Rule {
    /**
     * Create a new rule instance.
     *
     * @return void
     */
    public function __construct() {
        // ...
    }

    /**
     * Determine if the validation rule passes.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value) {
        // ...
    }

    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message() {
        return 'The validation error message.';
    }
}

Кроме того, нам потребуются два метода в модели Category, которые позволят определить, какие категории не могут быть родителями для текущей категории.

class Category extends Model {
    /**
     * Проверяет, что переданный идентификатор id может быть родителем
     * этой категории; что категорию не пытаются поместить внутрь себя
     */
    public function validParent($id) {
        $id = (integer)$id;
        // получаем идентификаторы всех потомков текущей категории
        $ids = $this->getAllChildren($this->id);
        $ids[] = $this->id;
        return ! in_array($id, $ids);
    }

    /**
     * Возвращает всех потомков категории с идентификатором $id
     */
    public function getAllChildren($id) {
        // получаем прямых потомков категории с идентификатором $id
        $children = self::where('parent_id', $id)->with('children')->get();
        $ids = [];
        foreach ($children as $child) {
            $ids[] = $child->id;
            // для каждого прямого потомка получаем его прямых потомков
            if ($child->children->count()) {
                $ids = array_merge($ids, $this->getAllChildren($child->id));
            }
        }
        return $ids;
    }
}

Теперь в классе CategoryCatalogRequest, где мы задавали правила валидации, добавим еще одно правило:

namespace App\Http\Requests;

use App\Rules\CategoryParent;
use Illuminate\Foundation\Http\FormRequest;

class CategoryCatalogRequest extends FormRequest {
    /* ... */
    public function rules() {
        switch ($this->method()) {
            case 'POST':
                return [
                    'parent_id' => 'required|regex:~^[0-9]+$~',
                    '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' => ['required', 'regex:~^[0-9]+$~', new CategoryParent($model)],
                    'name' => 'required|max:100',
                    'slug' => 'required|max:100|unique:categories,slug,'.$id.',id|regex:~^[-_a-z0-9]+$~i',
                    'image' => 'mimes:jpeg,jpg,png|max:5000'
                ];
        }
    }
}

При создании объекта класса CategoryParent мы передаем в конструктор объект класса модели Category — через него мы сможем обратиться к методу validParent().

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;
use App\Models\Category;

class CategoryParent implements Rule {

    private $category;

    /**
     * Create a new rule instance.
     *
     * @return void
     */
    public function __construct(Category $category) {
        $this->category = $category;
    }

    /**
     * Determine if the validation rule passes.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value) {
        return $this->category->validParent($value);
    }

    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message() {
        return trans('validation.custom.parent_id.invalid');
    }
}

В файл переводе validation.php в директории resources/lang/ru добавим строки перевода для сообщения об ошибке.

return [
    /* ... */
    'uuid'                 => 'Поле :attribute должно быть корректным UUID.',
    'parent_id'            => 'Поле «:attribute» имеет недопустимое значение',

    'custom' => [
        /* ... */
        'parent_id' => [
            'required' => 'Поле «:attribute» обязательно для заполнения',
            'regex' => 'Поле «:attribute» должно быть целым положительным числом',
            'invalid' => 'Категорию нелья поместить внутрь самой себя',
        ]
    ],
    
    'attributes' => [
        'name'                  => 'Имя, Фамилия',
        'slug'                  => 'ЧПУ (англ)',
        'email'                 => 'Адрес почты',
        'address'               => 'Адрес доставки',
        'phone'                 => 'Номер телефона',
        /* ... */
        'parent_id'             => 'Родитель',
    ],
];

Теперь все готово, можно проверять:

Заголовки страниц

Установим заголовок по умолчанию в layout-шаблоне views/layout/admin.blade.php:

<head>
    <!-- ..... -->
    <title>{{ $title ?? 'Панель управления' }}</title>
    <!-- ..... -->
</head>

И будем передавать значение заголовка из дочерних шаблонов:

@extends('layout.admin', ['title' => 'Все категории каталога'])

@section('content')
    <h1>Все категории</h1>
    <!-- ..... -->
@endsection
@extends('layout.admin', ['title' => 'Просмотр категории'])

@section('content')
    <h1>Просмотр категории</h1>
    <!-- ..... -->
@endsection
@extends('layout.admin', ['title' => 'Создание категории'])

@section('content')
    <h1>Создание категории</h1>
    <!-- ..... -->
@endsection
@extends('layout.admin', ['title' => 'Редактирование категории'])

@section('content')
    <h1>Редактирование категории</h1>
    <!-- ..... -->
@endsection

Создание, редактирование и удаление бренда

Нам опять нужен ресурсный контроллер, который позволит выполянить CRUD-операции над брендами.

> php artisan make:controller Admin/BrandController --resource --model=Models/Brand
namespace App\Http\Controllers\Admin;

use App\Helpers\ImageSaver;
use App\Http\Controllers\Controller;
use App\Models\Brand;
use Illuminate\Http\Request;

class BrandController extends Controller {

    private $imageSaver;

    public function __construct(ImageSaver $imageSaver) {
        $this->imageSaver = $imageSaver;
    }

    /**
     * Показывает список всех брендов
     *
     * @return \Illuminate\Http\Response
     */
    public function index() {
        $brands = Brand::all();
        return view('admin.brand.index', compact('brands'));
    }

    /**
     * Показывает форму для создания бренда
     *
     * @return \Illuminate\Http\Response
     */
    public function create() {
        return view('admin.brand.create');
    }

    /**
     * Сохраняет новый бренд в базу данных
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request) {
        $data = $request->all();
        $data['image'] = $this->imageSaver->upload($request, null, 'brand');
        $brand = Brand::create($data);
        return redirect()
            ->route('admin.brand.show', ['brand' => $brand->id])
            ->with('success', 'Новый бренд успешно создан');
    }

    /**
     * Показывает страницу бренда
     *
     * @param  \App\Models\Brand  $brand
     * @return \Illuminate\Http\Response
     */
    public function show(Brand $brand) {
        return view('admin.brand.show', compact('brand'));
    }

    /**
     * Показывает форму для редактирования бренда
     *
     * @param  \App\Models\Brand  $brand
     * @return \Illuminate\Http\Response
     */
    public function edit(Brand $brand) {
        return view('admin.brand.edit',compact('brand'));
    }

    /**
     * Обновляет бренд (запись в таблице БД)
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \App\Models\Brand  $brand
     * @return \Illuminate\Http\Response
     */
    public function update(Request $request, Brand $brand) {
        $data = $request->all();
        $data['image'] = $this->imageSaver->upload($request, $brand, 'brand');
        $brand->update($data);
        return redirect()
            ->route('admin.brand.show', ['brand' => $brand->id])
            ->with('success', 'Бренд был успешно отредактирован');
    }

    /**
     * Удаляет бренд (запись в таблице БД)
     *
     * @param  \App\Models\Brand  $brand
     * @return \Illuminate\Http\Response
     */
    public function destroy(Brand $brand) {
        if ($brand->products->count()) {
            return back()->withErrors('Нельзя удалить бренд, у которого есть товары');
        }
        $this->imageSaver->remove($brand, 'brand');
        $brand->delete();
        return redirect()
            ->route('admin.brand.index')
            ->with('success', 'Бренд каталога успешно удален');
    }
}

Добавляем маршруты в файл routes/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');
});

Создаем шаблон для просмотра списка брендов views/admin/brand/index.blade.php:

@extends('layout.admin', ['title' => 'Все бренды каталога'])

@section('content')
    <h1>Все бренды каталога</h1>
    <a href="{{ route('admin.brand.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($brands as $brand)
            <tr>
                <td>
                    <a href="{{ route('admin.brand.show', ['brand' => $brand->id]) }}">
                        {{ $brand->name }}
                    </a>
                </td>
                <td>{{ iconv_substr($brand->content, 0, 150) }}</td>
                <td>
                    <a href="{{ route('admin.brand.edit', ['brand' => $brand->id]) }}">
                        <i class="far fa-edit"></i>
                    </a>
                </td>
                <td>
                    <form action="{{ route('admin.brand.destroy', ['brand' => $brand->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>
@endsection

Создаем шаблон для просмотра отдельного бренда views/admin/brand/show.blade.php:

@extends('layout.admin', ['title' => 'Просмотр бренда'])

@section('content')
    <h1>Просмотр бренда</h1>
    <div class="row">
        <div class="col-md-6">
            <p><strong>Название:</strong> {{ $brand->name }}</p>
            <p><strong>ЧПУ (англ):</strong> {{ $brand->slug }}</p>
            <p><strong>Краткое описание</strong></p>
            @isset($brand->content)
                <p>{{ $brand->content }}</p>
            @else
                <p>Описание отсутствует</p>
            @endisset
        </div>
        <div class="col-md-6">
            @php
                if ($brand->image) {
                    // $url = url('storage/catalog/brand/source/' . $brand->image);
                    $url = Storage::disk('public')->url('catalog/brand/image/' . $brand->image);
                } else {
                    // $url = Storage::disk('public')->url('catalog/brand/image/' . $brand->image);
                    $url = Storage::disk('public')->url('catalog/brand/image/default.jpg');
                }
            @endphp
            <img src="{{ $url }}" alt="" class="img-fluid">
        </div>
    </div>
    <a href="{{ route('admin.brand.edit', ['brand' => $brand->id]) }}"
       class="btn btn-success">
        Редактировать бренд
    </a>
    <form method="post" class="d-inline"
          action="{{ route('admin.brand.destroy', ['brand' => $brand->id]) }}">
        @csrf
        @method('DELETE')
        <button type="submit" class="btn btn-danger">
            Удалить бренд
        </button>
    </form>
@endsection

Поиск: Laravel • MySQL • PHP • Web-разработка • База данных • Интернет магазин • Каталог товаров • Практика • Фреймворк • Валидация • Панель управления

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.