[Laravel series] 5. Build blog - Sử dụng route, controller để hiển thị bài viết



Trong bài số 3 [Laravel series] 3. Sử dụng Laravel Breeze để xác thực người dùng chúng ta đã sử dụng laravel breeze để xác thực người dùng. Trong bài này chúng ta sẽ sử dụng route, controller và view để điều hướng các trang cũng như hiển thị các bài blog. Mình sẽ note các ý chính của bài để mọi người tiện theo dõi.

  • Cách tạo route, route group, áp dụng middleware cho route
  • Cách tạo controller
  • Cách tạo job để xử lý nghiệp vụ
  • Cách tạo view và sử dụng view, dùng bootstrap để buid giao diện

Tạo controller PostController hiển thị danh sách và chi tiết bài viết
docker/run artisan make:controller PostController
Thêm các function sau:

/**
* @param Request $request
* @return View
*/
public function index(Request $request)
{
$categories = Category::withCount('posts')->get();
$tags = Tag::withCount('posts')->get();
$posts = Post::paginate(5);

return view('post.index')
->with([
'posts' => $posts,
'categories' => $categories,
'tags' => $tags,
]);
}

/**
* @param Post $post
* @return View
*/
public function show(Post $post): View
{
$categories = Category::withCount('posts')->get();
$tags = Tag::withCount('posts')->get();
$randomPosts = Post::inRandomOrder()->where('id', '!=', $post->id)->take(3)->get();
$comments = $post->comments()
->where('parent_id', 'in', 0)
->orderByDesc('id')
->get();

return view('post.show')
->with([
'post' => $post,
'comments' => $comments,
'categories' => $categories,
'tags' => $tags,
'randomPosts' => $randomPosts,
]);
}

Tạo controller CategoryController hiển thị danh sách bài viết theo category
docker/run artisan make:controller CategoryController
Thêm function:

/**
* @param Category $category
* @return View
*/
public function show(Category $category): View
{
$categories = Category::withCount('posts')->get();
$tags = Tag::withCount('posts')->get();
$posts = Post::where('category_id', $category->id)->paginate(5);

return view('post.index')
->with([
'posts' => $posts,
'categories' => $categories,
'tags' => $tags,
'currentCategory' => $category,
]);
}

Tạo controller TagController hiển thị danh sách bài viết theo tag
docker/run artisan make:controller TagController
Thêm function:

/**
* @param Tag $tag
* @return View
*/
public function show(Tag $tag): View
{
$categories = Category::withCount('posts')->get();
$tags = Tag::withCount('posts')->get();
$posts = Post::whereHas('tags', function (Builder $query) use ($tag) {
$query->where('id', $tag->id);
})->paginate(5);

return view('post.index')
->with([
'posts' => $posts,
'categories' => $categories,
'tags' => $tags,
'currentTag' => $tag,
]);
}

Tạo controller Auth/PostCotroller quản lý bài viết của user đang đăng nhập sử dụng middleware auth để xác thực người dùng.
docker/run artisan make:controller Auth/PostController

Update route /routers/web.php

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

require __DIR__.'/auth.php';

Route::prefix('categories')->group(function () {
Route::get('/{category}', [CategoryController::class, 'show'])->name('categories.show');
});

Route::prefix('tags')->group(function () {
Route::get('/{tag}', [TagController::class, 'show'])->name('tags.show');
});

Route::prefix('posts')->group(function () {
Route::get('/', [PostController::class, 'index'])->name('posts.index');
Route::get('/{post}', [PostController::class, 'show'])->name('posts.show');
});

Update đường dẫn trang chủ về /posts trang danh sách bài viết.
sửa file /app/Providers/RouteServiceProvider.php
public const HOME = '/posts';

Sử dụng bootstrap cho pagination.
sửa file /app/Providers/AppServiceProvider.php, hàm boot thêm đoạn sau:
Paginator::useBootstrap();

Update route binding cho Category, Post và Tag, vì mặc định laravel dùng primary key để binding model, trong khi chúng ta đang dùng field slug.
Sửa file app/Models/Category/Category.php thêm function:

/**
* @param mixed $value
* @param null $field
* @return Model|null
*/
public function resolveRouteBinding($value, $field = null): ?Model
{
return Category::where('slug', $value)->firstOrFail();
}

/**
* @return HasMany
*/
public function posts(): HasMany
{
return $this->hasMany(Post::class);
}

