Вы находитесь на странице: 1из 56

Мини-блог на Laravel, часть 1.

Создание таблиц БД,


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

Подключаемся к серверу БД и создаем новую базу данных larablog. После этого создаем первую
модель Post для работы с постами блога:
> php artisan make:model Post -m
Model created successfully.
Created Migration: 2020_09_06_104713_create_posts_table
При этом будет создан файл модели в директории app и файл миграции в
директории database/migrations:
<?php
namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model {


// .....
}
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreatePostsTable extends Migration {


/**
* Run the migrations.
*
* @return void
*/
public function up() {
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->timestamps();
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down() {
Schema::dropIfExists('posts');
}
}
Для таблицы базы данных posts добавим поля author_id, title, excerpt, image, body. Для
поля author_id зададим ограничение внешнего ключа — это будет ссылка на поле id таблицы users.
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreatePostsTable extends Migration {


/**
* Run the migrations.
*
* @return void
*/
public function up() {
Schema::create('posts', function (Blueprint $table) {
$table->bigIncrements('post_id');
$table->bigInteger('author_id')->unsigned()->nullable();
$table->string('title', 100);
$table->string('excerpt', 200);
$table->string('image', 100)->nullable();
$table->text('body');
$table->timestamps();
// внешний ключ, ссылается на поле id таблицы users
$table->foreign('author_id')
->references('id')
->on('users')
->nullOnDelete();
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down() {
Schema::dropIfExists('posts');
}
}
Миграция для создания таблицы users доступна изначально, так что нам ее создавать не нужно:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateUsersTable extends Migration {


/**
* Run the migrations.
*
* @return void
*/
public function up() {
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down() {
Schema::dropIfExists('users');
}
}
Перед тем, как создавать таблицы базы данных, надо задать параметры подключения к серверу БД в
файле .env:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=larablog
DB_USERNAME=root
DB_PASSWORD=qwerty

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

> php artisan migrate


Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_000000_create_users_table (0.03 seconds)
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated: 2014_10_12_100000_create_password_resets_table (0.03 seconds)
Migrating: 2019_08_19_000000_create_failed_jobs_table
Migrated: 2019_08_19_000000_create_failed_jobs_table (0.02 seconds)
Migrating: 2020_09_06_104713_create_posts_table
Migrated: 2020_09_06_104713_create_posts_table (0.05 seconds)
После этого в базе данных larablog будет пять таблиц и две из них — users и posts:
--
-- Структура таблицы `users`
--
CREATE TABLE `users` (
`id` bigint(20) UNSIGNED NOT NULL,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`email_verified_at` timestamp NULL DEFAULT NULL,
`password` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`remember_token` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

--
-- Индексы таблицы `users`
--
ALTER TABLE `users`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `users_email_unique` (`email`);

--
-- AUTO_INCREMENT для таблицы `users`
--
ALTER TABLE `users`
MODIFY `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT;
--
-- Структура таблицы `posts`
--
CREATE TABLE `posts` (
`post_id` bigint(20) UNSIGNED NOT NULL,
`author_id` bigint(20) UNSIGNED DEFAULT NULL,
`title` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`excerpt` varchar(200) COLLATE utf8mb4_unicode_ci NOT NULL,
`image` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`body` text COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Индексы таблицы `posts`
--
ALTER TABLE `posts`
ADD PRIMARY KEY (`id`),
ADD KEY `posts_author_id_foreign` (`author_id`);

--
-- AUTO_INCREMENT для таблицы `posts`
--
ALTER TABLE `posts`
MODIFY `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT;

--
-- Ограничения внешнего ключа таблицы `posts`
--
ALTER TABLE `posts`
ADD CONSTRAINT `posts_author_id_foreign` FOREIGN KEY (`author_id`) REFERENCES
`users` (`id`);
Laravel включает в себя механизм наполнения базы данных начальными данными (seeding) с помощью
специальных классов. Все такие классы хранятся в директории database/seeds. Для создания заготовок
классов UserTableSeeder и PostTableSeeder используем команду:
> php artisan make:seeder UserTableSeeder
> php artisan make:seeder PostTableSeeder
По умолчанию в Laravel уже определён класс DatabaseSeeder. Из этого класса можно вызывать
метод call() для подключения других классов с данными, что позволит контролировать порядок их
выполнения.
<?php
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder {


/**
* Seed the application's database.
*
* @return void
*/
public function run() {
$this->call(UserTableSeeder::class);
$this->command->info('Таблица пользователей загружена данными!');

$this->call(PostTableSeeder::class);
$this->command->info('Таблица постов блога загружена данными!');
}
}
Файлы фабрик моделей хранятся в директории database/factories, и там уже есть один готовый
файл UserFactory.php — это фабрика для модели User. Чтобы создать фабрику для модели Post,
используем Artisan-команду:
> php artisan make:factory PostFactory --model=Post
<?php
/** @var \Illuminate\Database\Eloquent\Factory $factory */

use App\Post;
use Faker\Generator as Faker;

$factory->define(Post::class, function (Faker $faker) {


return [
'author_id' => rand(1, 4),
'title' => $faker->realText(rand(25, 30)),
'excerpt' => $faker->realText(rand(100, 120)),
'body' => $faker->realText(rand(200, 300)),
'created_at' => $faker->dateTimeBetween('-60 days', '-30 days'),
'updated_at' => $faker->dateTimeBetween('-20 days', '-1 days'),

];
});
Осталось только отредактировать файлы классов UserTableSeeder и PostTableSeeder:
<?php
use Illuminate\Database\Seeder;

class UserTableSeeder extends Seeder {


public function run() {
// создать 4 пользователей сайта
factory(App\User::class, 4)->create();
}
}
<?php
use Illuminate\Database\Seeder;

class PostTableSeeder extends Seeder {


public function run() {
// создать 20 постов блога
factory(App\Post::class, 20)->create();
}
}

Заполняем таблицы базы данных начальными данными:

> php artisan migrate:fresh --seed

Мини-блог на Laravel, часть 2. Создание контроллера,


представления и маршрута
Создадим первый контроллер, первое представление и первый маршрут. В результате на главной
странице сайта будет показан список всех постов блога. Для создания контроллера используем artisan-
команду:

> php artisan make:controller PostController


Controller created successfully.
В результате будет создан файл класса контроллера в директории app/Http/Controllers:
<?php
namespace App\Http\Controllers;

use Illuminate\Http\Request;

class PostController extends Controller {


// .....
}

Теперь в контроллере получим массив всех записей блога и передадим этот массив в представление:

<?php
namespace App\Http\Controllers;

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

class PostController extends Controller {


public function index() {
$posts = Post::all();
return view('posts.index', ['posts' => $posts]);
}
}
Создадим представление index.blade.php в директории resources/views/posts:
<h1>Все посты блога</h1>
<ul>
<?php foreach ($posts as $post): ?>
<li><?= $post->title; ?></li>
<?php endforeach; ?>
</ul>
И зададим маршрут в файле routes/web.php в директории — для этого изменим код
<?php
use Illuminate\Support\Facades\Route;

/*
Route::get('/', function () {
return view('welcome');
});
*/

Route::get('/', 'PostController@index');

Открываем главную страницу сайта и видим список постов блога:

Если в какой-то момент при обновлении страницы не видно никаких изменений, помогут команды очистки
кэша:

> php artisan cache:clear # Очистка кэша приложения


> php artisan route:clear # очистка кэша маршрутов
> php artisan view:clear # Очистка кэша шаблонов
> php artisan config:clear # Очистка кэша конфигурации

Стилизуем нашу страницу со списком постов блога. Для этого нам потребуется CSS-фреймворк
Bootstrap4. В консоли последовательно запускаем три команды:

> cd D:/work/localhost25/www
> composer require laravel/ui --dev
> php artisan ui bootstrap
> npm install && npm run dev
Редактируем файл шаблона index.blade.php в директории resources/views/posts:
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0,
maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Все посты блога</title>
<link rel="stylesheet" href="{{ asset('css/app.css') }}">
</head>
<body>
<h1>Все посты блога</h1>
<ul>
<?php foreach ($posts as $post): ?>
<li><?= $post->title; ?></li>
<?php endforeach; ?>
</ul>
</body>
</html>

И смотрим в браузере, что получилось:

Добавим меню сверху, используя компонент Navbar:


<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0,
maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Все посты блога</title>
<link rel="stylesheet" href="{{ asset('css/app.css') }}">
</head>
<body>
<div class="container">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<a class="navbar-brand" href="#">Мой блог</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-
target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-
label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>

<div class="collapse navbar-collapse" id="navbarSupportedContent">


<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="#">Автор</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Портфолио</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Контакты</a>
</li>
</ul>
<form class="form-inline my-2 my-lg-0">
<input class="form-control mr-sm-2" name="search" type="search"
placeholder="Поиск" aria-label="Поиск">
<button class="btn btn-outline-success my-2 my-sm-0"
type="submit">Поиск</button>
</form>
</div>
</nav>

<h1>Все посты блога</h1>


<ul>
<?php foreach ($posts as $post): ?>
<li><?= $post->title; ?></li>
<?php endforeach; ?>
</ul>
</div>
</body>
</html>

Используем компонент Card для показа постов блога:


<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0,
maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Все посты блога</title>
<link rel="stylesheet" href="{{ asset('css/app.css') }}">
</head>
<body>
<div class="container">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<!-- ..... -->
</nav>

<h1>Все посты блога</h1>


<div class="row">
@foreach ($posts as $post)
<div class="col-6 mb-4">
<div class="card">
<div class="card-header">{{ $post->title }}</div>
<div class="card-body">{{ $post->excerpt }}</div>
</div>
</div>
@endforeach
</div>
</div>
</body>
</html>

Мини-блог на Laravel, часть 3. Постраничная навигация,


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

> php artisan make:controller PostController -r


<?php
namespace App\Http\Controllers;

use Illuminate\Http\Request;

class PostController extends Controller {


/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index() {
// .....
}

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

/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request) {
// .....
}

/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function show($id) {
// .....
}

/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function edit($id) {
// .....
}

/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $id) {
// .....
}

/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function destroy($id) {
// .....
}
}
Изменим метод index(), чтобы получать все посты блога с сортировкой по дате и с пагиницией:
class PostController extends Controller {
/* ... */
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'));
}
/* ... */
}

И внесем изменения в шаблон, чтобы показывать картинку, автора поста и дату публикации:

<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0,
maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Веб-разработка</title>
<link rel="stylesheet" href="{{ asset('css/app.css') }}">
</head>
<body>
<div class="container">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<!-- ... -->
</nav>

<h1 class="mt-2 mb-3">Все посты блога</h1>


<div class="row">
@foreach ($posts as $post)
<div class="col-6 mb-4">
<div class="card">
<div class="card-header"><h3>{{ $post->title }}</h3></div>
<div class="card-body">
<img src="{{ $posts->image ?? asset('img/default.jpg') }}"
alt="" class="img-fluid">
<p class="mt-3 mb-0">{{ $post->excerpt }}</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>
<a href="#" class="btn btn-dark float-right">Читать
дальше</a>
</div>
</div>
</div>
</div>
@endforeach
</div>
{{ $posts->links() }}
</div>
</body>
</html>
Если для поста не загружена картинка, тогда показываем картинку по умолчанию default.jpg, которую
разместим в директории img в корне веб-сервера.

Layout-шаблон
Сейчас у нас всего один шаблон, но скоро их будет много. У всех шаблонов будет много общего, так что
создадим общий шаблон и будем его наследовать. Создаем файл site.blade.php в
директории resources/views/layouts:
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0,
maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Все посты блога</title>
<link rel="stylesheet" href="{{ asset('css/app.css') }}">
</head>
<body>
<div class="container">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<a class="navbar-brand" href="{{ route('blog.index') }}">Веб-разработка</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-
target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-
label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>

<div class="collapse navbar-collapse" id="navbarSupportedContent">


<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="#">Авторы</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Создать</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Контакты</a>
</li>
</ul>
<form class="form-inline my-2 my-lg-0" action="">
<input class="form-control mr-sm-2" type="search" name="search"
placeholder="Найти пост..." aria-label="Поиск">
<button class="btn btn-outline-success my-2 my-sm-0"
type="submit">Поиск</button>
</form>
</div>
</nav>

@yield('content')
</div>
</body>
</html>
А шаблон index.blade.php в директории resources/views/posts будет
наследовать site.blade.php:
@extends('layouts.site')

@section('content')
<h1 class="mt-2 mb-3">Все посты блога</h1>
<div class="row">
@foreach ($posts as $post)
<div class="col-6 mb-4">
<div class="card">
<div class="card-header"><h3>{{ $post->title }}</h3></div>
<div class="card-body">
<img src="{{ $posts->image ?? asset('img/default.jpg') }}"
alt="" class="img-fluid">
<p class="mt-3 mb-0">{{ $post->excerpt }}</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>
<a href="#" class="btn btn-dark float-right">Читать
дальше</a>
</div>
</div>
</div>
</div>
@endforeach
</div>
{{ $posts->links() }}
@endsection

Поиск по блогу
Добавим еще один метод search() в класс контроллера, который будет искать посты блога по
введенной фразе:
class PostController extends Controller {
/* ... */
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'));
}
/* ... */
}
Добавим новый маршрут в файл routes/web.php, а заодно — зададим имена для маршрутов, чтобы с
ними было удобно работать:
<?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');
В layout-шаблоне site.blade.php установим значение атрибута action тега form:
<form class="form-inline my-2 my-lg-0" action="{{ route('post.search') }}">
<input class="form-control mr-sm-2" type="search" name="search"
placeholder="Найти пост..." aria-label="Поиск">
<button class="btn btn-outline-success my-2 my-sm-0"
type="submit">Поиск</button>
</form>
И осталось только создать шаблон search.blade.php в директории resources/views/posts:
@extends('layouts.site')

@section('content')
<h1 class="mt-2 mb-3">Результаты поиска</h1>
@if (isset($posts) && count($posts))
<div class="row">
@foreach ($posts as $post)
<div class="col-6 mb-4">
<div class="card">
<div class="card-header"><h3>{{ $post->title }}</h3></div>
<div class="card-body">
<img src="{{ $posts->image ?? asset('img/default.jpg')
}}" alt="" class="img-fluid">
<p class="mt-3 mb-0">{{ $post->excerpt }}</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>
<a href="#" class="btn btn-dark float-right">Читать
дальше</a>
</div>
</div>
</div>
</div>
@endforeach
</div>
{{ $posts->links() }}
@else
<p>По вашему запросу ничего не найдено</p>
@endif
@endsection

