Мини-блог на Laravel, часть 6. Исправление ошибок, удаление поста, семь маршрутов

17.09.2020

Теги: LaravelMySQLPHPWeb-разработкаБазаДанныхБлогПрактикаФормаФреймворкШаблонСайта

Работа над ошибками

Прежде, чем двигаться дальше, давайте внесем пару мелких изменений в проект. Во-первых, у нас сейчас два почти одинаковых шаблона create.blade.php и edit.blade.php. Во-вторых, если будет запрошен для показа или редактирования не существующий пост — будет выброшено исключение. Лучше, если в этом случае мы покажем страницу 404 Not Found.

Создадим шаблон resources/views/parts/form.blade.php:

@csrf
<div class="form-group">
    <input type="text" class="form-control" name="title" maxlength="100"
           placeholder="Заголовок" required value="{{ $post->title ?? '' }}">
</div>
<div class="form-group">
            <textarea class="form-control" name="excerpt" maxlength="200"
                      placeholder="Анонс поста" required>{{ $post->excerpt ?? '' }}</textarea>
</div>
<div class="form-group">
            <textarea class="form-control" name="body"
                      placeholder="Текст поста" rows="7" required>{{ $post->body ?? '' }}</textarea>
</div>
<div class="form-group">
    <input type="file" class="form-control-file" name="image">