Sửa file app/Models/Post/Post.php thêm function và các quan hệ:

protected $fillable = [
'author_id',
'category_id',
'title',
'slug',
'summary',
'content',
'status',
];

/**
* @param mixed $value
* @param null $field
* @return Model|null
*/
public function resolveRouteBinding($value, $field = null): ?Model
{
return Post::where('slug', $value)->firstOrFail();
}

/**
* @return BelongsTo
*/
public function author(): BelongsTo
{
return $this->belongsTo(User::class, 'author_id');
}

/**
* @return BelongsTo
*/
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}

/**
* @return HasMany
*/
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}

/**
* @return BelongsToMany
*/
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class);
}

Install bootstrap:
composer require laravel/ui --dev
docker/run artisan ui bootstrap

Sửa file webpack.mix.js như sau:

const mix = require('laravel-mix');

mix
.js('resources/js/app.js', 'public/js')
.postCss('resources/css/app.css', 'public/css/tailwind.css', [
require('postcss-import'),
require('tailwindcss'),
require('autoprefixer'),
])
.sass('resources/sass/app.scss', 'public/css/boostrap.css')
.sourceMaps();

Chạy lệnh:

npm install
npm run dev

File /resouces/views/layouts/guest.blade.php
sửa asset('css/app.css') thành asset('css/tailwind.css')

Vì ở bài [Laravel series] 3. Sử dụng Laravel Breeze để xác thực người dùng chúng ta dùng breeze để tạo ra các view để xác thực người dùng (login, logout …), mà breeze lại đang xài tailwind nên là ở phần authen mình sẽ dùng tailwind.

File /resouces/views/layouts/app.blade.php
sửa asset('css/app.css') thành asset('css/bootstrap.css')

File /resouces/views/layouts/guest.blade.php
sửa asset('css/app.css') thành asset('css/tailwind.css')

Thêm file resources/views/components/breadcrumb.blade.php

@php
if (!isset($currentCategory)) {
$currentCategory = isset($post) ? $post->category : null;
}

$currentTag = $currentTag ?? null;

$activeHome = request()->routeIs('posts.index');
$activeCategory = request()->routeIs('categories.show');
$activeTag = request()->routeIs('tags.show');
$activePost = request()->routeIs('posts.show');
@endphp

<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item {{ $activeHome ? 'active' : '' }}">
@if($activeHome)
Home
@else
<a href="{{ route('posts.index') }}">Home</a>
@endif
</li>
@if($currentCategory)
<li class="breadcrumb-item {{ $activeCategory ? 'active' : '' }}">
@if($activeCategory)
{{ $currentCategory->name }}
@else
<a href="{{ route('categories.show', ['category' => $currentCategory->slug]) }}">{{ $currentCategory->name }}</a>
@endif
</li>
@endif

@if($currentTag)
<li class="breadcrumb-item {{ $activeTag ? 'active' : '' }}">
@if($activeTag)
{{ $currentTag->name }}
@else
<a href="{{ route('tags.show', ['tag' => $currentTag->slug]) }}">{{ $currentTag->name }}</a>
@endif
</li>
@endif

@if(isset($post))
<li class="breadcrumb-item active" aria-current="page">{{ $post->title }}</li>
@endif
</ol>
</nav>

Thêm file resource/views/post/index.blade.php

@extends('layouts.app')

@section('content')
<div class="container">
@include('components.breadcrumb')
<div class="row justify-content-center">
<div class="col-md-3">
<ul class="nav nav-pills flex-column mb-5">
@foreach($categories as $category)
<li class="nav-item">
<a class="nav-link {{ isset($currentCategory) && $currentCategory->id === $category->id ? 'active' : '' }}" href="{{ route('categories.show', ['category' => $category->slug]) }}">{{ $category->name }} <span class="badge badge-primary">{{ $category->posts_count }}</span></a>
</li>
@endforeach
</ul>

<div id="tags-container" class="bm-5">
@foreach($tags as $tag)
<a class="btn btn-sm btn-light {{ isset($currentTag) && $currentTag->id === $tag->id ? 'active' : '' }}" href="{{ route('tags.show', ['tag' => $tag->slug]) }}">{{ $tag->name }} <span class="badge badge-primary">{{ $tag->posts_count }}</span></a>
@endforeach
</div>
</div>
<div class="col-md-9">
@forelse($posts as $post)
<div class="mb-5">
<h2><a href="{{ route('posts.show', ['post' => $post->slug]) }}">{{ $post->title }}</a></h2>
<p>{{ $post->summary }}</p>
<p>{{ $post->author->name }} | {{ $post->created_at }}</p>
<hr>
</div>
@empty
<div class="mb-5">
<p class="text-center">No posts</p>
</div>
@endforelse

