Finally, it is time for the most important part of this tutorial. Let’s create a fully featured blog application, with posts, categories, as well as tags. Previously, we discussed the CRUD operations for posts, and in this article, we are going to repeat that for categories and tags, and we are also going to discuss how to deal with the relations between them as well.
Once again, we’ll start with a fresh project. Create a working directory and change into it. Make sure Docker is up and running, then execute the following command:
1
curl -s https://laravel.build/<app_name> | bash
Change into the app directory and start the server.
1
cd <app_name>
1
./vendor/bin/sail up
To make things easier, let’s create an alias for sail. Run the following command:
1
alias sail='[ -f sail ] && sh sail || sh vendor/bin/sail'
From now on, you can run sail directly without specifying the entire path.
Step two, as we’ve mentioned before, Laravel comes with a large ecosystem, and Laravel Breeze is a part of this ecosystem. It provides a quick way to set up user authentication and registration in a Laravel application.
Breeze includes pre-built authentication views and controllers, as well as a set of backend APIs for handling user authentication and registration. The package is designed to be easy to install and configure, with minimal setup required.
Use the following commands to install Laravel Breeze:
1
sail composer require laravel/breeze --dev
1
sail artisan breeze:install
1
sail artisan migrate
1
sail npm install
1
sail npm run dev
This process will automatically generate required controllers, middleware and views that are necessary for creating a basic user authentication system. You may access the registration page by visiting http://127.0.0.1/register.
Register a new account and you will be redirected to the dashboard.
In this article, we are not going to discuss exactly how this user authentication system works, as it is related to some rather advanced concepts. But it is highly recommended that you take a look at the generated files, they offer you a deeper insight into how things work in Laravel.
Next, we need to have a big picture on how our blog app looks. First, we need to have a database that can store posts, categories, and tags. Each database table would have the following structure:
Posts
key
type
id
bigInteger
created_at
updated_at
title
string
cover
string
content
text
is_published
boolean
Categories
key
type
id
bigInteger
created_at
updated_at
name
string
Tags
key
type
id
bigInteger
created_at
updated_at
name
string
And of course, there should also be a users table, but it has already been generated for us by Laravel Breeze, so we’ll skip it this time.
These tables also have relations with each other, as shown in the list below:
Each user has multiple posts
Each category has many posts
Each tag has many posts
Each post belongs to one user
Each post belongs to one category
Each post has many tags
To create these relations, we must modify the posts table:
Posts with relations
key
type
id
bigInteger
created_at
updated_at
title
string
cover
string
content
text
is_published
boolean
user_id
bigInteger
category_id
bigInteger
And we also need a separate table for post/tag relation:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
returnnewclassextends Migration
{
/**
* Run the migrations.
*/publicfunctionup(): void
{
Schema::create('post_tag', function (Blueprint $table) {
$table->id();
$table->timestamps();
$table->bigInteger('post_id');
$table->bigInteger('tag_id');
});
}
/**
* Reverse the migrations.
*/publicfunctiondown(): void
{
Schema::dropIfExists('post_tag');
}
};
Apply these changes with the following command:
1
sail artisan migrate
And then for the corresponding models, we need to enable mass assignment for selected fields so that we may use create or update methods on them, as we’ve discussed in previous tutorials. And we also need to define relations between database tables.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
classCategoryextends Model
{
use HasFactory;
protected$fillable= [
'name',
];
publicfunctionposts(): HasMany
{
return$this->hasMany(Post::class);
}
}
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
classTagextends Model
{
use HasFactory;
protected$fillable= [
'name',
];
publicfunctionposts(): BelongsToMany
{
return$this->belongsToMany(Post::class);
}
}
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
classUserextends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/protected$fillable= [
'name',
'email',
'password',
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/protected$hidden= [
'password',
'remember_token',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/protected$casts= [
'email_verified_at'=>'datetime',
];
publicfunctionposts(): HasMany
{
return$this->hasMany(Post::class);
}
}
<?php
use App\Http\Controllers\CategoryController;
use App\Http\Controllers\PostController;
use App\Http\Controllers\ProfileController;
use App\Http\Controllers\TagController;
use Illuminate\Support\Facades\Route;
// Dashboard routes
Route::prefix('dashboard')->group(function () {
// Dashboard homepage
Route::get('/', function () {
return view('dashboard');
})->name('dashboard');
// Dashboard category resource
Route::resource('categories', CategoryController::class);
// Dashboard tag resource
Route::resource('tags', TagController::class);
// Dashboard post resource
Route::resource('posts', PostController::class);
})->middleware(['auth', 'verified']);
Route::middleware('auth')->group(function () {
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
});
require __DIR__ .'/auth.php';
Notice that all the routes are grouped with a /dashboard prefix, and the group has a middleware auth, meaning that the user must be logged in to access the dashboard.
The CategoryController and the TagController are fairly straightforward. You can set them up the same way we created the PostController in the previous article.
<?php
namespace App\Http\Controllers;
use App\Models\Category;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
classCategoryControllerextends Controller
{
/**
* Display a listing of the resource.
*/publicfunctionindex(): View
{
$categories= Category::all();
return view('categories.index', [
'categories'=>$categories ]);
}
/**
* Show the form for creating a new resource.
*/publicfunctioncreate(): View
{
return view('categories.create');
}
/**
* Store a newly created resource in storage.
*/publicfunctionstore(Request $request): RedirectResponse
{
// Get the data from the request
$name=$request->input('name');
// Create a new Post instance and put the requested data to the corresponding column
$category=new Category();
$category->name=$name;
// Save the data
$category->save();
return redirect()->route('categories.index');
}
/**
* Display the specified resource.
*/publicfunctionshow(string $id): View
{
$category= Category::all()->find($id);
$posts=$category->posts();
return view('categories.show', [
'category'=>$category,
'posts'=>$posts ]);
}
/**
* Show the form for editing the specified resource.
*/publicfunctionedit(string $id): View
{
$category= Category::all()->find($id);
return view('categories.edit', [
'category'=>$category ]);
}
/**
* Update the specified resource in storage.
*/publicfunctionupdate(Request $request, string $id): RedirectResponse
{
// Get the data from the request
$name=$request->input('name');
// Find the requested category and put the requested data to the corresponding column
$category= Category::all()->find($id);
$category->name=$name;
// Save the data
$category->save();
return redirect()->route('categories.index');
}
/**
* Remove the specified resource from storage.
*/publicfunctiondestroy(string $id): RedirectResponse
{
$category= Category::all()->find($id);
$category->delete();
return redirect()->route('categories.index');
}
}
<?php
namespace App\Http\Controllers;
use App\Models\Tag;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
classTagControllerextends Controller
{
/**
* Display a listing of the resource.
*/publicfunctionindex(): View
{
$tags= Tag::all();
return view('tags.index', [
'tags'=>$tags ]);
}
/**
* Show the form for creating a new resource.
*/publicfunctioncreate(): View
{
return view('tags.create');
}
/**
* Store a newly created resource in storage.
*/publicfunctionstore(Request $request): RedirectResponse
{
// Get the data from the request
$name=$request->input('name');
// Create a new Post instance and put the requested data to the corresponding column
$tag=new Tag();
$tag->name=$name;
// Save the data
$tag->save();
return redirect()->route('tags.index');
}
/**
* Display the specified resource.
*/publicfunctionshow(string $id): View
{
$tag= Tag::all()->find($id);
$posts=$tag->posts();
return view('tags.show', [
'tag'=>$tag,
'posts'=>$posts ]);
}
/**
* Show the form for editing the specified resource.
*/publicfunctionedit(string $id): View
{
$tag= Tag::all()->find($id);
return view('tags.edit', [
'tag'=>$tag ]);
}
/**
* Update the specified resource in storage.
*/publicfunctionupdate(Request $request, string $id): RedirectResponse
{
// Get the data from the request
$name=$request->input('name');
// Find the requested category and put the requested data to the corresponding column
$tag= Tag::all()->find($id);
$tag->name=$name;
// Save the data
$tag->save();
return redirect()->route('tags.index');
}
/**
* Remove the specified resource from storage.
*/publicfunctiondestroy(string $id): RedirectResponse
{
$tag= Tag::all()->find($id);
$tag->delete();
return redirect()->route('tags.index');
}
}
Remember that you can check the name of the routes using the following command:
The PostController, on the other hand, is a bit more complicated, since you have to deal with image uploads and relations in the store() method. Let’s take a closer look:
<?php
namespace App\Http\Controllers;
use App\Models\Category;
use App\Models\Post;
use App\Models\Tag;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Auth;
use Illuminate\Database\Eloquent\Builder;
classPostControllerextends Controller
{
.../**
* Store a newly created resource in storage.
*/publicfunctionstore(Request $request): RedirectResponse
{
// Get the data from the request
$title=$request->input('title');
$content=$request->input('content');
if ($request->input('is_published') =='on') {
$is_published=true;
} else {
$is_published=false;
}
// Create a new Post instance and put the requested data to the corresponding column
$post=new Post();
$post->title=$title;
$post->content=$content;
$post->is_published=$is_published;
// Save the cover image
$path=$request->file('cover')->store('cover', 'public');
$post->cover=$path;
// Set user
$user= Auth::user();
$post->user()->associate($user);
// Set category
$category= Category::find($request->input('category'));
$post->category()->associate($category);
// Save post
$post->save();
//Set tags
$tags=$request->input('tags');
foreach ($tagsas$tag) {
$post->tags()->attach($tag);
}
return redirect()->route('posts.index');
}
...}
A few things to be noted in this store() method. First, line 28 to 32, we are going to use an HTML checkbox to represent the is_published field, and its values are either 'on' or null. But in the database, its values are saved as true or false, so we must use an if statement to solve this issue.
Line 41 to 42, to retrieve files, we must use the file() method instead of input(), and the file is saved in the public disk under directory cover.
Line 45 to 46, get the current user using Auth::user(), and associate the post with the user using associate() method. And line 49 to 50 does the same thing for category. Remember you can only do this from $post and not $user or $category, since the user_id and category_id columns are in the posts table.
Lastly, for the tags, as demonstrated from line 56 to 60, you must save the current post to the database, and then retrieve a list of tags, and attach each of them to the post one by one, using the attach() method.
For the update() method, things work similarly, except that you must remove all existing tags before you can attach the new ones.
I’ve created three directories, posts, categories and tags, and each of them has four templates, create, edit, index and show (except for posts since it is unnecessary to have a show page for posts in the dashboard).
Including all of these views in one article would make this tutorial unnecessarily long, so instead, i’m only going to demonstrate the create, edit and index pages for posts. However, the source code for this tutorial is available for free here , if you need some reference.
I’m using TinyMCE as the rich text editor, you can replace it with something else, or simply use a <textarea></textarea> if you wish.
Line 19, this form must have enctype="multipart/form-data" since we are not just transferring texts, there are files as well.
Line 31, remember to use type="file" here since we are uploading an image.
Line 34 to 38, the value of the option will be transferred to the backend.
Line 41 to 45, there are two things you must pay attention to here. First, notice name="tags[]", the [] tells Laravel to transfer an iterable array instead of texts. And second, multiple creates a multi-select form instead of single select like the one for categories.
Line 19 to 21, by default, HTML doesn’t support PUT method, so what we can do is use method="POST", and then tell Laravel to use PUT method with {{ method_field('PUT') }}.