Мини-блог на Laravel, часть 4. Создание нового поста,


загрузка и обрезка изображения
Теперь нам нужно реализовать остальные методы контроллера PostController
— create(), store(), show(), edit(), update() и destroy(). Давайте создадим
шаблон create.blade.php, добавим два маршрута в файле routes/web.php и внесем изменения в
метод create(), который будет просто показывать форму.

Шаблон, маршруты, метод контроллера


Новый шаблон

@extends('layouts.site')

@section('content')
<h1 class="mt-2 mb-3">Создать пост</h1>
<form method="post" action="{{ route('post.store') }}">
@csrf
<div class="form-group">
<input type="text" class="form-control" name="title"
placeholder="Заголовок" required>
</div>
<div class="form-group">
<textarea class="form-control" name="excerpt" placeholder="Анонс поста"
required></textarea>
</div>
<div class="form-group">
<textarea class="form-control" name="body" placeholder="Текст поста"
rows="7" required></textarea>
</div>
<div class="form-group">
<input type="file" class="form-control-file" name="image">
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">Сохранить</button>
</div>
</form>
@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');
Метод create()
class PostController extends Controller {
/* ... */
public function create() {
return view('posts.create');
}
/* ... */
}

Ссылка на страницу с формой создания поста в layout-шаблоне:

<div class="collapse navbar-collapse" id="navbarSupportedContent">


<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="#">Авторы</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ route('post.create') }}">Создать</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Контакты</a>
</li>
</ul>
<form class="form-inline my-2 my-lg-0" action="{{ route('post.search') }}">
<input class="form-control mr-sm-2" type="search" name="search"
placeholder="Найти пост..." aria-label="Поиск">
<button class="btn btn-outline-success my-2 my-sm-0"
type="submit">Поиск</button>
</form>
</div>
Подготовка файлового хранилища
Сразу после установки Laravel доступны диски local и public, использующие драйвер local. Для
диска local место хранения — директория storage/app, для диска public место хранения —
директория storage/app/public. Диск local является диском по умолчанию.
Чтобы сделать файлы диска public доступными через веб, надо создать символьную ссылку
из public/storage на storage/app/public. Директория public проекта Laravel — является корневой
директорией сервера, поэтому файл storage/app/public/image.jpg будет доступен через веб
как http://server.com/storage/image.jpg.
> cd D:/work/localhost25/www
> php artisan storage:link
The [D:/work/localhost25/www/public/storage] link has been connected to
[D:/work/localhost25/www/storage/app/public].
The links have been created.

Метод store() контроллера


Теперь можно приступать к реализации метода store() контроллера PostController:
<?php
namespace App\Http\Controllers;

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

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');
$image = $request->file('image');
if ($image) {
$path = Storage::putFile('public', $image);
$post->image = Storage::url($path);
}
$post->save();
return redirect()->route('post.index');
}
/* ... */
}

