Мини-блог на Laravel, часть 7. Добавляем заголовки страниц и валидация полей формы

18.09.2020

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

Вроде блог у нас уже работает, можно просматривать, редактировать и удалять записи. Но есть проблема с заголовками страниц сайта — они везде одинаковые. Заголовок задается в layout-шаблоне site.blade.php и имеет значение «Веб-разработка». Давайте это изменим и будем для разных страниц устанавливать разные значения.

Добавляем заголовки страниц

Шаблон site.blade.php

<!doctype html>
<html lang="ru">
<head>
    <!-- ..... -->
    <title>{{ $title ?? 'Веб-разработка' }}</title>
    <!-- ..... -->
</head>
<body>
<!-- ..... -->
</body>
</html>

Шаблон show.blade.php

@extends('layouts.site', ['title' => $post->title])

@section('content')
    <div class="row">
    <!-- ..... -->
    </div>
@endsection

Шаблон create.blade.php

@extends('layouts.site', ['title' => 'Создать пост'])

@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

Шаблон edit.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

Весь код контроллера PostController

Здесь нет ничего нового, исключительно для того, чтобы посмотреть все, что уже было сделано.

<?php
namespace App\Http\Controllers;

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

class PostController extends Controller {
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index() {
        $posts = Post::select('posts.*', 'users.name as author')
            ->join('users', 'posts.author_id', '=', 'users.id')
            ->orderBy('posts.created_at', 'desc')
            ->paginate(4);
        return view('posts.index', compact('posts'));
    }

    /**
     * Display a listing of search result.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function search(Request $request) {
        $search = $request->input('search', '');
        // образаем слишком длинный запрос
        $search = iconv_substr($search, 0, 64);
        // удаляем все, кроме букв и цифр
        $search = preg_replace('#[^0-9a-zA-ZА-Яа-яёЁ]#u', ' ', $search);
        // сжимаем двойные пробелы
        $search = preg_replace('#\s+#u', ' ', $search);
        if (empty($search)) {
            return view('posts.search');
        }
        $posts = Post::select('posts.*', 'users.name as author')
            ->join('users', 'posts.author_id', '=', 'users.id')
            ->where('posts.title', 'like', '%'.$search.'%')  // поиск по заголовку поста
            ->orWhere('posts.body', 'like', '%'.$search.'%') // поиск по тексту поста
            ->orWhere('users.name', 'like', '%'.$search.'%') // поиск по автору поста
            ->orderBy('posts.created_at', 'desc')
            ->paginate(4)
            ->appends(['search' => $request->input('search')]);;
        return view('posts.search', compact('posts'));
    }

    /**
     * Show the form for creating a new resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function create() {
        return view('posts.create');
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    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', 'Новый пост успешно создан');
    }

    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function show($id) {
        $post = Post::select('posts.*', 'users.name as author')
            ->join('users', 'posts.author_id', '=', 'users.id')
            ->findOrFail($id);
        return view('posts.show', compact('post'));
    }

    /**
     * Show the form for editing the specified resource.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function edit($id) {
        $post = Post::findOrFail($id);
        return view('posts.edit', compact('post'));
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    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', 'Пост успешно отредактирован');
    }

    /**
     * Вспомогательный метод, загружает изображение и создает уменьшенные копии
     *
     * @param Request $request
     * @param Post $post
     */
    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');
        }
    }

    /**
     * Вспомогательный метод, удаляет все изображения, связанные с постом
     *
     * @param Post $post
     */
    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;
                }
            }
        }
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function destroy($id) {
        $post = Post::findOrFail($id);
        $this->removeImage($post);
        $post->delete();
        return redirect()
            ->route('post.index')
            ->with('success', 'Пост был успешно удален');
    }
}

Валидация данных формы

Для валидации данных при создании нового поста блога создадим специальный класс с помощью artisan-команды:

> cd D:/work/localhost25/www
> php artisan make:request PostRequest

Будет создан файл app/Http/Requests/PostRequest.php, который мы приведем к следующему виду:

<?php
namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class PostRequest extends FormRequest {
    /**
     * Определяет, есть ли права у пользователя на этот запрос
     *
     * @return bool
     */
    public function authorize() {
        return true;
    }

    /**
     * Возвращает массив правил для проверки полей формы
     *
     * @return array
     */
    public function rules() {
        return [
            'title' => 'required|unique:posts|min:3|max:100',
            'excerpt' => 'required|min:100|max:200',
            'body' => 'required',
            'image' => 'mimes:jpeg,bmp,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 [
            'title' => 'Заголовок',
            'excerpt' => 'Анонс поста',
            'body' => 'Текст поста',
            'image' => 'Изображение',
        ];
    }
}

Далее изменим метод store() контроллера PostController, указывая тип параметра $request как PostRequest вместо Request.

<?php
namespace App\Http\Controllers;

use App\Post;
use Illuminate\Http\Request;
use App\Http\Requests\PostRequest;

class PostController extends Controller {
    /* ... */
    public function store(PostRequest $request) {
        /* ... */
    }
    /* ... */
}

Входящий запрос перед вызовом метода контроллера store() будет проверяться на соответствие заданным правилам автоматически, что позволит не загромождать контроллер логикой валидации. Если проверка не пройдена, то ошибки будут записаны в сессию и мы сможем показать их в шаблоне. Так что еще редактируем layout-шаблон site.blade.php:

<!doctype html>
<html lang="ru">
<head>
    <!-- ..... -->
</head>
<body>
<div class="container">
    <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
        <!-- ..... -->
    </nav>

    @if ($message = Session::get('success'))
        <div class="alert alert-success alert-dismissible mt-4" role="alert">
            <button type="button" class="close" data-dismiss="alert" aria-label="Закрыть">
                <span aria-hidden="true">&times;</span>
            </button>
            {{ $message }}
        </div>
    @endif

    @if ($errors->any())
        <div class="alert alert-danger alert-dismissible mt-4" role="alert">
            <button type="button" class="close" data-dismiss="alert" aria-label="Закрыть">
                <span aria-hidden="true">&times;</span>
            </button>
            <ul>
                @foreach ($errors->all() as $error)
                    <li>{{ $error }}</li>
                @endforeach
            </ul>
        </div>
    @endif

    @yield('content')
</div>
</body>
</html>

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

Проверка работает, правда введенные пользователем значения теряются. Отредактируем шаблон form.blade.php:

@csrf
<div class="form-group">
    <input type="text" class="form-control" name="title" maxlength="100"
           placeholder="Заголовок" required value="{{ old('title') ?? $post->title ?? '' }}">
</div>
<div class="form-group">
    <textarea class="form-control" name="excerpt" placeholder="Анонс поста"
              maxlength="200" required>{{ old('excerpt') ?? $post->excerpt ?? '' }}</textarea>
</div>
<div class="form-group">
    <textarea class="form-control" name="body" placeholder="Текст поста" rows="7"
              required>{{ old('body') ?? $post->body ?? '' }}</textarea>
</div>
<div class="form-group">
    <input type="file" class="form-control-file" name="image" accept="image/png, image/jpeg">
</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>

Валидация при редактировании

Чтобы не создавать новый класс валидации для формы редактирования существующего поста, изменим класс PostRequest. Будем проверять, какой HTTP-метод используется — POST или PATCH — и задавать для каждого метода свои правила.

<?php
namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class PostRequest extends FormRequest {
    /* ... */
    public function rules() {
        $rules = [
            $rules['title'] = 'required|unique:posts|min:3|max:100',
            'excerpt' => 'required|min:100|max:200',
            'body' => 'required',
            'image' => 'mimes:jpeg,jpg,png|max:5000',
        ];
        if ($this->isMethod('PATCH')) {
            $rules['title'] = 'required|min:3|max:100';
        }
        return $rules;
    }
    /* ... */
}

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

И не забываем изменить тип параметра $request метода update() контроллера на PostRequest — как мы это делали для метода store().

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

Каталог оборудования
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.