Блог на Laravel 7, часть 6. Публичная часть — все посты, посты категории, посты автора
17.12.2020
Теги: Laravel • MySQL • PHP • Web-разработка • БазаДанных • Блог • Практика • Фреймворк • ШаблонСайта
Теперь займемся публичной частью блога — список всех постов, список постов категории, список постов автора, список постов с тегом, страница просмотра поста. Сначала добавим маршруты, потом создадим контроллер BlogController
, потом — шаблоны для показа всех этих страниц. И добавим связи между моделями, чтобы по автору и тегу получать список постов.
Добавляем маршртуты в файл route/web.php
:
use Illuminate\Support\Facades\Route; use App\Http\Controllers\BlogController; /* * Блог: все посты, посты категории, посты тега, страница поста */ Route::group([ 'as' => 'blog.', // имя маршрута, например blog.index 'prefix' => 'blog', // префикс маршрута, например blog/index ], function () { // главная страница (все посты) Route::get('index', [BlogController::class, 'index']) ->name('index'); // категория блога (посты категории) Route::get('category/{category:slug}', [BlogController::class, 'category']) ->name('category'); // тег блога (посты с этим тегом) Route::get('tag/{tag:slug}', [BlogController::class, 'tag']) ->name('tag'); // автор блога (посты этого автора) Route::get('author/{user}', [BlogController::class, 'author']) ->name('author'); // страница просмотра поста блога Route::get('post/{post:slug}', [BlogController::class, 'post']) ->name('post'); });
Создаем контроллер BlogController
:
namespace App\Http\Controllers; use App\Category; use App\Post; use App\Tag; use App\User; use Illuminate\Http\Request; class BlogController extends Controller { /** * Главная страница блога (список всех постов) */ public function index() { $posts = Post::orderBy('created_at', 'desc')->paginate(5); return view('blog.index', compact('posts')); } /** * Страница просмотра отдельного поста блога */ public function post(Post $post) { return view('blog.post', compact('post')); } /** * Список постов блога выбранной категории */ public function category(Category $category) { $posts = $category->posts() ->orderBy('created_at', 'desc') ->paginate(5); return view('blog.category', compact('category', 'posts')); } /** * Список постов блога выбранного автора */ public function author(User $user) { $posts = $user->posts() ->orderBy('created_at', 'desc') ->paginate(5); return view('blog.author', compact('user', 'posts')); } /** * Список постов блога с выбранным тегом */ public function tag(Tag $tag) { $posts = $tag->posts() ->orderBy('created_at', 'desc') ->paginate(5); return view('blog.tag', compact('tag', 'posts')); } }
Создаем связи между моделями:
class User extends Authenticatable { /** * Связь модели User с моделью Post, позволяет получить все * посты пользователя */ public function posts() { return $this->hasMany(Post::class); } /* ... */ }
namespace App; use Illuminate\Database\Eloquent\Model; class Tag extends Model { /** * Связь модели Tag с моделью Post, позволяет получить посты, * связанные с тегом через сводную таблицу post_tag */ public function posts() { return $this->belongsToMany(Post::class)->withTimestamps(); } /* ... */ }
namespace App; use Illuminate\Database\Eloquent\Model; class Category extends Model { /** * Связь модели Category с моделью Post, позволяет получить все * посты, размещенные в текущей категори */ public function posts() { return $this->hasMany(Post::class); } /** * Связь модели Category с моделью Category, позволяет получить * родителя текущей категории */ public function parent() { return $this->belongsTo(Category::class, 'parent_id'); } /** * Связь модели Category с моделью Category, позволяет получить все * дочерние категории текущей категории */ public function children() { return $this->hasMany(Category::class, 'parent_id'); } }
roots()
, descendants()
, hierarchy()
— которые были добавлены в предыдущей части. Поскольку построить дерево категорий блога для меню в левой колонке получилось и без этих методов.
namespace App; use Illuminate\Database\Eloquent\Model; class Post extends Model { /** * Связь модели Post с моделью Tag, позволяет получить * все теги поста */ public function tags() { return $this->belongsToMany(Tag::class)->withTimestamps(); } /** * Связь модели Post с моделью Category, позволяет получить * родительскую категорию поста */ public function category() { return $this->belongsTo(Category::class); } /** * Связь модели Post с моделью User, позволяет получить * автора поста */ public function user() { return $this->belongsTo(User::class); } }
Шаблон resources/views/blog/index.blade.php
для показа списка всех постов блога:
@extends('layout.site', ['title' => 'Все посты блога']) @section('content') <h1 class="mb-3">Все посты блога</h1> @foreach ($posts as $post) @include('blog.part.post', ['post' => $post]) @endforeach {{ $posts->links() }} @endsection
Шаблон resources/views/blog/index.blade.php
для показа списка постов категории:
@extends('layout.site', ['title' => $category->name]) @section('content') <h1 class="mb-3">{{ $category->name }}</h1> @foreach ($posts as $post) @include('blog.part.post', ['post' => $post]) @endforeach {{ $posts->links() }} @endsection
Шаблон resources/views/blog/author.blade.php
для показа списка постов одного автора:
@extends('layout.site', ['title' => 'Посты автора: ' . $user->name]) @section('content') <h1 class="mb-3">Посты автора: {{ $user->name }}</h1> @foreach ($posts as $post) @include('blog.part.post', ['post' => $post]) @endforeach {{ $posts->links() }} @endsection
Шаблон resources/views/blog/author.blade.php
для показа списка всех постов с тегом:
@extends('layout.site', ['title' => 'Посты с тегом: ' . $tag->name]) @section('content') <h1 class="mb-3">Посты с тегом: {{ $tag->name }}</h1> @foreach ($posts as $post) @include('blog.part.post', ['post' => $post]) @endforeach {{ $posts->links() }} @endsection
Все эти шаблоны подключают еще один шаблон resources/views/blog/part/post.blade.php
:
<div class="card mb-4"> <div class="card-header"> <h2>{{ $post->name }}</h2> </div> <div class="card-body"> <img src="https://via.placeholder.com/1000x300" 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"> Автор: <a href="{{ route('blog.author', ['user' => $post->user->id]) }}"> {{ $post->user->name }} </a> <br> Дата: {{ $post->created_at }} </span> <span class="float-right"> <a href="{{ route('blog.post', ['post' => $post->slug]) }}" class="btn btn-dark">Читать дальше</a> </span> </div> </div> @if ($post->tags->count()) <div class="card-footer"> Теги: @foreach($post->tags as $tag) @php $comma = $loop->last ? '' : ' • ' @endphp <a href="{{ route('blog.tag', ['tag' => $tag->slug]) }}">{{ $tag->name }}</a> {{ $comma }} @endforeach </div> @endif </div>
Шаблон resources/views/blog/post.blade.php
для показа отдельного поста блога:
@extends('layout.site', ['title' => $post->name]) @section('content') <div class="card mb-4"> <div class="card-header"> <h1>{{ $post->name }}</h1> </div> <div class="card-body"> <img src="http://via.placeholder.com/1000x300" alt="" class="img-fluid"> <div class="mt-4">{!! $post->content !!}</div> </div> <div class="card-footer"> Автор: <a href="{{ route('blog.author', ['user' => $post->user->id]) }}"> {{ $post->user->name }} </a> <br> Дата: {{ $post->created_at }} </div> @if ($post->tags->count()) <div class="card-footer"> Теги: @foreach($post->tags as $tag) @php $comma = $loop->last ? '' : ' • ' @endphp <a href="{{ route('blog.tag', ['tag' => $tag->slug]) }}"> {{ $tag->name }}</a> {{ $comma }} @endforeach </div> @endif </div> @endsection
С показом списка постов категории получилось неудачно — хотя мы добавили связь, позволяющую получить все посты категории, это не совсем то, что нам нужно. Эта связь позволяет получить только посты, размещенные в этой категории, но не позволяет получить посты, размещенные в дочерних категориях. Так что добавим еще один метод в модель Category
и исправим метод category()
контроллера.
class Category extends Model { /** * Возвращает массив идентификаторов всех потомков категории */ public static function descendants($id) { // получаем прямых потомков категории с идентификатором $id $children = self::where('parent_id', $id)->get(); $ids = []; foreach ($children as $child) { $ids[] = $child->id; // для каждого прямого потомка получаем его прямых потомков if ($child->children->count()) { $ids = array_merge($ids, self::descendants($child->id)); } } return $ids; } }
class BlogController extends Controller { /** * Список постов блога выбранной категории */ public function category(Category $category) { $descendants = array_merge(Category::descendants($category->id), [$category->id]); $posts = Post::whereIn('category_id', $descendants) ->orderBy('created_at', 'desc') ->paginate(5); return view('blog.category', compact('category', 'posts')); } }
Оптимизация кол-ва запросов
Посмотрел, сколько SQL-запросов было выполнено при формировании страницы списка постов категории:
# laravel получает данные о категории по slug (привязка модели к маршруту) select * from `categories` where `slug` = 'selifanu-sei-ze-cas-299' limit 1
# первый вызов рекурсивной функции Category::descendants() select * from `categories` where `parent_id` = 1
# запрос при обращении к виртуальному свойству children, при выполнении кода $child->children->count() select * from `categories` where `categories`.`parent_id` = 5 and `categories`.`parent_id` is not null
# второй вызов рекурсивной функции Category::descendants() select * from `categories` where `parent_id` = 5
# запрос при обращении к виртуальному свойству children, при выполнении кода $child->children->count() select * from `categories` where `categories`.`parent_id` = 6 and `categories`.`parent_id` is not null
# служебный запрос для построения постраничной навгигации select count(*) as `aggregate` from `posts` where `category_id` in (5, 6, 1)
# получаем посты, расположенные в этой категории и во всех потомках select * from `posts` where `category_id` in (5, 6, 1) order by `created_at` desc limit 5 offset 0
# получаем данные об авторе первого поста в списке select * from `users` where `users`.`id` = 4 limit 1
# получаем все теги для первого поста в списке select `tags`.*, `post_tag`.`post_id` as `pivot_post_id`, `post_tag`.`tag_id` as `pivot_tag_id`, `post_tag`.`created_at` as `pivot_created_at`, `post_tag`.`updated_at` as `pivot_updated_at` from `tags` inner join `post_tag` on `tags`.`id` = `post_tag`.`tag_id` where `post_tag`.`post_id` = 30
# получаем данные об авторе второго поста в списке select * from `users` where `users`.`id` = 10 limit 1
# получаем все теги для второго поста в списке select `tags`.*, `post_tag`.`post_id` as `pivot_post_id`, `post_tag`.`tag_id` as `pivot_tag_id`, `post_tag`.`created_at` as `pivot_created_at`, `post_tag`.`updated_at` as `pivot_updated_at` from `tags` inner join `post_tag` on `tags`.`id` = `post_tag`.`tag_id` where `post_tag`.`post_id` = 25
# получаем данные об авторе третьего поста в списке select * from `users` where `users`.`id` = 8 limit 1
# получаем все теги для третьего поста в списке select `tags`.*, `post_tag`.`post_id` as `pivot_post_id`, `post_tag`.`tag_id` as `pivot_tag_id`, `post_tag`.`created_at` as `pivot_created_at`, `post_tag`.`updated_at` as `pivot_updated_at` from `tags` inner join `post_tag` on `tags`.`id` = `post_tag`.`tag_id` where `post_tag`.`post_id` = 32
# получаем данные об авторе четвертого поста в списке select * from `users` where `users`.`id` = 5 limit 1
# получаем все теги для четвертого поста в списке select `tags`.*, `post_tag`.`post_id` as `pivot_post_id`, `post_tag`.`tag_id` as `pivot_tag_id`, `post_tag`.`created_at` as `pivot_created_at`, `post_tag`.`updated_at` as `pivot_updated_at` from `tags` inner join `post_tag` on `tags`.`id` = `post_tag`.`tag_id` where `post_tag`.`post_id` = 12
# получаем данные об авторе пятого поста в списке select * from `users` where `users`.`id` = 10 limit 1
# получаем все теги для пятого поста в списке select `tags`.*, `post_tag`.`post_id` as `pivot_post_id`, `post_tag`.`tag_id` as `pivot_tag_id`, `post_tag`.`created_at` as `pivot_created_at`, `post_tag`.`updated_at` as `pivot_updated_at` from `tags` inner join `post_tag` on `tags`.`id` = `post_tag`.`tag_id` where `post_tag`.`post_id` = 41
# все категории для построения меню в левой колонке select * from `categories`
# популяртные теги для меню в левой колонке select `tags`.*, (select count(*) from `posts` inner join `post_tag` on `posts`.`id` = `post_tag`.`post_id` where `tags`.`id` = `post_tag`.`tag_id`) as `posts_count` from `tags` order by `posts_count` desc limit 10
Давайте уменьшим кол-во запросов — для этого используем жадную загрузку в методах контроллера:
class BlogController extends Controller { /** * Главная страница блога (список всех постов) */ public function index() { $posts = Post::with('user')->with('tags') ->orderBy('created_at', 'desc') ->paginate(5); return view('blog.index', compact('posts')); } /** * Страница просмотра отдельного поста блога */ public function post(Post $post) { return view('blog.post', compact('post')); } /** * Список постов блога выбранной категории */ public function category(Category $category) { $descendants = array_merge(Category::descendants($category->id), [$category->id]); $posts = Post::whereIn('category_id', $descendants) ->with('user')->with('tags') ->orderBy('created_at', 'desc') ->paginate(5); return view('blog.category', compact('category', 'posts')); } /** * Список постов блога выбранного автора */ public function author(User $user) { $posts = $user->posts() ->with('user')->with('tags') ->orderBy('created_at', 'desc') ->paginate(5); return view('blog.author', compact('user', 'posts')); } /** * Список постов блога с выбранным тегом */ public function tag(Tag $tag) { $posts = $tag->posts() ->with('user')->with('tags') ->orderBy('created_at', 'desc') ->paginate(5); return view('blog.tag', compact('tag', 'posts')); } }
Кроме того, перепишем метод Category::descendants()
, чтобы он выполнял только один SQL-запрос:
class Category extends Model { /** * Возвращает массив идентификаторов всех потомков категории */ public static function descendants($parent) { static $items = null; if (is_null($items)) { $items = self::all(); } $ids = []; foreach ($items->where('parent_id', $parent) as $item) { $ids[] = $item->id; $ids = array_merge($ids, self::descendants($item->id)); } return $ids; } }
В итоге, получим только восемь запросов вместо девятнадцати:
# laravel получает данные о категории по slug (привязка модели к маршруту) select * from `categories` where `slug` = 'selifanu-sei-ze-cas-299' limit 1
# первый вызов рекурсивной функции Category::descendants() select * from `categories`
# служебный запрос для построения постраничной навгигации select count(*) as `aggregate` from `posts` where `category_id` in (5, 6, 1)
# получаем посты, расположенные в этой категории и во всех потомках select * from `posts` where `category_id` in (5, 6, 1) order by `created_at` desc limit 5 offset 0
# получаем данные об авторах всех постов в списке select * from `users` where `users`.`id` in (4, 5, 8, 10)
# получаем все теги для всех постов в списке select `tags`.*, `post_tag`.`post_id` as `pivot_post_id`, `post_tag`.`tag_id` as `pivot_tag_id`, `post_tag`.`created_at` as `pivot_created_at`, `post_tag`.`updated_at` as `pivot_updated_at` from `tags` inner join `post_tag` on `tags`.`id` = `post_tag`.`tag_id` where `post_tag`.`post_id` in (12, 25, 30, 32, 41)
# все категории для построения меню в левой колонке select * from `categories`
# популяртные теги для меню в левой колонке select `tags`.*, (select count(*) from `posts` inner join `post_tag` on `posts`.`id` = `post_tag`.`post_id` where `tags`.`id` = `post_tag`.`tag_id`) as `posts_count` from `tags` order by `posts_count` desc limit 10
Комментрии к посту
Совсем забыл про комментрии к посту, давайте внесем изменения в контроллер и создадим шаблон для показа комментариев. Кроме того, нам надо показывать не все посты и комментарии, а только опубликованные — в этом нам помогут методы scopePublished()
моделей Post
и Comment
.
class Post extends Model { /** * Количество постов на странице при пагинации */ protected $perPage = 5; /** * Выбирать из БД только опубликовынные посты */ public function scopePublished($builder) { return $builder->whereNotNull('published_by'); } /* ... */ }
class Comment extends Model { /** * Количество комментриев на странице при пагинации */ protected $perPage = 5; /** * Выбирать из БД только опубликованные комментарии */ public function scopePublished($builder) { return $builder->whereNotNull('published_by'); } /* ... */ }
namespace App\Http\Controllers; use App\Category; use App\Comment; use App\Post; use App\Tag; use App\User; use Illuminate\Http\Request; class BlogController extends Controller { /** * Главная страница блога (список всех постов) */ public function index() { $posts = Post::published() ->with('user')->with('tags') ->orderByDesc('created_at') ->paginate(); return view('blog.index', compact('posts')); } /** * Страница просмотра отдельного поста блога */ public function post(Post $post) { $comments = $post->comments() ->published() ->orderBy('created_at') ->paginate(); return view('blog.post', compact('post', 'comments')); } /** * Список постов блога выбранной категории */ public function category(Category $category) { $descendants = array_merge(Category::descendants($category->id), [$category->id]); $posts = Post::whereIn('category_id', $descendants) ->published() ->with('user')->with('tags') ->orderByDesc('created_at') ->paginate(); return view('blog.category', compact('category', 'posts')); } /** * Список постов блога выбранного автора */ public function author(User $user) { $posts = $user->posts() ->published() ->with('user')->with('tags') ->orderByDesc('created_at') ->paginate(); return view('blog.author', compact('user', 'posts')); } /** * Список постов блога с выбранным тегом */ public function tag(Tag $tag) { $posts = $tag->posts() ->published() ->with('user')->with('tags') ->orderByDesc('created_at') ->paginate(); return view('blog.tag', compact('tag', 'posts')); } }
Шаблон resources/views/blog/part/comments.blade.php
для просмотра коментариев:
<h3 id="comment-list">Все комментарии</h3> @if ($comments->count()) @foreach ($comments as $comment) <div class="card mb-3" id="comment-{{ $comment->id }}"> <div class="card-header p-2"> {{ $comment->user->name }} </div> <div class="card-body p-2"> {{ $comment->content }} </div> <div class="card-footer p-2"> {{ $comment->created_at }} </div> </div> @endforeach {{ $comments->fragment('comment-list')->links() }} @else <p>К этому посту еще нет комментариев</p> @endif
И подключим его в шаблоне resources/views/blog/post.blade.php
для просмотра поста:
@extends('layout.site', ['title' => $post->name]) @section('content') <div class="card mb-4"> <!-- просмотр поста --> </div> @include('blog.part.comments', ['comments' => $comments]) @endsection
- Блог на Laravel 7, часть 12. Доп.страницы сайта в панели управления и в публичной части
- Блог на Laravel 7, часть 11. Панель управления — назначение ролей и прав для пользователей
- Блог на Laravel 7, часть 10. Личный кабинет — CRUD-операции над постами и комментариями
- Блог на Laravel 7, часть 9. Панель управления — создание, публикация, удаление комментариев
- Блог на Laravel 7, часть 8. Панель управления — CRUD для категорий, тегов и пользователей
- Блог на Laravel 7, часть 7. Панель управления — создание, публикация, удаление постов
- Блог на Laravel 7, часть 3. Checkbox «Запомнить меня» и подтверждение адреса почты
Поиск: Laravel • MySQL • PHP • Web-разработка • База данных • Блог • Практика • Фреймворк • Шаблон сайта