{{ $posts->onEachSide(3)->links() }}
</div>
</div>
</div>
@endsection

Thêm file resource/views/post/show.blade.php

@extends('layouts.app')

@section('content')
<div class="container">
@include('components.breadcrumb')
<div class="row justify-content-center">
<div class="col-md-3">
<ul class="nav nav-pills flex-column mb-5">
@foreach($categories as $category)
<li class="nav-item">
<a class="nav-link {{ isset($currentCategory) && $currentCategory->id === $category->id ? 'active' : '' }}" href="{{ route('categories.show', ['category' => $category->slug]) }}">{{ $category->name }}</a>
</li>
@endforeach
</ul>

<div id="tags-container" class="bm-5">
@foreach($tags as $tag)
<a class="btn btn-sm btn-light {{ isset($currentTag) && $currentTag->id === $tag->id ? 'active' : '' }}" href="{{ route('tags.show', ['tag' => $tag->slug]) }}">{{ $tag->name }} <span class="badge badge-primary">{{ $tag->posts_count }}</span></a>
@endforeach
</div>
</div>
<div class="col-md-9">
<div class="mb-3">
<h1>{{ $post->title }}</h1>
<p>{{ $post->author->name }} | {{ $post->created_at }}</p>
<hr>

<blockquote class="blockquote">
<p class="mb-0">{{ $post->summary }}</p>
</blockquote>

<div>{{ $post->content }}</div>
</div>

<div>
<h3>{{ __('Tags') }}</h3>
@foreach($post->tags as $tag)
<a class="btn btn-sm btn-dark" href="{{ route('tags.show', ['tag' => $tag->slug]) }}">{{ $tag->name }}</a>
@endforeach
</div>

<div class="mt-3">
<h3>{{ __('Comments') }}</h3>
<form class="mb-3" action="{{ route('comments.store') }}" method="POST">
<div class="form-group">
@csrf
<input type="hidden" name="postId" value="{{ $post->id }}">
<input type="hidden" name="parentId" value="">
<textarea class="form-control" name="content" rows="5" placeholder="{{ __('Input your comment') }}"></textarea>
</div>
<button type="submit" class="btn btn-block btn-outline-primary">{{ __('Submit') }}</button>
</form>
@if ($comments->isNotEmpty())
<div id="comment-container">
@foreach($comments as $comment)
<div class="border rounded p-3 mb-3">
<div>{{ $comment->author->name }} | {{ $comment->created_at }}</div>
<div>{{ $comment->content }}</div>

@foreach($comment->children as $childComment)
<div class="ml-5 mt-3">
<div>{{ $childComment->content }}</div>
<div class="text-right">{{ $childComment->author->name }} | {{ $childComment->created_at }}</div>
@if(!$loop->last)
<hr>
@endif
</div>
@endforeach
</div>
@endforeach
</div>
@endif
</div>

<hr>
<h3 class="mt-5">{{ __('List Recommend') }}</h3>
<div class="list-group">
@foreach($randomPosts as $randomPost)
<a href="{{ route('posts.show', ['post' => $randomPost->slug]) }}" class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{ $randomPost->title }}</h5>
<small>{{ $post->author->name }}</small>
</div>
<p class="mb-1">{{ $randomPost->summary }}</p>
</a>
@endforeach
</div>
</div>
</div>
</div>

<style>
#comment-container {
max-height: 500px;
overflow: scroll;
}
</style>
@endsection

Cơ bản chúng ta đã có thể show được danh sách bài viết, theo category, theo tag, xem chi tiết bài viết nhưng chúng ta chỉ đang code để chạy được :), chứ chưa tối ưu cho việc bảo trì và đang bị lặp lại code khá nhiều, trong bài tiếp theo chúng ta hãy cùng refactor (tối ưu) lại code để code clear hơn nhé.

Nhận xét

Bài đăng phổ biến từ blog này

[Laravel series] 3. Sử dụng Laravel Breeze để xác thực người dùng

[Laravel series] 2. Cài đặt môi trường local