Сохраним в сессию сообщение, чем завершилось добавление поста:

class PostController extends Controller {


/* ... */
public function store(Request $request) {
/* ... */
return redirect()->route('post.index')->with('success', 'Новый пост успешно
создан');
}
/* ... */
}

И будем показывать это сообщение в layout-шаблоне:

<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0,
maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Веб-разработка</title>
<link rel="stylesheet" href="{{ asset('css/app.css') }}">
<script src="{{ asset('js/app.js') }}"></script>
</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

@yield('content')
</div>
</body>
</html>
Заодно подключим js-файл app.js — без него сообщения работать не будут.
Создание превьюшки
Не слишком удачно, что в списке постов блога мы показываем большое изображение. Поэтому при
добавлении нового поста будем создавать два уменьшенных изображения и сохранять путь к ним в базе
данных. Для этого установим пакет intervention/image с помощью composer.
> cd D:/work/localhost25/www
> composer require intervention/image
Открываем файл config/app.php и добавляем следующие строки:
return [
/* ... */
'providers' => [
/* ... */
Intervention\Image\ImageServiceProvider::class,
],

'aliases' => [
/* ... */
'Image' => Intervention\Image\Facades\Image::class,
]
/* ... */
]
Исходный файл изображения будем сохранять без изменений в
директорию storage/public/image/source, а уменьшенные копии — в
директории storage/public/image/image и storage/public/image/thumb. Так что эти три директории
нужно сразу создать. Кроме того, добавить в таблицу БД posts еще одно поле thumb.
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');
$source = $request->file('image');
if ($source) {
$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');
}
$post->save();
return redirect()->route('post.index')->with('success', 'Новый пост успешно
создан');
}
/* ... */
}

Все готово, осталось только изменить шаблоны index.blade.php и search.blade.php:


<img src="{{ $post->thumb ?? asset('img/default.jpg') }}" alt="" class="img-fluid">

Мини-блог на Laravel, часть 5. Просмотр и


редактирование отдельного поста блога
Хорошо, с добавлением нового поста вроде разобрались, теперь нужно его показать. Для этого создаем
новый шаблон show.blade.php, добавляем новый маршрут, реализуем метод show() контроллера. В
шаблонах index.blade.php и search.blade.php делаем ссылку для просмотра отдельного поста.

Шаблон, маршрут, метод контроллера


Новый шаблон

@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>
<a href="#" class="btn btn-dark float-
right">Редактировать</a>
</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');
Метод show()
<?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 {


/* ... */
public function show($id) {
$post = Post::select('posts.*', 'users.name as author')
->join('users', 'posts.author_id', '=', 'users.id')
->find($id);
return view('posts.show', compact('post'));
}
/* ... */
}
Добавляем ссылки для просмотра постов в шаблонах index.blade.php и search.blade.php
<a href="{{ route('post.show', ['id' => $post->post_id]) }}" class="btn btn-dark
float-right">Читать дальше</a>