</div>
@isset($post->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">
            Удалить загруженное <a href="{{ $post->image }}" target="_blank">изображение</a>
        </label>
    </div>
@endisset
<div class="form-group">
    <button type="submit" class="btn btn-primary">Сохранить</button>
</div>

Отредактируем шаблон resources/views/posts/create.blade.php:

@extends('layouts.site')

@section('content')
    <h1 class="mt-2 mb-3">Создать пост</h1>
    <form method="post" action="{{ route('post.store') }}" enctype="multipart/form-data">
        @include('parts.form')
    </form>
@endsection

Отредактируем шаблон resources/views/posts/update.blade.php:

@extends('layouts.site')

@section('content')
    <h1 class="mt-2 mb-3">Редактировать пост</h1>
    <form method="post" action="{{ route('post.update', ['id' => $post->post_id]) }}"
          enctype="multipart/form-data">
        @method('PATCH')
        @include('parts.form')
    </form>
@endsection

С дублированием кода разобрались, теперь исправим проблему с просмотром или редактированием не существующего поста. Заменим в контроллере вызов Post::find() на Post::findOrFail(). При этом Laravel выбросит исключение ModelNotFoundException, которое мы можем централизованно отлавливать и показывать страницу 404 Not Found. Для этого отредактируем файл app/Exceptions/Handler.php.

<?php
namespace App\Exceptions;

use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;

class Handler extends ExceptionHandler {
    /* ... */
    public function render($request, Throwable $exception) {
        if ($exception instanceof ModelNotFoundException) {
            abort(404);
        }
        return parent::render($request, $exception);
    }
    /* ... */
}

Добавляем иконку favicon.png

Тут все просто — кладем favicon.png (у меня это логотип Laravel) в директорию public и прописываем путь к ней в layout-шаблоне site.blade.php.

<!doctype html>
<html lang="ru">
<head>
    <!-- ..... -->
    <title>{{ $title ?? 'Веб-разработка' }}</title>
    <link rel="shortcat icon" type="image/png" href="{{ asset('favicon.png') }}"/>
    <!-- ..... -->
</head>
<body>
<!-- ..... -->
</body>
</html>

Удаление поста блога

На страницу поста добавим кнопку «Удалить»

@extends('layouts.site')

@section('content')
    <div class="row">
        <div class="col-12">
            <div class="card mt-4 mb-4">
                <div class="card-header">
                    <h1>{{ $post->title }}</h1>
                </div>
                <div class="card-body">
                    <img src="{{ $post->image ?? asset('img/default.jpg') }}" alt="" class="img-fluid">
                    <p class="mt-3 mb-0">{{ $post->body }}</p>
                </div>
                <div class="card-footer">
                    <div class="clearfix">
                        <span class="float-left">
                            Автор: {{ $post->author }}
                            <br>
                            Дата: {{ date_format($post->created_at, 'd.m.Y H:i') }}
                        </span>
                        <span class="float-right">
                            <a href="{{ route('post.edit', ['id' => $post->post_id]) }}"
                               class="btn btn-dark mr-2">Редактировать</a>
                            <!-- Форма для удаления поста -->
                            <form action="{{ route('post.delete', ['id' => $post->post_id]) }}"
                                  method="post" onsubmit="return confirm('Удалить этот пост?')"
                                  class="d-inline">
                                @csrf
                                @method('DELETE')
                                <input type="submit" class="btn btn-danger" value="Удалить">
                            </form>
                        </span>
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection

Добавим еще один маршрут

<?php
use Illuminate\Support\Facades\Route;

Route::get('/', 'PostController@index')->name('blog.index');

Route::get('post/index', 'PostController@index')->name('post.index');
Route::get('post/search', 'PostController@search')->name('post.search');
Route::get('post/create', 'PostController@create')->name('post.create');
Route::post('post/store', 'PostController@store')->name('post.store');
Route::get('post/show/{id}', 'PostController@show')->name('post.show');
Route::get('post/edit/{id}', 'PostController@edit')->name('post.edit');
Route::patch('post/update/{id}', 'PostController@update')->name('post.update');
Route::delete('post/destroy/{id}', 'PostController@destroy')->name('post.destroy');

Реализуем метод destroy()

class PostController extends Controller {
    /* ... */
    public function destroy($id) {
        $post = Post::findOrFail($id);
        $post->delete();
        return redirect()
            ->route('post.index')
            ->with('success', 'Пост был успешно удален');
    }
    /* ... */
}

Но это еще не окончательная реализация. При удалении поста надо удалять и связанные с ним изображения. Код для удаления изображений у нас уже есть, надо только его приспособить к решению еще одной задачи.

<?php
namespace App\Http\Controllers;

use App\Post;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Intervention\Image\Facades\Image;

class PostController extends Controller {
    /* ... */
    public function store(Request $request) {
        $post = new Post();
        $post->author_id = rand(1, 4);
        $post->title = $request->input('title');
        $post->excerpt = $request->input('excerpt');
        $post->body = $request->input('body');
        $this->uploadImage($request, $post);
        $post->save();
        return redirect()
            ->route('post.index')
            ->with('success', 'Новый пост успешно создан');
    }
    /* ... */
    public function update(Request $request, $id) {
        $post = Post::findOrFail($id);
        $post->title = $request->input('title');
        $post->excerpt = $request->input('excerpt');
        $post->body = $request->input('body');
        // если надо удалить старое изображение
        if ($request->input('remove')) {
            $this->removeImage($post);
        }
        // если было загружено новое изображение
        $this->uploadImage($request, $post);
        // все готово, можно сохранять пост в БД
        $post->update();
        return redirect()
            ->route('post.show', compact('id'))
            ->with('success', 'Пост успешно отредактирован');
    }

    private function uploadImage(Request $request, Post $post) {
        $source = $request->file('image');
        if ($source) {
            // перед тем, как загружать новое изображение, удаляем загруженное ранее
            $this->removeImage($post);
            /*
             * сохраняем исходное изображение и создаем две копии 1200x400 и 600x200
             */
            $ext = str_replace('jpeg', 'jpg', $source->extension());
            // уникальное имя файла, под которым сохраним его в storage/image/source
            $name = md5(uniqid());
            Storage::putFileAs('public/image/source', $source, $name. '.' . $ext);
            // создаем jpg изображение для с страницы поста размером 1200x400, качество 100%
            $image = Image::make($source)
                ->resizeCanvas(1200, 400, 'center', false, 'dddddd')
                ->encode('jpg', 100);
            // сохраняем это изображение под именем $name.jpg в директории public/image/image
            Storage::put('public/image/image/' . $name . '.jpg', $image);
            $image->destroy();
            $post->image = Storage::url('public/image/image/' . $name . '.jpg');
            // создаем jpg изображение для списка постов блога размером 600x200, качество 100%
            $thumb = Image::make($source)
                ->resizeCanvas(600, 200, 'center', false, 'dddddd')
                ->encode('jpg', 100);
            // сохраняем это изображение под именем $name.jpg в директории public/image/thumb
            Storage::put('public/image/thumb/' . $name . '.jpg', $thumb);
            $thumb->destroy();
            $post->thumb = Storage::url('public/image/thumb/' . $name . '.jpg');
        }
    }

    private function removeImage(Post $post) {
        if (!empty($post->image)) {
            $name = basename($post->image);
            if (Storage::exists('public/image/image/' . $name)) {
                Storage::delete('public/image/image/' . $name);
            }
            $post->image = null;
        }
        if (!empty($post->thumb)) {
            $name = basename($post->thumb);
            if (Storage::exists('public/image/thumb/' . $name)) {
                Storage::delete('public/image/thumb/' . $name);
            }
            $post->thumb = null;
        }
        // здесь сложнее, мы не знаем, какое у файла расширение
        if (!empty($name)) {
            $images = Storage::files('public/image/source');
            $base = pathinfo($name, PATHINFO_FILENAME);
            foreach ($images as $img) {
                $temp = pathinfo($img, PATHINFO_FILENAME);
                if ($temp == $base) {
                    Storage::delete($img);
                    break;
                }
            }
        }
    }
    /* ... */
    public function destroy($id) {
        $post = Post::findOrFail($id);
        $this->removeImage($post);
        $post->delete();
        return redirect()
            ->route('post.index')
            ->with('success', 'Пост был успешно удален');
    }
    /* ... */
}

Оптимизация маршрутов

Наш контроллер PostController — ресурсный, то есть позволяет производить над неким ресурсом (пост блога) базовые операции: просмотр списка постов, просмотр отдельного поста, создание или редактирование, удаление существующего поста. И в этом случае мы можем описать семь маршрутов, которые добавляли ранее, всего одной строкой.

// на главной странице сайта показываем список всех постов
Route::get('/', 'PostController@index')->name('blog.index');

Route::get('post/search', 'PostController@search')->name('post.search');            // поиск по блогу

Route::get('post/index', 'PostController@index')->name('post.index');               // все посты блога
Route::get('post/create', 'PostController@create')->name('post.create');            // форма создания
Route::post('post/store', 'PostController@store')->name('post.store');              // сохранение поста
Route::get('post/show/{id}', 'PostController@show')->name('post.show');             // просмотр поста
Route::get('post/edit/{id}', 'PostController@edit')->name('post.edit');             // форма редактирования
Route::patch('post/update/{id}', 'PostController@update')->name('post.update');     // обновление поста
Route::delete('post/destroy/{id}', 'PostController@destroy')->name('post.destroy'); // удаление поста
// на главной странице сайта показываем список всех постов; но если проводить аналогию с WordPress — на
// главной странице может быть показана статичная страница или список постов блога или еще что-то третье
Route::get('/', 'PostController@index')->name('blog.index');

// этот маршрут оставляем, потому как он не входит в число семи стандартных маршрутов
Route::get('post/search', 'PostController@search')->name('post.search');

/*
Тип        URI              Действие  Имя маршрута
--------------------------------------------------
GET        /post            index     post.index
GET        /post/create     create    post.create
POST       /post            store     post.store
GET        /post/{id}       show      post.show
GET        /post/{id}/edit  edit      post.edit
PUT/PATCH  /post/{id}       update    post.update
DELETE     /post/{id}       destroy   post.destroy
*/
Route::resource('post', 'PostController');

Здесь Laravel предлагает использовать точно такие же имена маршрутов, которые мы уже использовали. Но вот предлагаемые URI немного другие, хотя отличия несущественные. Так что можно заменить семь маршрутов одним — и все будет работать. Поскольку имена маршрутов у нас совпадают, а в шаблонах мы используем только имена — в шаблонах ничего изменять не надо.

При объявлении маршрута можно указать подмножество всех возможных действий, которые должен обрабатывать контроллер вместо полного набора стандартных действий:

Route::resource('post', 'PostController', ['only' => ['index', 'show']]);
Route::resource('post', 'PostController', ['except' => ['create', 'store', 'update', 'destroy']]);

Поиск: 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.