Академический Документы
Профессиональный Документы
Культура Документы
Подключаемся к серверу БД и создаем новую базу данных 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;
/**
* 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;
/**
* 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;
/**
* 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
Теперь все готово к миграции, создаем таблицы базы данных с помощью команды:
--
-- Индексы таблицы `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;
$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;
];
});
Осталось только отредактировать файлы классов UserTableSeeder и PostTableSeeder:
<?php
use Illuminate\Database\Seeder;
use Illuminate\Http\Request;
Теперь в контроллере получим массив всех записей блога и передадим этот массив в представление:
<?php
namespace App\Http\Controllers;
use App\Post;
use Illuminate\Http\Request;
/*
Route::get('/', function () {
return view('welcome');
});
*/
Route::get('/', 'PostController@index');
Если в какой-то момент при обновлении страницы не видно никаких изменений, помогут команды очистки
кэша:
Стилизуем нашу страницу со списком постов блога. Для этого нам потребуется 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>
use Illuminate\Http\Request;
/**
* 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>
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>
@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
@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');
}
/* ... */
}
use App\Post;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
<!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>
@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', 'Новый пост успешно
создан');
}
/* ... */
}
@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;
Illuminate\Database\QueryException
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'posts.id' in 'where clause'
use Illuminate\Database\Eloquent\Model;
<?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;
use App\Post;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Intervention\Image\Facades\Image;
@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;
@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;
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');
/*
Тип 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 немного другие, хотя отличия несущественные. Так что можно заменить семь
маршрутов одним — и все будет работать. Поскольку имена маршрутов у нас совпадают, а в шаблонах
мы используем только имена — в шаблонах ничего изменять не надо.
При объявлении маршрута можно указать подмножество всех возможных действий, которые должен
обрабатывать контроллер вместо полного набора стандартных действий:
@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
<?php
namespace App\Http\Controllers;
use App\Post;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Intervention\Image\Facades\Image;
/**
* 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', 'Пост был успешно удален');
}
}
> 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;
/**
* Возвращает массив правил для проверки полей формы
*
* @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;
@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">×</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>
use Illuminate\Foundation\Http\FormRequest;
При обновлении поста блога не нужно проверять уникальность заголовка, потому что такая проверка не
будет пройдена, пока не будет изменен заголовок. Но при редактировании заголовок совсем не
обязательно должен быть измененен — может быть, пользователь хочет изменить анонс или исправить
ошибку в тексте поста.
И не забываем изменить тип параметра $request метода update() контроллера на PostRequest — как
мы это делали для метода store().
Но не хватает перевода на русский язык. Чтобы получить языковые файлы, установим пакет
@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">×</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-переменная не установлена, так что давайте это
исправим.
/**
* Сразу после регистрации выполняем редирект и устанавливаем 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.
Защита маршрутов
Давайте защитим наши маршруты создания, редактирования и удаления поста.
<?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;
Администратор
Первый пользователь, который зарегистрировался, будет обладать правами администратора — сможет
редактировать и удалять не только свои посты, но и посты других пользователй. Мы бы могли добавить
еще одно поле 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