Вроде все готово, можно проверять. Но тут у меня появилась ошибка:

Illuminate\Database\QueryException

SQLSTATE[42S22]: Column not found: 1054 Unknown column 'posts.id' in 'where clause'

(SQL: select * from `posts` where `posts`.`id` = 20 limit 1)


Это потому, что по умолчанию Laravel считает, что первичный ключ таблицы должен быть id, а у нас
это post_id. Давайте сообщим об этом Laravel, для этого отредактируем файл класса
модели app/Post.php.
<?php
namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model {


protected $primaryKey = 'post_id';
}

Еще раз проверяем — теперь все в порядке:

Редактирование поста блога


Добавляем шаблон 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">
@csrf
@method('PATCH')
<div class="form-group">
<input type="text" class="form-control" name="title"
placeholder="Заголовок" required value="{{ $post->title }}">
</div>
<div class="form-group">
<textarea class="form-control" name="excerpt"
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>
</form>
@endsection
Поскольку HTML-формы не могут создавать запросы PUT, PATCH или DELETE, нам нужно добавить скрытое
поле _method для имитации этих HTTP-запросов — это можно сделать с помощью
директивы @method('PATCH').

Добавляем новые маршруты

<?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');
Реализуем метод edit()
<?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 {


/* ... */
public function edit($id) {
$post = Post::find($id);
return view('posts.edit', compact('post'));
}
/* ... */
}

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


<?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 {


/* ... */
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::find($id);
$post->title = $request->input('title');
$post->excerpt = $request->input('excerpt');
$post->body = $request->input('body');
$this->uploadImage($request, $post);
$post->update();
return redirect()
->route('post.show', compact('id'))
->with('success', 'Пост успешно отредактирован');
}
/* ... */
private function uploadImage(Request $request, Post $post) {
// если надо удалить старое изображение
if ($request->input('remove')) {
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;
}
}
}
}
// если было загружено новое изображение
$source = $request->file('image');
if ($source) {
$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');
}
}
/* ... */
}
Заодно исправим метод store(), потому что код загрузки изображения теперь в отдельном методе
(чтобы не дублировать код). После обновления поста делаем редирект на страницу поста и показываем
сообщение, что пост успешно отредактирован.
Получилось не слишком удачно с маленькими изображениями. Мало того, что в таблице posts два почти
одинаковых поля image и thumb, так и работать с ними неудобно. Надо сохранять в БД только уникальное
имя файла изображения, тогда при обновлении поста будет удобнее удалять старые изображения.

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


удаление поста, семь маршрутов
Работа над ошибками
Прежде, чем двигаться дальше, давайте внесем пару мелких изменений в проект. Во-первых, у нас
сейчас два почти одинаковых шаблона 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, часть 7. Добавляем заголовки
страниц и валидация полей формы
Вроде блог у нас уже работает, можно просматривать, редактировать и удалять записи. Но есть
проблема с заголовками страниц сайта — они везде одинаковые. Заголовок задается в 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, часть 8. Регистрация и


аутентификация пользователей на сайте
В Laravel сделать аутентификацию очень просто — почти всё готово из коробки. Мы уже установили
ранее пакет laravel/ui, чтобы использовать в шаблонах фреймворк bootstrap. Для создания заготовок
всех необходимых для аутентификации контроллеров, шаблонов и роутов нужно выполнить artisan-
команду.
> php artisan ui:auth
> npm install && npm run dev

Будут созданы контроллеры:

• RegisterController — обеспечивает регистрацию пользователей


• LoginController — обеспечивает аутентификацию пользователей
• ForgotPasswordController — отправляет письмо на сброс пароля
• ResetPasswordController — содержит логику для сброса паролей

Будут созданы шаблоны:

• auth.register — форма регистрации пользователей


• auth.login — форма аутентификации пользователей
• auth.passwords.email — форма для ввода адреса почты (восстановление пароля)
• auth.passwords.reset — форма для ввода нового пароля (восстановление пароля)

Будут добавлены маршруты:


Auth::routes();
Route::get('/home', 'HomeController@index')->name('home');
Вызов Auth::routes() добавляет сразу около дюжины маршрутов, посмотреть эти маршруты можно в
классе Laravel\Ui\AuthRouteMethods.
Кроме того, будет добавлен контроллер HomeController, отвечающий за показ страницы /home. После
регистрации и аутентификации пользователи перенаправляются на эту страницу. Нам этот контроллер
не нужен, так что можно его удалить. Также можно удалить
шаблоны home.blade.php и app.blade.php — но пока не будем этого делать, нам нужно кое-что
забрать оттуда.
Сейчас пользователи уже могут регистрироваться и аутентифицироваться. Если открыть в браузере
страницу /register, то будет показана форма регистрации нового пользователя.

Но не хватает перевода на русский язык. Чтобы получить языковые файлы, установим пакет

> composer require laravel-lang/lang:~7.0


