Блог на Laravel 7, часть 8. Панель управления — CRUD для категорий, тегов и пользователей
25.12.2020
Теги: Laravel • MySQL • PHP • Web-разработка • БазаДанных • Блог • ПанельУправления • Практика • Форма • Фреймворк • ШаблонСайта
Продолжаем работать над панелью управления сайтом. Добавим возможность создавать, редактировать и удалять категориии и теги блога. Для каждой сущности надо добавить маршруты, создать контроллер и несколько шаблонов. Напоследок добавим возможность редактирования пользователей — можно будет изменить имя, почту и пароль.
CRUD-операции над категориями
Новые маршруты:
/* * Панель управления: CRUD-операции над постами, категориями, тегами */ Route::group([ 'as' => 'admin.', // имя маршрута, например admin.index 'prefix' => 'admin', // префикс маршрута, например admin/index 'namespace' => 'Admin', // пространство имен контроллеров 'middleware' => ['auth'] // один или несколько посредников ], function () { /* ... */ /* * CRUD-операции над категориями блога */ Route::resource('category', 'CategoryController', ['except' => 'show']); });
Контроллер CategoryController
:
> php artisan make:controller Admin/CategoryController --resource --model=Category
namespace App\Http\Controllers\Admin; use App\Category; use App\Http\Controllers\Controller; use Illuminate\Http\Request; class CategoryController extends Controller { public function __construct() { $this->middleware('perm:manage-categories')->only('index'); $this->middleware('perm:create-category')->only(['create', 'store']); $this->middleware('perm:edit-category')->only(['edit', 'update']); $this->middleware('perm:delete-category')->only('destroy'); } /** * Показывает список всех категорий */ public function index() { $items = Category::all(); return view('admin.category.index', compact('items')); } /** * Показывает форму для создания категории */ public function create() { return view('admin.category.create'); } /** * Сохраняет новую категорию в базу данных */ public function store(Request $request) { Category::create($request->all()); return redirect() ->route('admin.category.index') ->with('success', 'Новая категория успешно создана'); } /** * Показывает форму для редактирования категории */ public function edit(Category $category) { return view('admin.category.edit', compact('category')); } /** * Обновляет категорию блога в базе данных */ public function update(Request $request, Category $category) { $category->update($request->all()); return redirect() ->route('admin.category.index') ->with('success', 'Категория была успешно исправлена'); } /** * Удаляет категорию блога */ public function destroy(Category $category) { if ($category->children->count()) { $errors[] = 'Нельзя удалить категорию с дочерними категориями'; } if ($category->posts->count()) { $errors[] = 'Нельзя удалить категорию, которая содержит посты'; } if (!empty($errors)) { return back()->withErrors($errors); } $category->delete(); return redirect() ->route('admin.category.index') ->with('success', 'Категория блога успешно удалена'); } }
Поскольку в контроллере мы используем «mass assignment» — добавляем свойство $fillable
в модель Category
:
class Category extends Model { protected $fillable = [ 'parent_id', 'name', 'slug', 'content', 'image', ]; /* ... */ }
Шаблон resource/views/admin/category/index.blade.php
для показа списка всех категорий:
@extends('layout.admin', ['title' => 'Все категории']) @section('content') <h1>Все категории</h1> @perm('create-category') <a href="{{ route('admin.category.create') }}" class="btn btn-success mb-4"> Создать категорию </a> @endperm <table class="table table-bordered"> <tr> <th width="45%">Наименование</th> <th width="45%">ЧПУ (англ.)</th> <th><i class="fas fa-edit"></i></th> <th><i class="fas fa-trash-alt"></i></th> </tr> @include('admin.part.all-ctgs', ['level' => -1, 'parent' => 0]) </table> @endsection
Для показа дерева категорий рекурсивно подключается шаблон resource/views/admin/part/all-ctgs.blade.php
:
@if ($items->where('parent_id', $parent)->count()) @php $level++ @endphp @foreach ($items->where('parent_id', $parent) as $item) <tr> <td> @if ($level) {{ str_repeat('—', $level) }} @endif @if($level) <span>{{ $item->name }}</span> @else <strong>{{ $item->name }}</strong> @endif </td> <td>{{ $item->slug }}</td> <td> @perm('edit-category') <a href="{{ route('admin.category.edit', ['category' => $item->id]) }}"> <i class="far fa-edit"></i> </a> @endperm </td> <td> @perm('delete-category') <form action="{{ route('admin.category.destroy', ['category' => $item->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> @endperm </td> </tr> @include('admin.part.all-ctgs', ['level' => $level, 'parent' => $item->id]) @endforeach @endif
Шаблон resource/views/admin/category/create.blade.php
для создания новой категории:
@extends('layout.admin', ['title' => 'Создание категории']) @section('content') <h1>Создание категории</h1> <form method="post" action="{{ route('admin.category.store') }}" enctype="multipart/form-data"> @csrf @include('admin.category.part.form') </form> @endsection
Шаблон resource/views/admin/category/edit.blade.php
для редактирования категории:
@extends('layout.admin', ['title' => 'Редактирование категории']) @section('content') <h1>Редактирование категории</h1> <form method="post" enctype="multipart/form-data" action="{{ route('admin.category.update', ['category' => $category->id]) }}"> @csrf @method('PUT') @include('admin.category.part.form') </form> @endsection
Форма создания-редактирования категории, шаблон resource/views/admin/category/part/form.blade.php
:
<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> @include('admin.part.parents', ['level' => -1, 'parent' => 0]) </select> </div> <div class="form-group"> <textarea class="form-control" name="content" placeholder="Краткое описание" maxlength="500" rows="5">{{ 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>
Шаблон resource/views/admin/part/parents.blade.php
для возможности выбора родителя:
@if ($items->where('parent_id', $parent)->count()) @php $level++ @endphp @foreach ($items->where('parent_id', $parent) as $item) <option value="{{ $item->id }}" @if ($item->id == $parent_id) selected @endif> @if ($level) {!! str_repeat(' ', $level) !!} @endif {{ $item->name }} </option> @include('admin.part.parents', ['level' => $level, 'parent' => $item->id]) @endforeach @endif
Еще одна задача — передать коллекцию всех категорий блога в шаблоны admin.part.all-ctgs
и admin.part.parents
:
class ComposerServiceProvider extends ServiceProvider { public function register() { $views = [ 'layout.part.categories', // меню в левой колонке в публичной части 'admin.part.categories', // выбор категории поста при редактировании 'admin.part.parents', // выбор родителя категории при редактировании 'admin.part.all-ctgs', // все категории в административной части ]; View::composer($views, function($view) { static $items = null; if (is_null($items)) { $items = Category::all(); } $view->with(['items' => $items]); }); View::composer('layout.part.popular-tags', function($view) { $view->with(['items' => Tag::popular()]); }); View::composer('admin.part.all-tags', function($view) { $view->with(['items' => Tag::all()]); }); } /* ... */ }
И последнее — валидация данных формы при создании и редактировании категории блога:
> php artisan make:request CategoryRequest
namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class CategoryRequest extends FormRequest { /** * Определяет, есть ли права у пользователя на этот запрос * * @return bool */ public function authorize() { return true; } /** * Возвращает массив правил для проверки полей формы * * @return array */ public function rules() { $unique = 'unique:categories,slug'; if ('admin.category.update' == $this->route()->getName()) { // получаем модель Category через маршрут admin/category/{category} $model = $this->route('category'); /* * Проверка на уникальность slug, исключая этот пост по идентифкатору: * 1. categories — таблица базы данных, где проверяется уникальность * 2. slug — имя колонки, уникальность значения которой проверяется * 3. значение, по которому из проверки исключается запись таблицы БД * 4. поле, по которому из проверки исключается запись таблицы БД * Для проверки будет использован такой SQL-запрос к базе данныхЖ * SELECT COUNT(*) FROM `categories` WHERE `slug` = '...' AND `id` <> 17 */ $unique = 'unique:categories,slug,'.$model->id.',id'; } return [ 'name' => [ 'required', 'min:3', 'max:100', ], 'slug' => [ 'required', 'max:100', $unique, 'regex:~^[-_a-z0-9]+$~i', ], 'content' => [ 'max:500', ], 'image' => [ 'mimes:jpeg,jpg,png', 'max:5000' ], ]; } /** * Возвращает массив сообщений об ошибках для заданных правил * * @return array */ public function messages() { return [ 'required' => 'Поле «:attribute» обязательно для заполнения', 'unique' => 'Такое значение поля «:attribute» уже используется', 'min' => [ 'string' => 'Поле «:attribute» должно быть не меньше :min символов', 'file' => 'Файл «:attribute» должен быть не меньше :min Кбайт' ], 'max' => [ 'string' => 'Поле «:attribute» должно быть не больше :max символов', 'file' => 'Файл «:attribute» должен быть не больше :max Кбайт' ], 'mimes' => 'Файл «:attribute» должен иметь формат :values', ]; } /** * Возвращает массив дружественных пользователю названий полей * * @return array */ public function attributes() { return [ 'name' => 'Наименование', 'slug' => 'ЧПУ (англ.)', 'content' => 'Краткое описание', 'image' => 'Изображение', ]; } }
namespace App\Http\Controllers\Admin; use App\Category; use App\Http\Controllers\Controller; use App\Http\Requests\CategoryRequest; use Illuminate\Http\Request; class CategoryController extends Controller { /* ... */ public function store(CategoryRequest $request) { $category = Category::create($request->all()); return redirect() ->route('admin.category.show', ['category' => $category->id]) ->with('success', 'Новая категория успешно создана'); } /* ... */ public function update(CategoryRequest $request, Category $category) { $category->update($request->all()); return redirect() ->route('admin.category.index') ->with('success', 'Категория была успешно исправлена'); } /* ... */ }
CRUD-операции над тегами блога
Новые маршруты:
/* * Панель управления: CRUD-операции над постами, категориями, тегами */ Route::group([ 'as' => 'admin.', // имя маршрута, например admin.index 'prefix' => 'admin', // префикс маршрута, например admin/index 'namespace' => 'Admin', // пространство имен контроллеров 'middleware' => ['auth'] // один или несколько посредников ], function () { /* ... */ /* * CRUD-операции над тегами блога */ Route::resource('tag', 'TagController', ['except' => 'show']); });
Контроллер TagController
:
> php artisan make:controller Admin/TagController --resource --model=Tag
namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; use App\Tag; use Illuminate\Http\Request; use Illuminate\Support\Facades\Validator; class TagController extends Controller { public function __construct() { $this->middleware('perm:manage-tags')->only('index'); $this->middleware('perm:create-tag')->only(['create', 'store']); $this->middleware('perm:edit-tag')->only(['edit', 'update']); $this->middleware('perm:delete-tag')->only('destroy'); } /** * Показывает список всех тегов блога */ public function index() { $items = Tag::paginate(8); return view('admin.tag.index', compact('items')); } /** * Показывает форму для создания тега */ public function create() { return view('admin.tag.create'); } /** * Сохраняет новый тег в базу данных */ public function store(Request $request) { $this->validator($request->all(), null)->validate(); $tag = Tag::create($request->all()); return redirect() ->route('admin.tag.index') ->with('success', 'Новый тег блога успешно создан'); } /** * Показывает форму для редактирования тега */ public function edit(Tag $tag) { return view('admin.tag.edit', compact('tag')); } /** * Обновляет тег блога в базе данных */ public function update(Request $request, Tag $tag) { $this->validator($request->all(), $tag->id)->validate(); $tag->update($request->all()); return redirect() ->route('admin.tag.index') ->with('success', 'Тег блога был успешно исправлен'); } /** * Удаляет тег блога из базы данных */ public function destroy(Tag $tag) { $tag->delete(); return redirect() ->route('admin.tag.index') ->with('success', 'Тег блога был успешно удален'); } /** * Возвращает объект валидатора с нужными правилами */ private function validator($data, $id) { $unique = 'unique:tags,slug'; if ($id) { // проверка на уникальность slug тега при редактировании, // исключая этот тег по идентифкатору в таблице БД tags $unique = 'unique:tags,slug,'.$id.',id'; } $rules = [ 'name' => [ 'required', 'string', 'max:50', ], 'slug' => [ 'required', 'max:50', $unique, 'regex:~^[-_a-z0-9]+$~i', ] ]; $messages = [ 'required' => 'Поле «:attribute» обязательно для заполнения', 'max' => 'Поле «:attribute» должно быть не больше :max символов', ]; $attributes = [ 'name' => 'Наименование', 'slug' => 'ЧПУ (англ.)' ]; return Validator::make($data, $rules, $messages, $attributes); } }
Поскольку в контроллере мы используем «mass assignment» — добавляем свойство $fillable
в модель Tag
:
class Tag extends Model { protected $fillable = [ 'name', 'slug', ]; /* ... */ }
Шаблон resource/views/admin/tag/index.blade.php
для показа списка всех тегов:
@extends('layout.admin', ['title' => 'Все теги блога']) @section('content') <h1>Все теги блога</h1> @perm('create-tag') <a href="{{ route('admin.tag.create') }}" class="btn btn-success mb-4"> Создать тег </a> @endperm @if ($items->count()) <table class="table table-bordered"> <tr> <th>#</th> <th width="45%">Наименование</th> <th width="45%">ЧПУ (англ.)</th> <th><i class="fas fa-edit"></i></th> <th><i class="fas fa-trash-alt"></i></th> </tr> @foreach ($items as $item) <tr> <td>{{ $item->id }}</td> <td>{{ $item->name }}</td> <td>{{ $item->slug }}</td> <td> @perm('edit-tag') <a href="{{ route('admin.tag.edit', ['tag' => $item->id]) }}"> <i class="far fa-edit"></i> </a> @endperm </td> <td> @perm('delete-tag') <form action="{{ route('admin.tag.destroy', ['tag' => $item->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> @endperm </td> </tr> @endforeach </table> {{ $items->links() }} @endif @endsection
Шаблон resource/views/admin/tag/create.tag.php
для создания нового тега:
@extends('layout.admin', ['title' => 'Создание тега']) @section('content') <h1>Создание тега</h1> <form method="post" action="{{ route('admin.tag.store') }}"> @csrf <div class="form-group"> <input type="text" class="form-control" name="name" placeholder="Наименование" required maxlength="50" value="{{ old('name') ?? '' }}"> </div> <div class="form-group"> <input type="text" class="form-control" name="slug" placeholder="ЧПУ (на англ.)" required maxlength="50" value="{{ old('slug') ?? '' }}"> </div> <div class="form-group"> <button type="submit" class="btn btn-primary">Сохранить</button> </div> </form> @endsection
Шаблон resource/views/admin/category/edit.blade.php
для редактирования тега:
@extends('layout.admin', ['title' => 'Редактирование тега']) @section('content') <h1>Редактирование тега</h1> <form method="post" action="{{ route('admin.tag.update', ['tag' => $tag->id]) }}"> @csrf @method('PUT') <div class="form-group"> <input type="text" class="form-control" name="name" placeholder="Наименование" required maxlength="50" value="{{ old('name') ?? $tag->name ?? '' }}"> </div> <div class="form-group"> <input type="text" class="form-control" name="slug" placeholder="ЧПУ (на англ.)" required maxlength="50" value="{{ old('slug') ?? $tag->slug ?? '' }}"> </div> <div class="form-group"> <button type="submit" class="btn btn-primary">Сохранить</button> </div> </form> @endsection
CRUD-операции над пользователями
Новые маршруты:
/* * Панель управления: CRUD-операции над постами, категориями, тегами */ Route::group([ 'as' => 'admin.', // имя маршрута, например admin.index 'prefix' => 'admin', // префикс маршрута, например admin/index 'namespace' => 'Admin', // пространство имен контроллеров 'middleware' => ['auth'] // один или несколько посредников ], function () { /* ... */ /* * Просмотр и редактирование пользователей */ Route::resource('user', 'UserController', ['except' => [ 'create', 'store', 'show', 'destroy' ]]); });
Контроллер UserController
:
> php artisan make:controller Admin/UserController --resource --model=User
namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; use App\User; use Illuminate\Http\Request; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Validator; class UserController extends Controller { public function __construct() { $this->middleware('perm:manage-users')->only('index'); $this->middleware('perm:edit-user')->only(['edit', 'update']); } /** * Показывает список всех пользователей */ public function index() { $users = User::paginate(8); return view('admin.user.index', compact('users')); } /** * Показывает форму для редактирования пользователя */ public function edit(User $user) { return view('admin.user.edit', compact('user')); } /** * Обновляет данные пользователя в базе данных */ public function update(Request $request, User $user) { /* * Проверяем данные формы */ $this->validator($request->all(), $user->id)->validate(); /* * Обновляем пользователя */ if ($request->change_password) { // если надо изменить пароль $request->merge(['password' => Hash::make($request->password)]); $user->update($request->all()); } else { $user->update($request->except('password')); } /* * Возвращаемся к списку */ return redirect() ->route('admin.user.index') ->with('success', 'Данные пользователя успешно обновлены'); } /** * Возвращает объект валидатора с нужными нам правилами */ private function validator(array $data, int $id) { $rules = [ 'name' => [ 'required', 'string', 'max:255' ], 'email' => [ 'required', 'string', 'email', 'max:255', // проверка на уникальность email, исключая // этого пользователя по идентифкатору 'unique:users,email,'.$id.',id', ], ]; if (isset($data['change_password'])) { $rules['password'] = ['required', 'string', 'min:8', 'confirmed']; } return Validator::make($data, $rules); } }
Шаблон resource/views/admin/user/index.blade.php
для просмотра списка всех пользователей:
@extends('layout.admin', ['title' => 'Все пользователи']) @section('content') <h1 class="mb-4">Все пользователи</h1> <table class="table table-bordered"> <tr> <th>#</th> <th width="20%">Дата регистрации</th> <th width="25%">Имя, фамилия</th> <th width="20%">Адрес почты</th> <th width="15%">Публикаций</th> <th width="15%">Комментариев</th> <th><i class="fas fa-edit"></i></th> </tr> @foreach($users as $user) <tr> <td>{{ $user->id }}</td> <td>{{ $user->created_at }}</td> <td>{{ $user->name }}</td> <td><a href="mailto:{{ $user->email }}">{{ $user->email }}</a></td> <td>{{ $user->posts->count() }}</td> <td>{{ $user->comments->count() }}</td> <td> @perm('edit-user') <a href="{{ route('admin.user.edit', ['user' => $user->id]) }}"> <i class="far fa-edit"></i> </a> @endperm </td> </tr> @endforeach </table> {{ $users->links() }} @endsection
Шаблон resource/views/admin/user/index.blade.php
для редактирования данных пользователя:
@extends('layout.admin', ['title' => 'Редактирование пользователя']) @section('content') <h1 class="mb-4">Редактирование пользователя</h1> <form method="post" action="{{ route('admin.user.update', ['user' => $user->id]) }}"> @csrf @method('PUT') <div class="form-group"> <input type="text" class="form-control" name="name" placeholder="Имя, Фамилия" required maxlength="255" value="{{ old('name') ?? $user->name }}"> </div> <div class="form-group"> <input type="email" class="form-control" name="email" placeholder="Адрес почты" required maxlength="255" value="{{ old('email') ?? $user->email }}"> </div> <div class="form-group form-check"> <input type="checkbox" class="form-check-input" name="change_password" id="change_password"> <label class="form-check-label" for="change_password"> Изменить пароль пользователя </label> </div> <div class="form-group"> <input type="text" class="form-control" name="password" maxlength="255" placeholder="Новый пароль" value=""> </div> <div class="form-group"> <input type="text" class="form-control" name="password_confirmation" maxlength="255" placeholder="Пароль еще раз" value=""> </div> <div class="form-group"> <button type="submit" class="btn btn-success">Сохранить</button> </div> </form> @endsection
- Блог на Laravel 7, часть 9. Панель управления — создание, публикация, удаление комментариев
- Блог на Laravel 7, часть 7. Панель управления — создание, публикация, удаление постов
- Блог на Laravel 7, часть 12. Доп.страницы сайта в панели управления и в публичной части
- Блог на Laravel 7, часть 10. Личный кабинет — CRUD-операции над постами и комментариями
- Магазин на Laravel 7, часть 13. Панель управления, обрезка изображения и валидация данных
- Магазин на Laravel 7, часть 12. Панель управления, создание и редактирование категорий
- Мини-блог на Laravel, часть 6. Исправление ошибок, удаление поста, семь маршрутов
Поиск: Laravel • MySQL • PHP • Web-разработка • База данных • Блог • Панель управления • Форма • Фреймворк • Шаблон сайта • Практика