Блог на Laravel 7, часть 9. Панель управления — создание, публикация, удаление комментариев
29.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('comment', 'CommentController', ['except' => ['create', 'store']]); // доп.маршрут, чтобы разрешить публикацию комментария Route::get('comment/enable/{comment}', 'CommentController@enable') ->name('comment.enable'); // доп.маршрут, чтобы запретить публикацию комментария Route::get('comment/disable/{comment}', 'CommentController@disable') ->name('comment.disable'); });
Контроллер CommentController
:
> php artisan make:controller Admin/CommentController --resource --model=Comment
namespace App\Http\Controllers\Admin; use App\Comment; use App\Http\Controllers\Controller; use App\Http\Requests\CommentRequest; use Illuminate\Http\Request; class CommentController extends Controller { public function __construct() { $this->middleware('perm:manage-comments')->only(['index', 'show']); $this->middleware('perm:edit-comment')->only('update'); $this->middleware('perm:publish-comment')->only(['enable', 'disable']); $this->middleware('perm:delete-comment')->only('destroy'); } /** * Показывает список всех комментариев */ public function index() { $comments = Comment::orderBy('created_at', 'desc')->paginate(); return view('admin.comment.index', compact('comments')); } /** * Просмотр комментария к посту блога */ public function show(Comment $comment) { // сигнализирует о том, что это режим пред.просмотра session()->flash('preview', 'yes'); // это тот пост блога, к которому оставлен комментарий $post = $comment->post; // коллекция всех комментариев к этому посту блога $comments = $post->comments()->orderBy('created_at')->paginate(); // используем шаблон предварительного просмотра поста return view('admin.post.show', compact('post', 'comments')); } /** * Показывает форму редактирования комментария */ public function edit(Comment $comment) { // нужно сохранить flash-переменную, которая сигнализирует о том, // что кнопка редактирования была нажата в режиме пред.просмотра session()->keep('preview'); return view('admin.comment.edit', compact('comment')); } /** * Обновляет комментарий в базе данных */ public function update(CommentRequest $request, Comment $comment) { $comment->update($request->all()); return $this->redirectAfterUpdate($comment); } /** * Разрешить публикацию комментария */ public function enable(Comment $comment) { $comment->enable(); $redirect = back(); if (session('preview')) { $redirect = $redirect->withFragment('comment-list'); } return $redirect->with('success', 'Комментарий был опубликован'); } /** * Запретить публикацию комментария */ public function disable(Comment $comment) { $comment->disable(); $redirect = back(); if (session('preview')) { $redirect = $redirect->withFragment('comment-list'); } return $redirect->with('success', 'Комментарий снят с публикации'); } /** * Удаляет комментарий из базы данных */ public function destroy(Comment $comment) { $comment->delete(); $redirect = back(); if (session('preview')) { $redirect = $redirect->withFragment('comment-list'); } return $redirect->with('success', 'Комментарий успешно удален'); } /** * Выполянет редирект после обновления */ private function redirectAfterUpdate(Comment $comment) { // кнопка редактирования может быть нажата в режиме пред.просмотра // или в панели управления блогом, поэтому и редирект будет разный $redirect = redirect(); if (session('preview')) { $redirect = $redirect->route( 'admin.comment.show', ['comment' => $comment->id, 'page' => $comment->adminPageNumber()] )->withFragment('comment-list'); } else { $redirect = $redirect->route('admin.comment.index'); } return $redirect->with('success', 'Комментарий был успешно исправлен'); } }
CommentRequest
, который создадим чуть позже, когда будем писать код для добавления нового комментария.
Шаблон resource/views/admin/comment/index.blade.php
для просмотра списка всех комментариев:
@extends('layout.admin', ['title' => 'Все комментарии']) @section('content') <h1>Все комментарии</h1> <table class="table table-bordered"> <tr> <th>#</th> <th width="12%">Дата и время</th> <th width="47%">Текст комментария</th> <th width="17%">Автор комментария</th> <th width="20%">Разрешил публикацию</th> <th><i class="fas fa-eye"></i></th> <th><i class="fas fa-toggle-on"></i></th> <th><i class="fas fa-edit"></i></th> <th><i class="fas fa-trash-alt"></i></th> </tr> @foreach ($comments as $comment) <tr> <td>{{ $comment->id }}</td> <td>{{ $comment->created_at }}</td> <td>{{ iconv_substr($comment->content, 0, 100) }}…</td> <td>{{ $comment->user->name }}</td> <td> @if ($comment->editor) {{ $comment->editor->name }} @endif </td> <td> @php($params = ['comment' => $comment->id, 'page' => $comment->adminPageNumber()]) <a href="{{ route('admin.comment.show', $params) }}#comment-list" title="Предварительный просмотр"> <i class="far fa-eye"></i> </a> </td> <td> @perm('publish-comment') @if ($comment->isVisible()) <a href="{{ route('admin.comment.disable', ['comment' => $comment->id]) }}" title="Запретить комментарий"> <i class="far fa-toggle-on"></i> </a> @else <a href="{{ route('admin.comment.enable', ['comment' => $comment->id]) }}" title="Разрешить комментарий"> <i class="far fa-toggle-off"></i> </a> @endif @endperm </td> <td> @perm('edit-comment') <a href="{{ route('admin.comment.edit', ['comment' => $comment->id]) }}"> <i class="far fa-edit"></i> </a> @endperm </td> <td> @perm('delete-comment') <form action="{{ route('admin.comment.destroy', ['comment' => $comment->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> {{ $comments->links() }} @endsection
Мы здесь используем метод adminPageNumber()
модели Comment
— комментариев может быть много, поэтому используется постраничная навигация. Для предварительного просмотра надо перейти на ту страницу, где расположен комментарий.
class Comment extends Model { /** * Номер страницы пагинации, на которой расположен комментарий; * учитываются все комментарии, в том числе не опубликованные */ public function adminPageNumber() { $comments = $this->post->comments()->orderBy('created_at')->get(); if ($comments->count() == 0) { return 1; } if ($comments->count() <= $this->getPerPage()) { return 1; } foreach ($comments as $i => $comment) { if ($this->id == $comment->id) { break; } } return (int) ceil(($i+1) / $this->getPerPage()); } }
Шаблон resource/views/admin/comment/edit.blade.php
для редактирования комментария:
@extends('layout.admin', ['title' => 'Редактирование комментария']) @section('content') <h1>Редактирование комментария</h1> <form method="post" action="{{ route('admin.comment.update', ['comment' => $comment->id]) }}"> @csrf @method('PUT') <div class="form-group"> <textarea class="form-control" name="content" placeholder="Текст комментария" maxlength="500" rows="5">{{ old('content') ?? $comment->content }}</textarea> </div> <div class="form-group"> <button type="submit" class="btn btn-primary">Сохранить</button> </div> </form> @endsection
Для предварительного просмотра комментария используется тот же шаблон resource/views/admin/post/show.blade.php
, который мы уже использовали ранее. Но тогда мы не рассмотрели шаблон resource/views/admin/post/comments.blade.php
, так что сделаем это сейчас. В режиме предварительного просмотра поста показывается только сам пост без комментариев. В режиме предварительного просмотра комментария показывается сам пост (или анонс) + все комментарии.
<h3 id="comment-list">Все комментарии</h3> @if ($comments->count()) @foreach ($comments as $comment) <div class="card mb-3" id="comment-{{ $comment->id }}"> <div class="card-header p-2"> @if ( ! $comment->isVisible()) <i class="far fa-eye-slash text-danger" title="Предварительный просмотр"></i> @else <i class="far fa-eye text-success" title="Комментарий опубликован"></i> @endif {{ $comment->user->name }} </div> <div class="card-body p-2"> {{ $comment->content }} </div> <div class="card-footer p-2 d-flex justify-content-between"> <span>{{ $comment->created_at }}</span> <span> @perm('publish-post') @if ($comment->isVisible()) <a href="{{ route('admin.comment.disable', ['comment' => $comment->id]) }}" class="btn btn-outline-success btn-sm" title="Запретить публикацию"> <i class="fas fa-toggle-on"></i> </a> @else <a href="{{ route('admin.comment.enable', ['comment' => $comment->id]) }}" class="btn btn-outline-dark btn-sm" title="Разрешить публикацию"> <i class="fas fa-toggle-off"></i> </a> @endif @endperm @perm('edit-comment') <a href="{{ route('admin.comment.edit', ['comment' => $comment->id]) }}" class="btn btn-outline-primary btn-sm" title="Редактировать комментарий"> <i class="far fa-edit"></i> </a> @endperm @perm('delete-comment') <form action="{{ route('admin.comment.destroy', ['comment' => $comment->id]) }}" method="post" class="d-inline" onsubmit="return confirm('Удалить этот комментарий?')"> @csrf @method('DELETE') <button type="submit" class="btn btn-outline-danger btn-sm" title="Удалить комментарий"> <i class="far fa-trash-alt"></i> </button> </form> @endperm </span> </div> </div> @endforeach {{ $comments->fragment('comment-list')->links() }} @else <p>К этому посту еще нет комментариев</p> @endif
С режимом предварительного просмотра довольно много хлопот, потому что требуется всегда помнить, где была нажата кнопка редактирования. Если в режиме пред.просмотра — то после редактирования надо вернуться в режим пред.просмотра, если в панели управления — надо вернуться в панель управления. В этом нам помогает flash-переменная сессии preview
, по аналогии с редактированием поста.
Поскольку в контроллере мы используем «mass assignment» — добавляем свойство $fillable
в модель Comment
:
class Comment extends Model { protected $fillable = [ 'user_id', 'post_id', 'published_by', 'content', ]; /* ... */ }
Добавление нового комментария
Для этого добавим новый маршрут, создадим шаблон с формой и реализуем новый метод comment()
в контроллер BlogController
.
/* * Блог: все посты, посты категории, посты тега, страница поста */ Route::group([ 'as' => 'blog.', // имя маршрута, например blog.index 'prefix' => 'blog', // префикс маршрута, например blog/index ], function () { /* ... */ // добавление комментария к посту Route::post('post/{post}/comment', [BlogController::class, 'comment']) ->name('comment'); });
class BlogController extends Controller { /** * Сохраняет новый комментарий в базу данных */ public function comment(CommentRequest $request) { $request->merge(['user_id' => auth()->user()->id]); $message = 'Комментарий добавлен, будет доступен после проверки'; if (auth()->user()->hasPermAnyWay('publish-comment')) { $request->merge(['published_by' => auth()->user()->id]); $message = 'Комментарий добавлен и уже доступен для просмотра'; } $comment = Comment::create($request->all()); // комментариев может быть много, поэтому есть пагинация; надо // перейти на последнюю страницу — новый комментарий будет там $page = $comment->post->comments()->published()->paginate()->lastPage(); return redirect() ->route('blog.post', ['post' => $comment->post->slug, 'page' => $page]) ->withFragment('comment-list') ->with('success', $message); } }
Шаблон resources/views/blog/part/form.blade.php
с формой комментария:
<h3 id="comment-form">Ваш комментарий</h3> <form method="post" action="{{ route('blog.comment', ['post' => $post->id]) }}"> @csrf <input type="hidden" name="post_id" value="{{ $post->id }}"> <div class="form-group"> <textarea class="form-control" name="content" placeholder="Текст комментария" maxlength="500" rows="4">{{ old('content') ?? '' }}</textarea> </div> <div class="form-group"> <button type="submit" class="btn btn-primary">Отправить</button> </div> </form>
Подключим его в шаблоне resources/views/blog/part/comments.blade.php
:
@perm('create-comment') @include('blog.part.form') @endperm <h3 id="comment-list">Все комментарии</h3> @if ($comments->count()) @foreach ($comments as $comment) <div class="card mb-3" id="comment-{{ $comment->id }}"> <div class="card-header p-2"> {{ $comment->user->name }} </div> <div class="card-body p-2"> {{ $comment->content }} </div> <div class="card-footer p-2"> {{ $comment->created_at }} </div> </div> @endforeach {{ $comments->fragment('comment-list')->links() }} @else <p>К этому посту еще нет комментариев</p> @endif
Для валидации данных формы создадим отдельный класс CommentRequest
:
> php artisan make:request CommentRequest
namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class CommentRequest extends FormRequest { /** * Определяет, есть ли права у пользователя на этот запрос * * @return bool */ public function authorize() { return true; } /** * Возвращает массив правил для проверки полей формы * * @return array */ public function rules() { $rules = [ 'content' => [ 'required', 'string', 'max:500', ], ]; // при добавлении нового комментария есть скрытое поле post_id if ($this->route()->getName() == 'blog.comment') { $rules['post_id'] = [ 'required', 'numeric', 'min:1', 'exists:posts,id' ]; } return $rules; } /** * Возвращает массив сообщений об ошибках для заданных правил * * @return array */ public function messages() { return [ 'required' => 'Поле «:attribute» обязательно для заполнения', 'max' => 'Поле «:attribute» должно быть не больше :max символов', 'numeric' => 'Идентификатор публикации должен быть числом', 'min' => 'Идентификатор публикации должен быть :min или больше', 'exists' => 'Публикации с таким идентификатором не существует', ]; } /** * Возвращает массив дружественных пользователю названий полей * * @return array */ public function attributes() { return [ 'content' => 'Текст комментария' ]; } }
Главная страница панели управления
У нас уже есть страница управления постами, комментариями, категориями и тегами. Но нет главной страницы панели управления, куда администратор переходит для управления блогом. Подразумевается, что после аутентификации пользователь изначально попадает в личный кабинет — а оттуда, если есть соответствующие права, может попасть в панель управления. Немного позже в личном кабинете для некоторых пользователей сделаем ссылку для перехода на главную страницу панели управления.
Добавляем новый маршрут:
/* * Панель управления: CRUD-операции над постами, категориями, тегами */ Route::group([ 'as' => 'admin.', // имя маршрута, например admin.index 'prefix' => 'admin', // префикс маршрута, например admin/index 'namespace' => 'Admin', // пространство имен контроллеров 'middleware' => ['auth'] // один или несколько посредников ], function () { /* * Главная страница панели управления */ Route::get('index', 'IndexController')->name('index'); /* ... */ });
Создаем контроллер:
> php artisan make:controller Admin/IndexController --invokable
namespace App\Http\Controllers\Admin; use App\Comment; use App\Http\Controllers\Controller; use App\Post; use Illuminate\Http\Request; class IndexController extends Controller { /** * Handle the incoming request. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function __invoke(Request $request) { $posts = Post::whereNull('published_by')->orderBy('created_at')->limit(5)->get(); $comments = Comment::whereNull('published_by')->orderBy('created_at')->limit(5)->get(); return view('admin.index', compact('posts', 'comments')); } }
Шаблон resources/views/admin/index.blade.php
для показа новых публикаций и комментариев:
@extends('layout.admin') @section('content') <h1>Панель управления</h1> @perm('manage-posts') <h3>Новые публикации</h3> @if ($posts->count()) <table class="table table-bordered"> <tr> <th>#</th> <th width="25%">Дата и время</th> <th width="40%">Наименование</th> <th width="25%">Автор публикации</th> <th><i class="fas fa-eye"></i></th> <th><i class="fas fa-toggle-on"></i></th> </tr> @foreach ($posts as $item) <tr> <td>{{ $item->id }}</td> <td>{{ $item->created_at }}</td> <td>{{ $item->name }}</td> <td>{{ $item->user->name }}</td> <td> <a href="{{ route('admin.post.show', ['post' => $item->id]) }}" title="Предварительный просмотр"> <i class="far fa-eye"></i> </a> </td> <td> @perm('publish-post') <a href="{{ route('admin.post.enable', ['post' => $item->id]) }}" title="Разрешить публикацию"> <i class="far fa-toggle-off"></i> </a> @endperm </td> </tr> @endforeach </table> @else <p>Нет новых публикаций</p> @endif @endperm @perm('manage-comments') <h3>Новые комментарии</h3> @if ($comments->count()) <table class="table table-bordered"> <tr> <th>#</th> <th width="25%">Дата и время</th> <th width="40%">Текст комментария</th> <th width="25%">Автор комментария</th> <th><i class="fas fa-eye"></i></th> <th><i class="fas fa-toggle-on"></i></th> </tr> @foreach ($comments as $item) <tr> <td>{{ $item->id }}</td> <td>{{ $item->created_at }}</td> <td>{{ iconv_substr($item->content, 0, 50) }}…</td> <td>{{ $item->user->name }}</td> <td> @php $params = ['comment' => $item->id, 'page' => $item->adminPageNumber()]; @endphp <a href="{{ route('admin.comment.show', $params) }}#comment-list" title="Предварительный просмотр"> <i class="far fa-eye"></i> </a> </td> <td> @perm('publish-comment') <a href="{{ route('admin.comment.enable', ['comment' => $item->id]) }}" title="Разрешить комментарий"> <i class="far fa-toggle-off"></i> </a> @endperm </td> </tr> @endforeach </table> @else <p>Нет новых комментариев</p> @endif @endperm @endsection
На главной странице администратор сразу видит, есть ли новые публикации и/или комментарии. И переходит в нужный раздел панели управления, чтобы проверить их и опубликовать.
Верхнее меню панели управления
Верхнее меню панели управления содержит ссылки для перехода к управлению публикациями, комментариями, категориями блога и так далее. Нам надо показывать эти ссылки только тем пользователям, у которых есть соответствующие права. Поэтому редактироуем шаблон resources/views/layout/admin.blade.php
.
<nav class="navbar navbar-expand-lg navbar-dark bg-danger mb-4"> <!-- Логотип и кнопка «Гамбургер» --> <a class="navbar-brand" href="{{ route('admin.index') }}">Панель управления</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar-blog" aria-controls="navbar-blog" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <!-- Основная часть меню (может содержать ссылки, формы и прочее) --> <div class="collapse navbar-collapse" id="navbar-blog"> <!-- Этот блок расположен слева --> <ul class="navbar-nav mr-auto"> @perm('manage-posts') <li class="nav-item"> <a class="nav-link" href="{{ route('admin.post.index') }}"> Посты </a> </li> @endperm @perm('manage-comments') <li class="nav-item"> <a class="nav-link" href="{{ route('admin.comment.index') }}"> Комментарии </a> </li> @endperm @perm('manage-categories') <li class="nav-item"> <a class="nav-link" href="{{ route('admin.category.index') }}"> Категории </a> </li> @endperm @perm('manage-tags') <li class="nav-item"> <a class="nav-link" href="{{ route('admin.tag.index') }}"> Теги </a> </li> @endperm @perm('manage-users') <li class="nav-item"> <a class="nav-link" href="{{ route('admin.user.index') }}"> Пользователи </a> </li> @endperm <li class="nav-item"> <a class="nav-link" href="#">Страницы</a> </li> <li class="nav-item"> <a class="nav-link" href="#">Корзина</a> </li> </ul> <!-- Этот блок расположен справа --> <ul class="navbar-nav ml-auto"> <li class="nav-item"> <a class="nav-link" href="{{ route('user.index') }}">Личный кабинет</a> </li> <li class="nav-item"> <a class="nav-link" href="{{ route('auth.logout') }}">Выйти</a> </li> </ul> </div> </nav>
- Блог на Laravel 7, часть 8. Панель управления — CRUD для категорий, тегов и пользователей
- Блог на Laravel 7, часть 7. Панель управления — создание, публикация, удаление постов
- Блог на Laravel 7, часть 12. Доп.страницы сайта в панели управления и в публичной части
- Блог на Laravel 7, часть 10. Личный кабинет — CRUD-операции над постами и комментариями
- Магазин на Laravel 7, часть 13. Панель управления, обрезка изображения и валидация данных
- Магазин на Laravel 7, часть 12. Панель управления, создание и редактирование категорий
- Мини-блог на Laravel, часть 6. Исправление ошибок, удаление поста, семь маршрутов
Поиск: Laravel • MySQL • PHP • Web-разработка • База данных • Блог • Практика • Форма • Фреймворк • Шаблон сайта • Панель управления