После этого нужно с директорию vendor/laravel-lang/lang/src/ru и файл vendor/laravel-
lang/lang/json/ru.json в директорию resources/lang. И смотрим на форму регистрации
пользователей еще раз.
Теперь посмотрим, что находится в layout-шаблоне app.blade.php. Нас интересует следующий
фрагмент кода:
<!-- Right Side Of Navbar -->
<ul class="navbar-nav ml-auto">
<!-- Authentication Links -->
@guest
<li class="nav-item">
<a class="nav-link" href="{{ route('login') }}">{{ __('Login') }}</a>
</li>
@if (Route::has('register'))
<li class="nav-item">
<a class="nav-link" href="{{ route('register') }}">{{
__('Register') }}</a>
</li>
@endif
@else
<li class="nav-item dropdown">
<a id="navbarDropdown" class="nav-link dropdown-toggle" href="#"
role="button" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false" v-pre>
{{ Auth::user()->name }}
</a>
<div class="dropdown-menu dropdown-menu-right" aria-
labelledby="navbarDropdown">
<a class="dropdown-item" href="{{ route('logout') }}"
onclick="event.preventDefault();
document.getElementById('logout-form').submit();">
{{ __('Logout') }}
</a>
<form id="logout-form" action="{{ route('logout') }}" method="POST"
class="d-none">
@csrf
</form>
</div>
</li>
@endguest
</ul>
Скопируем его и вставим в наш layout-шаблон site.blade.php:
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0,
maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>{{ $title ?? 'Веб-разработка' }}</title>
<link rel="shortcat icon" type="image/png" href="{{ asset('favicon.png') }}"/>
<link rel="stylesheet" href="{{ asset('css/app.css') }}">
<script src="{{ asset('js/app.js') }}"></script>
</head>
<body>
<div class="container">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<a class="navbar-brand" href="{{ route('blog.index') }}">Веб-разработка</a>
<button class="navbar-toggler" type="button" data-toggle="collapse"
data-target="#navbarSupportedContent" aria-
controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>

<!-- ссылки слева -->


<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="#">Авторы</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ route('post.create')
}}">Создать</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Контакты</a>
</li>
</ul>

<!-- форма поиска -->


<form class="form-inline my-2 my-lg-0" action="{{ route('post.search')
}}">
<input class="form-control mr-sm-2" type="search" name="search"
placeholder="Найти пост..." aria-label="Поиск">
<button class="btn btn-outline-success my-2 my-sm-0"
type="submit">Поиск</button>
</form>

<!-- ссылки справа -->


<ul class="navbar-nav ml-auto">
@guest
<li class="nav-item"> <!-- ссылка для входа -->
<a class="nav-link" href="{{ route('login') }}">{{ __('Login')
}}</a>
</li>
@if (Route::has('register'))
<li class="nav-item"> <!-- ссылка для регистрации -->
<a class="nav-link" href="{{ route('register') }}">{{
__('Register') }}</a>
</li>
@endif
@else
<li class="nav-item">
<a class="nav-link" href="{{ route('logout') }}"
onclick="event.preventDefault();
document.getElementById('logout-form').submit();">
{{ __('Logout') }} <!-- ссылка выхода -->
</a>
<form id="logout-form" action="{{ route('logout') }}"
method="POST" class="d-none">
@csrf
</form>
</li>
@endguest
</ul>
</div>
</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 class="mb-0">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif

@yield('content')
</div>
</body>
</html>
Теперь посмотрим на шаблон home.blade.php:
@extends('layouts.app')

@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">{{ __('Dashboard') }}</div>
<div class="card-body">
@if (session('status'))
<div class="alert alert-success" role="alert">
{{ session('status') }}
</div>
@endif
{{ __('You are logged in!') }}
</div>
</div>
</div>
</div>
</div>
@endsection

Здесь должно показываться flash-сообщение, но flash-переменная не установлена, так что давайте это
исправим.

class RegisterController extends Controller {


/* ... */

/**
* Сразу после регистрации выполняем редирект и устанавливаем flash-сообщение
*/
protected function registered(Request $request, $user) {
return redirect($this->redirectTo)
->with('status', trans('auth.success'));
}
}
class LoginController extends Controller {
/* ... */

/**
* Сразу после входа выполняем редирект и устанавливаем flash-сообщение
*/
protected function authenticated(Request $request, $user) {
return redirect($this->redirectTo)
->with('status', __('You are logged in!'));
}

/**
* Сразу после выхода выполняем редирект и устанавливаем flash-сообщение
*/
protected function loggedOut(Request $request) {
return redirect($this->redirectTo)
->with('status', trans('auth.loggedout'));
}
}
Для сообщения «You are logged in!» нашелся подходящий перевод в файле ru.json, а вот сообщения об
успешной регистрации и выходе из системы найти не удалось, так что добавим
ключи regsuccess и loggedout в файл resources/lang/ru/auth.php.
return [
'failed' => 'Неверное имя пользователя или пароль.',
'throttle' => 'Слишком много попыток входа. Пожалуйста, попробуйте еще раз
через :seconds секунд.',
'regsuccess' => 'Регистрация прошла успешно!',
'loggedout' => 'Вы вышли из системы!',
];
Мы все flash-сообщения показываем в layout-шаблоне, так что изменим в
методах registered(), authenticated() и loggedout() ключ status на ключ success. И изменим
значение переменной redirectTo, чтобы после регистрации, входа и выхода пользователи
перенаправлялись на главную страницу.
class RegisterController extends Controller {
/**
* Сразу после регистрации выполняем редирект на главную страницу сайта
*/
protected $redirectTo = '/';

/* ... */

/**
* Сразу после регистрации выполняем редирект и устанавливаем flash-сообщение
*/
protected function registered(Request $request, $user) {
return redirect($this->redirectTo)
->with('success', trans('auth.success'));
}
}
class LoginController extends Controller {
/**
* Куда выполнять редирект после входа в систему и после выхода из системы
*/
protected $redirectTo = '/';

/* ... */

/**
* Сразу после входа выполняем редирект и устанавливаем flash-сообщение
*/
protected function authenticated(Request $request, $user) {
return redirect($this->redirectTo)
->with('success', __('You are logged in!'));
}

/**
* Сразу после выхода выполняем редирект и устанавливаем flash-сообщение
*/
protected function loggedOut(Request $request) {
return redirect($this->redirectTo)
->with('success', trans('auth.loggedout'));
}
}
И нам еще осталось стилизовать страницы регистрации, аутентификации и восстановления пароля,
чтобы шапка сайта была как на всех прочих страницах. Для этого достаточно изменить
директиву @extends() и убрать div с css-классом container (контейнер у нас уже есть в layout-
шаблоне).
@extends('layouts.site')

@section('content')
<div class="row justify-content-center">
<div class="col-md-8">
<!-- здесь форма регистрации -->
</div>
</div>
@endsection

@extends('layouts.site')

@section('content')
<div class="row justify-content-center">
<div class="col-md-8">
<!-- здесь форма аутентификации -->
</div>
</div>
@endsection
@extends('layouts.site')

@section('content')
<div class="row justify-content-center">
<div class="col-md-8">
<!-- здесь форма ввода адреса почты (восстановление пароля) -->
</div>
</div>
@endsection
@extends('layouts.site')

@section('content')
<div class="row justify-content-center">
<div class="col-md-8">
<!-- здесь форма ввода нового пароля (восстановление пароля) -->
</div>
</div>
@endsection
Теперь можно удалить контроллер HomeController, layout-шаблон app.blade.php и
шаблон home.blade.php.

Мини-блог на Laravel, часть 9. Защита маршрутов


создания, редактирования и удаления
Чтобы предоставить доступ к определённым роутам только аутентифицированным пользователям,
можно использовать посредник (middleware). Laravel поставляется с посредником auth, который
определён в Illuminate\Auth\Middleware\Authenticate. Когда посредник определяет, что
пользователь не аутентифицирован, он перенаправляет его на роут с именем login. Мы можем
изменить это поведение в методе redirectTo класса app/Http/Middleware/Authenticate.php.
<?php
namespace App\Http\Middleware;

use Illuminate\Auth\Middleware\Authenticate as Middleware;

class Authenticate extends Middleware {


/**
* Get the path the user should be redirected to when they are not
authenticated.
*
* @param \Illuminate\Http\Request $request
* @return string|null
*/
protected function redirectTo($request) {
if (! $request->expectsJson()) {
return route('login');
}
}
}

Защита маршрутов
Давайте защитим наши маршруты создания, редактирования и удаления поста.

<?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')-
>middleware('auth');
Route::post('post/store', 'PostController@store')->name('post.store')-
>middleware('auth');
Route::get('post/show/{id}', 'PostController@show')->name('post.show');
Route::get('post/edit/{id}', 'PostController@edit')->name('post.edit')-
>middleware('auth');
Route::patch('post/update/{id}', 'PostController@update')->name('post.update')-
>middleware('auth');
Route::delete('post/destroy/{id}', 'PostController@destroy')->name('post.destroy')-
>middleware('auth');
Auth::routes();
Второй способ защитить маршруты — добавить посредник auth в конструктор контроллера.
class PostController extends Controller {
/* ... */
public function __construct() {
// не аутентифицированнные пользователи могут только просматривать
$this->middleware('auth')->except('index', 'show', 'search');
}
/* ... */
}
Определить, что пользователь уже вошёл в систему, можно с помощью
методов check() и guest() фасада Auth или хелпера auth().
use Illuminate\Support\Facades\Auth;
if (Auth::check()) {
// Пользователь вошёл в систему...
}
if (auth()->guest()) {
// Пользователь не вошёл в систему...
}

Права доступа
Теперь добавлять, редактировать и удалять посты блога могут только аутентифицированнные
пользователи. Но плохо, что все пользователи могут видеть кнопки «Редактировать» и «Удалить».
Давайте это исправим — для этого редактируем шаблон show.blade.php.
@extends('layouts.site', ['title' => $post->title])

@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">
@auth <!-- Только аутентифицированные пользователи могут
редактировать и удалять -->
@if (auth()->id() == $post->author_id) <!-- …причем,
только свои посты блога -->
<a href="{{ route('post.edit', ['id' => $post-
>post_id]) }}"
class="btn btn-dark mr-2">Редактировать</a>
<!-- Форма для удаления поста -->
<form action="{{ route('post.destroy', ['id' =>
$post->post_id]) }}"
method="post" onsubmit="return
confirm('Удалить этот пост?')"
class="d-inline">
@csrf
@method('DELETE')
<input type="submit" class="btn btn-danger"
value="Удалить">
</form>
@endif
@endauth
</span>
</div>
</div>
</div>
</div>
</div>
@endsection

Но это еще не все — посты блога может редактировать и удалять только автор. Так что вносим
изменения в методы контроллера.

<?php
namespace App\Http\Controllers;

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

class PostController extends Controller {


/* ... */
public function __construct() {
// не аутентифицированнные пользователи могут только просматривать
$this->middleware('auth')->except('index', 'show', 'search');
}
/* ... */
public function edit($id) {
$post = Post::findOrFail($id);
// пользователь может редактировать только свои посты
if (Auth::id() != $post->author_id) {
return redirect()
->route('post.index')
->withErrors('Вы можете редактировать только свои посты');
}
return view('posts.edit', compact('post'));
}
/* ... */
public function update(PostRequest $request, $id) {
$post = Post::findOrFail($id);
// пользователь может редактировать только свои посты
if (Auth::id() != $post->author_id) {
return redirect()
->route('post.index')
->withErrors('Вы можете редактировать только свои посты');
}
$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', 'Пост успешно отредактирован');
}
/* ... */
public function destroy($id) {
$post = Post::findOrFail($id);
// пользователь может удалять только свои посты
if (Auth::id() != $post->author_id) {
return redirect()
->route('post.index')
->withErrors('Вы можете удалять только свои посты');
}
$this->removeImage($post);
$post->delete();
return redirect()
->route('post.index')
->with('success', 'Пост был успешно удален');
}
}
И еще один момент — будем устанавливать идентификатор пользователя в поле author_id при
добавлении нового поста.
class PostController extends Controller {
/* ... */
public function store(PostRequest $request) {
$post = new Post();
// автор поста — текущий пользователь
$post->author_id = Auth::id();
$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', 'Новый пост успешно создан');
}
/* ... */
}

Администратор
Первый пользователь, который зарегистрировался, будет обладать правами администратора — сможет
редактировать и удалять не только свои посты, но и посты других пользователй. Мы бы могли добавить
еще одно поле admin в таблицу базы данных users. Но тогда надо будет и создавать какую-то админку,
а у нас блог с минимальным функционалом. Так что просто будем проверять, что идентификатор
администратора равен единице.
class PostController extends Controller {
/* ... */
public function edit($id) {
$post = Post::findOrFail($id);
// проверяем права пользователя на это действие
if (!$this->checkRights($post)) {
return redirect()
->route('post.index')
->withErrors('У вас нет прав на это действие');
}
return view('posts.edit', compact('post'));
}
/* ... */
public function update(PostRequest $request, $id) {
$post = Post::findOrFail($id);
// проверяем права пользователя на это действие
if (!$this->checkRights($post)) {
return redirect()
->route('post.index')
->withErrors('У вас нет прав на это действие');
}
$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 checkRights(Post $post) {
return Auth::id() == $post->author_id || Auth::id() == 1;
}
/* ... */
public function destroy($id) {
$post = Post::findOrFail($id);
// проверяем права пользователя на это действие
if (!$this->checkRights($post)) {
return redirect()
->route('post.index')
->withErrors('У вас нет прав на это действие');
}
$this->removeImage($post);
$post->delete();
return redirect()
->route('post.index')
->with('success', 'Пост был успешно удален');
}
}
@extends('layouts.site', ['title' => $post->title])

@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">
@auth
@if (auth()->id() == $post->author_id || auth()->id()
== 1)
<a href="{{ route('post.edit', ['id' => $post-
>post_id]) }}"
class="btn btn-dark mr-2">Редактировать</a>
<!-- Форма для удаления поста -->
<form action="{{ route('post.destroy', ['id' => $post-
>post_id]) }}"
method="post" onsubmit="return confirm('Удалить
этот пост?')"
class="d-inline">
@csrf
@method('DELETE')
<input type="submit" class="btn btn-danger"
value="Удалить">
</form>
@endif
@endauth
</span>
</div>
</div>
</div>
</div>
</div>
@endsection

Страница 404
Разместим картинку 404.jpg в директории public/img, где она будет доступна из веб. Создадим
директорию resources/views/errors и внутри нее — шаблон 404.blade.php.
@extends('layouts.site', ['title' => 'Страница не найдена'])

@section('content')
<div class="row">
<div class="col-12">
<div class="card mt-4 mb-4">
<div class="card-header">
<h1>Страница не найдена</h1>
</div>
<div class="card-body">
<img src="{{ asset('img/404.jpg') }}" alt="" class="img-fluid">
<p class="mt-3 mb-0">Запрошенная страница не найдена.</p>
</div>
</div>
</div>
</div>
@endsection

Вам также может понравиться