Laravel is a free, open-source PHP web framework that is widely used for web application development. It is known for its elegant syntax, built-in tools for common tasks such as routing, authentication, and caching, and its ability to handle large traffic. Laravel follows the model-view-controller (MVC) architecture and includes built-in support for object-relational mapping (ORM), making it easy to interact with databases.
Laravel is considered one of the most popular and powerful web frameworks for PHP, and is trusted by many businesses and organizations to build robust and scalable web applications. This tutorial explains the basic use of the Laravel framework by building a simple blogging system. Without further ado, let's get started.
You can access the source code for this tutorial here 👈.
Creating a fresh Laravel project
Setting up a PHP dev environment used to be a headache, especially if you are using Linux systems. But luckily, since the release of Laravel 8.0, it offers us a new tool called Laravel Sail, a easy solution for running Laravel applications through Docker, regardless of which operating system you are on, as long as you have Docker installed.
First of all, create a new work directory and change into it.
1mkdir <work_dir>
1cd <work_dir>
And then execute the following command to create a fresh Laravel project. Make sure Docker in up and running.
1curl -s https://laravel.build/<app_name> | bash
This process could take 5 to 10 minutes, and after it is finished, a new <app_name>
directory should be created.
Note: If you are working on Windows, make sure you are running this command inside WSL.
Next, change into the app directory and start the server.
1cd <app_name>
1./vendor/bin/sail up
This command will start the Docker container as well as the Laravel dev server. Open the browser and go to http://localhost/
. You should see the following Laravel welcome page.
Exploring Laravel application structure
And now, let's take a look inside the project directory.
1.
2├── app
3│ ├── Console
4│ ├── Exceptions
5│ ├── Http
6│ │ ├── Controllers
7│ │ └── Middleware
8│ ├── Models
9│ └── Providers
10├── bootstrap
11├── config
12├── database
13│ ├── factories
14│ ├── migrations
15│ └── seeders
16├── public
17├── resources
18│ ├── css
19│ ├── js
20│ └── views
21├── routes
22├── storage
23│ ├── app
24│ ├── framework
25│ └── logs
26├── tests
27│ ├── Feature
28│ └── Unit
29└── vendor
- The
app
directory: This directory is the core component of our project. It contains the controllers, middleware, and models. The controller defines the core logic of the app, the middleware defines what actions should be taken before the controller is called, and the model provides an interface allowing us to deal with databases. We'll discuss each of them in detail later. - The
bootstrap
directory: This directory contains theapp.php
file which bootstraps the entire project. You don't need to modify anything in this directory. - The
config
directory: As the name suggests, it contains the configuration files. - The
database
directory: Contains migration files, factories and seeds. The migration files describes the structure of the database. The factories and seeds are two different ways we can fill the database with dummy data using Laravel. - The
public
directory: Containsindex.php
, which is the entry point of our app. - The
resources
directory: Contains view files, which is the frontend part of a Laravel application. - The
routes
directory: Contains all URL routers for our project. - The
storage
directory: A storage space for the entire project. Contains logs, compiled views, as well as user uploaded files. - The
tests
directory: Contains test files. - The
vendor
directory: Includes all dependencies.
Environmental variables
Next, we also need to take a closer look at the environmental variables. All the environmental variables are stored in the .env
file. Since we created our project using Laravel Sail, a lot of configurations are set by default, but we still need to talk about what they do.
- App URL
The APP_URL
variable defines the URL of the application. By default, it is http://localhost
, but you might need to change it to http://127.0.0.1
if http://localhost
is giving you the apache2 default page.
1APP_URL=http://127.0.0.1
1APP_URL=http://localhost
- Database configurations
By default, Laravel Sail will use MySQL as our database, and the connection settings are defined as follows:
1DB_CONNECTION=mysql
2DB_HOST=mysql
3DB_PORT=3306
4DB_DATABASE=curl_demo
5DB_USERNAME=sail
6DB_PASSWORD=password
- Custom variables
If you want to, you may also define your own custom environmental variables, and then you can access them anywhere in this project.
1CUSTOM_VARIABLE=true
This variable can be accessed like this:
1env('CUSTOM_VARIABLE', true)
The second parameter is the default value, if CUSTOM_VARIABLE
does not exist, then the second parameter will be returned instead.
Basic routing in Laravel
In this section, we are going to look at Laravel's route and middleware. In Laravel, the routes are defined in the routes
directory.
Look inside, and notice that there are four different files. In most cases, you only need api.php
and web.php
. If you intend to use Laravel strictly as the backend (without the view), you should define the routes in the api.php
. For our tutorial, we are going to use Laravel as a full-stack framework, so we are going to use web.php
instead.
Their main difference is that api.php
is wrapped inside the api
middleware group and web.php
is inside the web
middleware group, they provide different functionalities, and the routes defined in api.php
will have the URL prefix /api/
. Meaning that in order to access an api
route, the URL has to be something like: http://example.com/api/somthing-else
.
A route handler in Laravel accepts a URL and then returns a value. The value could be a string, a view or a controller. Go to routes/web.php
, and you can see there is already a pre-defined route:
routes/web.php
1use Illuminate\Support\Facades\Route;
2
3Route::get('/', function () {
4 return view('welcome');
5});
This piece of code means when the Laravel route receives "/
", it returns a view called "welcome
", which is located at resources/views/welcome.blade.php
.
Open the browser, go to http://127.0.0.1:8000, and you will get this page:
To verify welcome.blade.php
is the view we are looking at, try making some changes to the file, and refresh your browser to see if the page changes.
Router methods
Now, let's take a closer look at this router and understand how it works.
routes/web.php
1use Illuminate\Support\Facades\Route;
2
3Route::get('/', function () {
4 return view('welcome');
5});
We first import the Route
class, and invoked the get()
method. This get()
matches the GET
HTTP method we talked about before. There are other methods built into the Route
class allowing us to match any other HTTP request methods.
1Route::get($uri, $callback);
2Route::post($uri, $callback);
3Route::put($uri, $callback);
4Route::patch($uri, $callback);
5Route::delete($uri, $callback);
6Route::options($uri, $callback);
If you want a route to match multiple HTTP methods, you can use the match()
or any()
method instead. match()
requires you to specify an array of HTTP methods that you wish to match, and any()
simply matches all HTTP requests.
1Route::match(['get', 'post'], '/', function () {
2 . . .
3});
4
5Route::any('/', function () {
6 . . .
7});
Passing data to view
Now, look inside the get()
method.
1use Illuminate\Support\Facades\Route;
2
3Route::get('/', function () {
4 return view('welcome');
5});
The method accepts two parameters, the first one is the URL pattern this router is supposed to match, and the second one is a callback function that will be executed when the match is successful.
Inside the callback function, a built-in function view()
is returned. This function will look for the corresponding view file based on the given parameter.
Laravel offers a simple shortcut, Route::view()
, if you only need the route handler to return a view. This method allows you to avoid writing a full route.
1Route::view('/welcome', 'welcome');
The first argument is the URL, and the second parameter is the corresponding view. There is also an optional third argument, which allows us to pass some extra data to that view like this
1Route::view('/welcome', 'welcome', ['name' => 'Taylor']);
We'll talk about how to access the data when we get to the view layer.
From router to controller
It is also possible to make the router point to a controller, which then points to a view. A controller is essentially an expanded version of the callback function. We'll talk about controllers in detail later.
1Route::get('/user', [UserController::class, 'index']);
This line of code means if the router receives "/user
", Laravel will go to the UserController
, and invoke the index
method.
Route parameters
Sometimes you need to use a part of the URL as parameter. For example, imagine you have a blog application, and there is a user looking for a post with the slug this-is-a-post
, and he is trying to find that post by typing http://www.example.com/posts/this-is-a-post
in his browser.
To ensure that user finds the correct post, we need to take the segment after posts/
as a parameter and send it to the backend. After that, our controller can use that parameter to find the correct post and return it back to the user.
To do that, you need to have the following route handler:
1Route::get('post/{slug}', [PostController::class, 'show']);
This example asks the route handler to match the word after post/
as a parameter, and then assign it to the variable slug
.
Since we haven't talked about the controllers, you can replace the second parameter with a simple callback function, and test this code:
1Route::get('/post/{slug}', function ($slug) {
2 return $slug;
3});
Now, open your browser and go to http://127.0.0.1:8000/posts/this-is-a-slug.
It is also possible to match multiple parameters in a URL:
1Route::get('category/{category}/post/{slug}', [PostController::class, 'show']);
In this case, the segment after category/
will be assigned to the variable category
, and the segment after post/
will be assigned to slug
.
Sometimes you don't know if a parameter will be present in the URL, in this case, you can make that parameter optional by appending a question mark (?
).
1Route::get('post/{slug?}', [PostController::class, 'show']);
And finally, you can validate the parameters using regular expressions. For example, you can make sure the user ID is always a number.
1Route::get('user/{id}', [UserController::class, 'show'])->where('id', '[0-9]+');
Named routers
You may specify a name for a route by chaining the name
method onto the route definition.
1Route::get('user/profile', [UserController::class, 'show'])->name('profile');
This allows us to back trace this route in the template layer. For example, when we need to access this URL named profile
, all we need to do is invoke the function route('profile')
.
Group routers
When you are building a large website, it is very common for you to have a few dozen or even hundreds of routers. In this case, it would make more sense if you categorize then into different groups. For example, we can group them based on the middleware.
1Route::middleware(['auth'])->group(function () {
2 Route::get('/user/profile', [UserController::class, 'show']);
3 Route::get('/user/setting', [UserController::class, 'setting']);
4});
Now both these routers will be assigned the middleware auth
.
We can also assign prefixes to a group of routers like this:
1Route::prefix('admin')->group(function () {
2 Route::get('/users', [UserController::class, 'show']);
3 . . .
4});
All the routers defined in this group will have the prefix /admin/
.
Middleware
A middleware is something that can inspect and filter the incoming HTTP requests, before it hits your application. It is something that happens after the route handler has matched the URL, but the callback function hasn't been executed, it is something in the middle, hence the name middleware. A common example of middleware would be user authentication. If the user is authenticated, the route will take the user to the supposed destination, if not, it will take the user to the login page first.
For this tutorial, we are not writing any middleware. The only one we need to use is the built-in auth
middleware for authentication purposes, but we are still going to cover the basics of it. To create a middleware, run the following command:
1php artisan make:middleware EnsureTokenIsValid
This will create a new EnsureTokenIsValid
class under the app/Http/Middleware
directory.
1<?php
2
3namespace App\Http\Middleware;
4
5use Closure;
6
7class EnsureTokenIsValid
8{
9 /**
10 * Handle an incoming request.
11 *
12 * @param \Illuminate\Http\Request $request
13 * @param \Closure $next
14 * @return mixed
15 */
16 public function handle($request, Closure $next)
17 {
18 if ($request->input('token') !== 'my-secret-token') {
19 return redirect('home');
20 }
21
22 return $next($request);
23 }
24}
This middleware will get the token value from the request, and compare it with the secret token stored in your site, and if it matches, proceed to the next step, if not, redirect to the home page.
In order to use this middleware, we still need to register it with Laravel. Go to app/Http/Kernel.php
, and find the $routeMiddleware
property, list the middleware we just created.
1/**
2 * The application's route middleware.
3 *
4 * These middleware may be assigned to groups or used individually.
5 *
6 * @var array<string, class-string|string>
7**/
8 protected $routeMiddleware = [
9 'auth' => \App\Http\Middleware\Authenticate::class,
10 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
11 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
12 'can' => \Illuminate\Auth\Middleware\Authorize::class,
13 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
14 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
15 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
16 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
17 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
18 ];
And finally, this is how you can use a middleware on a route handler.
1Route::get('/profile', function () {. . .})->middleware('auth');
The MVC architecture
The MVC structure is a web design principle consists of a model (M), which is in charge of communicating with the database, a controller (C), which is the command central of our application, and a view (V), which is the frontend interface of the app.
Typically, in a web application, there will be a router, which we've discussed before. The router will point to a controller, the controller will look for the required data in the database through the model, and then put the retrieved data to the corresponding location in a view, and finally return that view back to the user.
The view layer
Let's start with views. Recall that in the previous section, we defined this route:
1Route::get('/', function () {
2 return view('welcome');
3});
This route points to a view welcome
, which is stored in the resources/views
directory, and it has an extension .blade.php
. This extension informs Laravel that we are using the Blade template system, which we'll talk about later.
To create another view, we simply create another file with the same extension inside the views
directory.
greetings.blade.php
1<html>
2 <body>
3 <h1>Hello!</h1>
4 </body>
5</html>
We also talked about that it is possible to pass data from the route to the view like this:
1Route::get('/', function () {
2 return view('greeting', ['name' => 'James']);
3});
In this case, we assigned the variable name
with the value 'James'
, and passed it to the greeting.blade.php
we just created. And in order to display this data in the view, we can use the double curly braces {{ }}
.
greetings.blade.php
1<html>
2 <body>
3 <h1>Hello, {{ $name }}</h1>
4 </body>
5</html>
For a large web application, it is very common for you to organize your view files in different folders. For example, your app could have an administration panel, and you probably want to store the corresponding views in a folder called admin
. When pointing to view files in a nested structure from the router, remember to use .
instead of /
.
1Route::get('/admin', function () {
2 return view('admin.home');
3});
This example points to views/admin/home.blade.php
.
Blade template syntax
By default, Laravel's views utilizes the Blade template. We know that views are just HTML documents, and the Blade template adds programming features like inheritance system, flow control, loops and other programming concepts. It allows us to create a more dynamic webpage while writing less code.
if
Statements
You can write if
statements using directives @if
, @elseif
, @else
, and @endif
. They work exactly the same as the if
statements you see in other programming languages.
1@if ($num == 1)
2<p>The number is one.</p>
3@elseif ($num == 2)
4<p>The number is two.</p>
5@else
6<p>The number is three.</p>
7@endif
Let's test this code with a route:
1Route::get('/number/{num}', function ($num) {
2 return view('view', ['num' => $num]);
3});
Here, we assume num
can only be 1
, 2
or 3
.
switch
Statements
1@switch($i)
2 @case(1)
3 First case. . .
4 @break
5
6 @case(2)
7 Second case. . .
8 @break
9
10 @default
11 Default case. . .
12@endswitch
Loops
The Blade template allows you to create a for
loop:
1@for ($i = 0; $i < 10; $i++)
2 The current value is {{ $i }}
3@endfor
Or a foreach
loop. For every iteration, $user
equals to the next element of $users
.
1@foreach ($users as $user)
2<p>This is user {{ $user->id }}</p>
3@endforeach
Or a while
loop:
1@while (true)
2<p>I'm looping forever.</p>
3@endwhile
They work exactly like their PHP counterparts. However, there is one thing special about these loops, and that is the $loop
variable.
For example, you have something that you only want displayed once, in the first iteration, this is what you can do:
1@foreach ($users as $user) @if ($loop->first)
2<p>This is the first iteration.</p>
3@endif
4
5<p>This is user {{ $user->id }}</p>
6@endforeach
This time, the paragraph element <p>This is the first iteration.</p>
will only be rendered once in the first iteration. There are other properties you can access, you can read about them here. Note that the $loop
variable can only be accessed inside a loop.
Conditional Class
We know that class is the most commonly used way to assign styles to different HTML elements, and by changing the class of an element, we can easily change the look of that element. Laravel offers us a way to dynamically assign classes based on variables.
1<span @class([
2 'p-4',
3 'font-bold' => $isActive,
4 'bg-red' => $hasError,
5])></span>
In this example, the presence of classes font-bold
and bg-red
will depend on the truthiness of variables $isActive
and $hasError
.
Building the layout
When building a web application, there are always some parts of the page that will appear on multiple pages. It would be a waste of time and resources if you write the same code over and over again, and it's not good for maintenance. The smart thing to do here is to separate the part that will appear multiple times and put it into another file, and we'll simply import it when we need it.
Laravel offers two different ways to accomplish this, template inheritance and components. For beginners, template inheritance is much easier to understand, but components do offer some extra features. Let's start with the template inheritance system.
Template Inheritance
As an example, here we are trying to define a home page, and this is our route.
1Route::get('/', function () {
2 return view('home');
3});
Typically, you should have a layout.blade.php
, which imports the necessary CSS and JavaScript files, as well as the navbar and the footer, since they will appear on every page. And then, in your home.blade.php
file, extend to layout.blade.php
.
layout.blade.php
1<html>
2 <head>
3 @yield('title')
4 </head>
5
6 <body>
7 <div class="container">@yield('content')</div>
8 </body>
9</html>
home.blade.php
1@extends('layout')
2
3@section('title')
4<title>Home Page</title>
5@endsection
6
7@section('content')
8<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. . .</p>
9@endsection
Notice that @section('title')
matches @yield('title')
, and @section('content')
matches @yield('content')
.
In this example, when the home page is being rendered, Laravel will first encounter the directive @extends('layout')
, and it will go to layout.blade.php
to locate @yield('title')
and replace it with the title
section, and then find the @yield('content')
directive and replace it with the content
section.
Sometimes you need to load another view from the current view. For example, we want to add a sidebar to our homepage. Since it is something that will be included in some pages but not the others, we don't want to put it in the layout.blade.php
. However, it will be very difficult to maintain if you create a sidebar for every page that requires it. In this scenario, we can create a sidebar.blade.php
file, and then use the @include
directive to import it into the home page.
1@extends('layout')
2
3@section('title')
4<title>Home Page</title>
5@endsection
6
7@section('content')
8<p>. . .</p>
9
10@include('sidebar')
11
12@endsection
Components system
As for the component system, again, we are building a home page, and we also need a layout, but this time we'll define it as a component. All you need to do is create a components
folder under resources/views/
, and then put the layout.blade.php
inside.
views/components/layout.blade.php
1<html>
2 <head>
3 . . .
4 </head>
5
6 <body>
7 <div class="container">{{ $slot }}</div>
8 </body>
9</html>
To use this component, you need to use the <x-layout>
tag. Always remember the x-
prefix.
1<x-layout>
2 <p>
3 Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum eu elit
4 semper ex varius vehicula.
5 </p>
6</x-layout>
The content inside this <x-layout>
element will automatically be assigned to the variable $slot
. However, what if we have multiple slots? Like our previous example, we have a title
and a content
section. To solve this problem, we can define a <x-slot>
element.
1<x-layout>
2 <x-slot name="title">
3 <title>Home Page</title>
4 </x-slot>
5
6 <p>
7 Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum eu elit
8 semper ex varius vehicula.
9 </p>
10</x-layout>
The content of this <x-slot name="title">
will be assigned to the variable $title
, and the rest will still be assigned to the $slot
variable.
1<html>
2 <head>
3 {{ $title }}
4 </head>
5
6 <body>
7 <div class="container">{{ $slot }}</div>
8 </body>
9</html>
Dealing with databases in Laravel
Before we can move on to the model and controller layers, we must discuss how to deal with databases in Laravel.
Since we used Laravel Sail to create this project, a MySQL database has been prepared for us. Make sure you have the following environmental variables in the .env
file:
1DB_CONNECTION=mysql
2DB_HOST=mysql
3DB_PORT=3306
4DB_DATABASE=curl_demo
5DB_USERNAME=sail
6DB_PASSWORD=password
Database migrations
Right now, this database is empty, and we need to add some database tables. These tables will have columns, and the columns will have names and some special requirements. The process of setting this up is called running migrations.
In Laravel, all the migration files will be stored in the database/migrations
directory. And we can create a new migration file with a simple command.
Important Note: If you used Sail to create this Laravel project, in order to run PHP inside the Docker container, please replace php
with ./vendor/bin/sail
.
1php artisan make:migration create_flights_table
We can apply the migration file like this:
1php artisan migrate
If you want to roll back the previous migration:
1php artisan migrate:rollback
Or reset the migrations completely:
1php artisan migrate:reset
Create table
However, before creating our own migration file, let's take a look at an example. Open the migration file database/migrations/2014_10_12_000000_create_users_table.php
, which comes with Laravel.
1<?php
2
3use Illuminate\Database\Migrations\Migration;
4use Illuminate\Database\Schema\Blueprint;
5use Illuminate\Support\Facades\Schema;
6
7return new class extends Migration
8{
9 /**
10 * Run the migrations.
11 */
12 public function up(): void
13 {
14 Schema::create('users', function (Blueprint $table) {
15 $table->id();
16 $table->string('name');
17 $table->string('email')->unique();
18 $table->timestamp('email_verified_at')->nullable();
19 $table->string('password');
20 $table->rememberToken();
21 $table->timestamps();
22 });
23 }
24
25 /**
26 * Reverse the migrations.
27 */
28 public function down(): void
29 {
30 Schema::dropIfExists('users');
31 }
32};
In this migration file, we have a class with two different methods. up()
is used to create new tables and columns, while down()
can be used to reverse the operations performed by up()
.
Inside the up()
method, Laravel uses a the create()
method to create a new table named users
. From line 15 to 21, each line creates a different column with a different name and type. For example, $table->string('name');
creates a column called name
inside the users
table and that column should store strings.
Here is a full list of column types available in Laravel. We are not going through all of them, but instead, we'll discuss why we choose the specific column type as we encounter specific problems in the future.
Notice line 17, $table->string('email')->unique();
, there is something else here, after we've declared the column name and type. The unique()
is called a column modifier, it adds extra constraints to that column, in this case, it makes sure that the column cannot contain repeating values, each email the user provides must be unique.
Here is a full list of column modifiers available in Laravel.
Apply changes to table
Besides creating tables, it is also possible for us to update a table using the table()
method:
1Schema::table('users', function (Blueprint $table) {
2 $table->integer('age');
3});
This code adds a new column age
to the existing table users
.
To rename a table:
1Schema::rename($from, $to);
Or drop a table completely:
1Schema::drop('users');
2
3Schema::dropIfExists('users');
Seeding
Seeder is used to generate dummy data for your database. To generate a seeder, run the following command:
1php artisan make:seeder UserSeeder
This command will generate a seeder for the user
table, which will be placed under the database/seeders
directory.
UserSeeder.php
1<?php
2
3namespace Database\Seeders;
4
5use Illuminate\Database\Seeder;
6use Illuminate\Support\Facades\DB;
7use Illuminate\Support\Facades\Hash;
8use Illuminate\Support\Str;
9
10class UserSeeder extends Seeder
11{
12 /**
13 * Run the database seeders.
14 */
15 public function run(): void
16 {
17 DB::table('users')->insert([
18 'name' => Str::random(10),
19 'email' => Str::random(10).'@gmail.com',
20 'password' => Hash::make('password'),
21 ]);
22 }
23}
To execute this seeder, run the following command:
1php artisan db:seed UserSeeder
Query builder
The query builder is an interface that allows us to interact with the database. For example, we can use the table()
method to retrieve all rows in a table.
1use Illuminate\Support\Facades\DB;
2
3$users = DB::table('users')->get();
We can add more constraints by chaining a where()
method.
1$users = DB::table('users')->where('age', '>=', 21)->get();
This code will retrieve all the rows whose age
is greater than or equal to 21.
There are many other methods besides where()
and get()
, we'll talk about them later when we encounter specific problems.
The model layer
Model is a very important concept in modern web development. It is in charge of interacting with the database. The model is part of Laravel's Eloquent ORM (Object-Relational Mapper) system. Think of it as a query builder, with some extra features. We can use the make:model
command to generate a new model.
1php artisan make:model Post
If you'd like to generate a corresponding migration file, use the --migration
option.
1php artisan make:model Post --migration
app/Models/Post.php
1<?php
2
3namespace App\Models;
4
5use Illuminate\Database\Eloquent\Factories\HasFactory;
6use Illuminate\Database\Eloquent\Model;
7
8class Post extends Model
9{
10 use HasFactory;
11 . . .
12}
By default, this model assumes that there is a posts
table in the database. If you wish to change that setting, set a $table
property like this:
1class Post extends Model
2{
3 /**
4 * The table associated with the model.
5 *
6 * @var string
7 */
8 protected $table = 'my_posts';
9}
Retrieving model
The eloquent model is a powerful query builder that allows us to communicate with the database. For example, we can use the all()
method to retrieve all the records in the post
table.
1use App\Models\Post;
2
3$posts = Post::all();
Notice that we imported the Post
model, instead of the DB
class. The Post
model will automatically connect with the posts
table.
Since models are query builders, that means all the query builder methods can be accessed here.
1$posts = Post::where('published', true)
2 ->orderBy('title')
3 ->take(10)
4 ->get();
When you are using all()
or get()
to retrieve data, the value that is returned is not a simple array or object, but instead, it is an instance of Illuminate\Database\Eloquent\Collection
. This Collection
provides a few more powerful methods than a simple object.
For example, the find()
method can be used to locate a record with a certain primary key (usually the id
).
1$posts = Post::all();
2
3$post = $posts->find(1);
In this example, $post
will be the post with id==1
.
Inserting & updating model
We can also insert or update records using the model. First, make sure the corresponding model has a $fillable
property, and ensure you list all the columns that should be fillable.
1class Post extends Model
2{
3 /**
4 * The attributes that are mass assignable.
5 *
6 * @var array
7 */
8 protected $fillable = ['title', 'content'];
9}
After that, you can use the create()
method to insert a new record.
1$flight = Post::create([
2 'title' => '. . .',
3 'content' => '. . .',
4]);
Or, you can update existing records with the update()
method.
1Flight::where('published', true)->update(['published' => false]);
Deleting model
There are two methods allowing you to delete records. If you wish to delete a single record, use the delete()
method.
1$posts = Post::all();
2
3$post = $posts->find(1);
4
5$post->delete();
If you want to mass delete records, use the truncate()
method.
1$posts = Post::where('published', true)
2 ->orderBy('title')
3 ->take(10)
4 ->get();
5
6$posts->truncate();
Database relations
That's all the basics we need to cover for models. However, we are not quite done yet. In a typical web application, database tables are usually not independent but correlated. For example, we could have a user who has many posts, and posts that belong to a category. And Laravel offers us a way to define these relations using the Eloquent models.
There are three fundamental relations for most web applications, one-to-one, one-to-many, and many-to-many.
One to one
This is the most basic relation. For example, each User
is associated with one Pet
. To define this relationship, we need to place a pet
method on the User
model.
1<?php
2
3namespace App\Models;
4
5use Illuminate\Database\Eloquent\Model;
6
7class User extends Model
8{
9 /**
10 * Get the pet associated with the user.
11 */
12 public function pet()
13 {
14 return $this->hasOne(Pet::class);
15 }
16}
Now, what if we want to do the opposite? The inverse of has one would be belongs to one, meaning each Pet
would belong to one User
. In order to define the inverse one-to-one relationship. We place a user
method on the Pet
model.
1class Pet extends Model
2{
3 /**
4 * Get the user that owns the pet.
5 */
6 public function user()
7 {
8 return $this->belongsTo(User::class);
9 }
10}
However, we are not quite done yet. We must also make some changes to the corresponding database tables. The User
model assumes that there is a pet_id
column inside the users
table, and it stores the id
of the pet that the user owns.
Also make adjustments to the corresponding pets
table. There should be a user_id
column storing the id
of the user that this pet belongs to.
One to many
A one-to-many relationship is used to define relationships where a single model owns multiple instances of another model. For example, one Category
could have many Post
s. Similar to the one-to-one relation, it can be defined by putting a posts
method in the Category
model.
1class Category extends Model
2{
3 public function posts()
4 {
5 return $this->hasMany(Post::class);
6 }
7}
However, sometimes we need to find the category through the post. The inverse of has many would be belongs to. In order to define the inverse one-to-many relation. you must place a category
method in the Post
model.
1class Post extends Model
2{
3 public function category()
4 {
5 return $this->belongsTo(Category::class);
6 }
7}
This relation assumes the posts
table contains a category_id
column, storing the id
of the category this post belongs to.
Many to many
The many-to-many relation is a bit more tricky. For example, we can have a User
who has many roles, and a Role
which has many users.
1class User extends Model
2{
3 /**
4 * The roles that belong to the user.
5 */
6 public function roles()
7 {
8 return $this->belongsToMany(Role::class);
9 }
10}
1class Role extends Model
2{
3 /**
4 * The users that belong to the role.
5 */
6 public function users()
7 {
8 return $this->belongsToMany(User::class);
9 }
10}
This relation assumes there is a role_user
pivot table in the database. And the role_user
table with user_id
and role_id
columns. This way, you can match the user with the role and vice versa.
To make things clearer, imagine we have a role_user
table like this:
user_id | role_id |
1 | 1 |
2 | 1 |
3 | 2 |
1 | 2 |
2 | 3 |
For the user with id=1
, there are two roles, each with id=1
and id=2
. If we want to do things backward and find users through a role, for the role with id=2
, there are two users, id=3
and id=1
.
The controller layer
Now that we've studied routes, views, models as well as database relations, it's time to talk about the thing that connects them all together. Remember when we talked about routes, we demonstrated an example like this?
1Route::get('/number/{num}', function ($num) {
2 return view('view', ['num' => $num]);
3});
It looks fine right now, but imagine you have a huge project, putting everything in the route file will make things very messy. A better solution would be making sure that the route always points to a controller method, and we'll put the logic inside that method.
1use App\Http\Controllers\UserController;
2
3Route::get('/users/{name}', [UserController::class, 'show']);
To create a new controller, run the following command:
1php artisan make:controller UserController
Laravel's controller files are stored under the directory app/Http/Controllers/
.
Basic controller
Let's take a look at an example. The show()
method expects a variable $id
, and it uses that id
to locate the user in the database and returns that user to the view.
1<?php
2
3namespace App\Http\Controllers;
4
5use App\Models\User;
6use Illuminate\View\View;
7
8class UserController extends Controller
9{
10 /**
11 * Show the profile for a given user.
12 */
13 public function show(string $name): View
14 {
15 return view('user.profile', [
16 'user' => User::firstWhere('name', $name);
17 ]);
18 }
19}
Sometimes, you need to define a controller that only has one method. In this case, you can simplify the code by changing the method's name to __invoke
:
1class UserController extends Controller
2{
3 public function __invoke(string $name): View
4 {
5 return view('user.profile', [
6 'user' => User::firstWhere('name', $name);
7 ]);
8 }
9}
Now you don't have to specify the method name in the route.
1Route::get('/users/{id}', UserController::class);
Resource controller
The resource is another modern web design concept we need to talk about. Usually, we see all the Eloquent models as resources, meaning that we perform the same set of actions on all of them, create, read, update and delete (CRUD).
To create a resource controller, we can use the --resource
flag:
1php artisan make:controller PostController --resource
The generated controller will contain the following methods:
1<?php
2
3namespace App\Http\Controllers;
4
5use Illuminate\Http\RedirectResponse;
6use Illuminate\Http\Request;
7use Illuminate\Http\Response;
8
9class PostController extends Controller
10{
11 /**
12 * Display a listing of the resource.
13 */
14 public function index(): Response
15 {
16 //
17 }
18
19 /**
20 * Show the form for creating a new resource.
21 */
22 public function create(): Response
23 {
24 //
25 }
26
27 /**
28 * Store a newly created resource in storage.
29 */
30 public function store(Request $request): RedirectResponse
31 {
32 //
33 }
34
35 /**
36 * Display the specified resource.
37 */
38 public function show(string $id): Response
39 {
40 //
41 }
42
43 /**
44 * Show the form for editing the specified resource.
45 */
46 public function edit(string $id): Response
47 {
48 //
49 }
50
51 /**
52 * Update the specified resource in storage.
53 */
54 public function update(Request $request, string $id): RedirectResponse
55 {
56 //
57 }
58
59 /**
60 * Remove the specified resource from storage.
61 */
62 public function destroy(string $id): RedirectResponse
63 {
64 //
65 }
66}
The comments explain their corresponding function and purposes.
Laravel also offers us a shortcut when registering routes for these methods, by using the resource()
method.
1Route::resource('posts', PostController::class);
The following routes with different HTTP methods will be automatically created.
HTTP Method | URI | Action | Route Name |
GET | /posts | index | posts.index |
GET | /posts/create | create | posts.create |
POST | /posts | store | posts.store |
GET | /posts/{post} | show | posts.show |
GET | /posts/{post}/edit | edit | posts.edit |
PUT/PATCH | /posts/{post} | update | posts.update |
DELETE | /posts/{post} | destroy | posts.destroy |
Designing database structure
Now, we are finally ready to put everything discussed in this tutorial into practice. Let's start with something easier. In this section, we are going to create a mini blog, and it only contains posts, without categories or tags. Each post has a title and a content.
This is not a fully featured blog application, but through this example, we are going to demonstrate exactly how to retrieve data from the database, how to create/update new information, how to save them in the database, as well as how to delete them.
First, let's deal with the database. We'll create a migration file for the post
table. This table should contain the title and the content. Generate a migration file with the following command:
1php artisan make:migration create_posts_table
1<?php
2
3use Illuminate\Database\Migrations\Migration;
4use Illuminate\Database\Schema\Blueprint;
5use Illuminate\Support\Facades\Schema;
6
7return new class extends Migration
8{
9 /**
10 * Run the migrations.
11 */
12 public function up(): void
13 {
14 Schema::create('posts', function (Blueprint $table) {
15 $table->id();
16 $table->timestamps();
17 $table->string('title');
18 $table->text('content');
19 });
20 }
21
22 /**
23 * Reverse the migrations.
24 */
25 public function down(): void
26 {
27 Schema::dropIfExists('posts');
28 }
29};
In this example, we created five columns in the post
table:
id()
creates anid
column, which is commonly used for indexing.timestamps()
creates two columns,created_at
anduptated_at
. These two columns will be automatically updated when the record is created or updated.string('title')
creates a columntitle
with typeVARCHAR
, whose default length is 255 bytes.string('content')
creates thecontent
column.
To apply the changes, run the following command:
1php artisan migrate
And a new posts
table should be generated:
Now, we can create the corresponding model for this table.
1php artisan make:model Post
app/Models/Post.php
1<?php
2
3namespace App\Models;
4
5use Illuminate\Database\Eloquent\Factories\HasFactory;
6use Illuminate\Database\Eloquent\Model;
7
8class Post extends Model
9{
10 use HasFactory;
11}
And generate the corresponding resource controller:
1php artisan make:controller PostController --resource
Finally, register this controller in the router:
1use App\Http\Controllers\PostController;
2
3Route::resource('posts', PostController::class);
You may check what routes are registered by running the following command:
1php artisan route:list
1GET|HEAD posts ................................................................. posts.index › PostController@index
2POST posts ................................................................. posts.store › PostController@store
3GET|HEAD posts/create ........................................................ posts.create › PostController@create
4GET|HEAD posts/{post} ............................................................ posts.show › PostController@show
5PUT|PATCH posts/{post} ........................................................ posts.update › PostController@update
6DELETE posts/{post} ...................................................... posts.destroy › PostController@destroy
7GET|HEAD posts/{post}/edit ....................................................... posts.edit › PostController@edit
The output contains information such as request methods, controller methods, as well as route names. These information are very important, and we are going to need them later in this tutorial.
The CRUD operations
Now it is time for us to delve into the application itself. When building real-life applications, it is unlikely that you can create all the controllers first, and then design the templates, and then move on to the routers. Instead, you need to think from the user's perspective, and think about what actions the user might take.
In general, the user should have the ability to perform four operations to each resource, which in our case, is the Post
.
- Create: The user should be able to create new resources and save them in the database.
- Read: The user should be able to read resources, both retrieving a list of resources, as well as checking the details of a specific resource.
- Update: The user should be able to update existing resources, and update the corresponding database record.
- Delete: The user should be able to remove a resource from the database.
Together, they are referred to as the CRUD operations.
The create action
Right now, our database is still empty, so the user might want to create new posts. SO let's start with the create action. To complete this create action, you need two things:
- A
create()
controller method, which displays a form, allowing the user to fill out the title and content. - A
store()
controller method, which saves the newly created post to the database, and redirect the user to the list page.
The create()
method matches the URL pattern /posts/create
(GET
method), and the store()
method matches the URL /post
(POST
method).
Let's start with the create()
method:
app/Http/Controllers/PostController.php
1<?php
2
3namespace App\Http\Controllers;
4
5use App\Models\Post;
6use Illuminate\Contracts\View\View;
7. . .
8
9class PostController extends Controller
10{
11 . . .
12
13 /**
14 * Show the form for creating a new resource.
15 */
16 public function create(): View
17 {
18 return view('posts.create');
19 }
20
21 . . .
22}
This method will be executed when you send a GET
request to /posts/create
, and it points to the views/posts/create.blade.php
template. Notice that on line 16, Response
has been changed to View
since this methods needs to return a view instead.
Next, we should create the corresponding view. We can start with the layout
:
views/layout.blade.php
1<!doctype html>
2<html lang="en">
3 <head>
4 <meta charset="UTF-8" />
5 <meta http-equiv="X-UA-Compatible" content="IE=edge" />
6 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7 <script src="https://cdn.tailwindcss.com"></script>
8 @yield('title')
9 </head>
10
11 <body class="container mx-auto font-serif">
12 <div class="bg-white text-black font-serif">
13 <div id="nav">
14 <nav class="flex flex-row justify-between h-16 items-center shadow-md">
15 <div class="px-5 text-2xl">
16 <a href="{{ route('posts.index') }}"> My Blog </a>
17 </div>
18 <div class="hidden lg:flex content-between space-x-10 px-10 text-lg">
19 <a
20 href="{{ route('posts.create') }}"
21 class="hover:underline hover:underline-offset-1"
22 >New Post</a
23 >
24 <a
25 href="https://github.com/thedevspacehq"
26 class="hover:underline hover:underline-offset-1"
27 >GitHub</a
28 >
29 </div>
30 </nav>
31 </div>
32
33 @yield('content')
34
35 <footer class="bg-gray-700 text-white">
36 <div
37 class="flex justify-center items-center sm:justify-between flex-wrap lg:max-w-screen-2xl mx-auto px-4 sm:px-8 py-10">
38 <p class="font-serif text-center mb-3 sm:mb-0">
39 Copyright ©
40 <a href="https://www.thedevspace.io/" class="hover:underline"
41 >Eric Hu</a
42 >
43 </p>
44
45 <div class="flex justify-center space-x-4">. . .</div>
46 </div>
47 </footer>
48 </div>
49 </body>
50</html>
Line 16 and 20, the double curly braces ({{ }}
) allows you to execute PHP code inside the template, and in the case, the route('posts.index')
method will return the route whose name is posts.index
. You may check the route names by referring to the output of the php artisan route:list
command.
Next, let's move on to the create
view. You need to be organized here. Since this create
view is for post related actions, you should created a post
directory to store this view.
views/posts/create.blade.php
1@extends('layout')
2
3@section('title')
4<title>Create</title>
5@endsection
6
7@section('content')
8<div class="w-96 mx-auto my-8">
9 <h2 class="text-2xl font-semibold underline mb-4">Create new post</h2>
10 <form action="{{ route('posts.store') }}" method="POST">
11 {{ csrf_field() }}
12 <label for="title">Title:</label><br />
13 <input
14 type="text"
15 id="title"
16 name="title"
17 class="p-2 w-full bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300" /><br />
18 <br />
19 <label for="content">Content:</label><br />
20 <textarea
21 type="text"
22 id="content"
23 name="content"
24 rows="15"
25 class="p-2 w-full bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300"></textarea
26 ><br />
27 <br />
28 <button
29 type="submit"
30 class="font-sans text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-md text-sm w-full px-5 py-2.5 text-center">
31 Submit
32 </button>
33 </form>
34</div>
35@endsection
There are a few things we need to notice when using forms to transfer data.
Line 10, the action
attribute defines what happens when this form is submitted, and in this case, it instructs the browser to visit the route posts.store
, with a POST
HTTP method.
Line 11, {{ csrf_field() }}
. CSRF is a malicious attack targeting web applications, and this csrf_field()
function provides protection against that type of attack. You can read more about CSRF (Cross-site request forgery) here.
Line 13 to 17, pay attention to the name
attribute. When the form is submitted, the user input will be tied to a variable, whose name is specified by the name
attribute. For instance, when name="title"
, the user input will be tied to the variable title
, and we can access its value using $request->input('title')
. We'll see exactly how this works later.
Line 20 to 26, the type
attribute must be set to submit
for this form to work.
Now start the dev server and go to http://127.0.0.1:8000/posts/create
.
When the submit button is clicked, the browser will send a POST
request to the server, and this time, the store()
method will be executed. This POST
request will contain user input, and they can be accessed like this:
PostController.php
1<?php
2
3namespace App\Http\Controllers;
4
5. . .
6
7class PostController extends Controller
8{
9 . . .
10
11 /**
12 * Store a newly created resource in storage.
13 */
14 public function store(Request $request): RedirectResponse
15 {
16 // Get the data from the request
17 $title = $request->input('title');
18 $content = $request->input('content');
19
20 // Create a new Post instance and put the requested data to the corresponding column
21 $post = new Post;
22 $post->title = $title;
23 $post->content = $content;
24
25 // Save the data
26 $post->save();
27
28 return redirect()->route('posts.index');
29 }
30
31 . . .
32}
After the data has been stored, you will be redirected to the posts.index
route.
Head back to your browser and type in a new title and content, and then click the submit button. The index
view hasn't been created yet, so an error message will be returned. However, if you check the database, a new record should be added.
The list action
Next, let's work on the read operation. There are, in fact, two different types of read operations. The first one is the list action, which returns a listing of all posts to the user. This action corresponds to the index()
method:
PostController.php
1<?php
2
3namespace App\Http\Controllers;
4
5use App\Models\Post;
6use Illuminate\Contracts\View\View;
7use Illuminate\Http\RedirectResponse;
8use Illuminate\Http\Request;
9use Illuminate\Http\Response;
10
11class PostController extends Controller
12{
13 /**
14 * Display a listing of the resource.
15 */
16 public function index(): View
17 {
18 $posts = Post::all();
19
20 return view('posts.index', [
21 'posts' => $posts,
22 ]);
23 }
24
25 . . .
26}
27
And here is the corresponding index
view:
views/post/index.blade.php
1@extends('layout')
2
3@section('title')
4<title>Page Title</title>
5@endsection
6
7@section('content')
8<div class="max-w-screen-lg mx-auto my-8">
9 @foreach($posts as $post)
10 <h2 class="text-2xl font-semibold underline mb-2">
11 <a href="{{ route('posts.show', ['post' => $post->id]) }}"
12 >{{ $post->title }}</a
13 >
14 </h2>
15 <p class="mb-4">{{ Illuminate\Support\Str::words($post->content, 100) }}</p>
16 @endforeach
17</div>
18@endsection
Line 9, foreach
iterates over all retrieved $posts
, and assign each value to the variable $post
.
Line 11, notice how the post id
is passed to the posts.show
route. The route will then pass this variable to the show()
controller method, which you'll see later.
Line 15, the Str::words()
method is a PHP helper, and it will only take the first 100 words of the content
.
The show action
The second read operation is the show action, which displays the details of a specific resource. This action uses the show()
method.
PostController.php
1<?php
2
3namespace App\Http\Controllers;
4
5use App\Models\Post;
6use Illuminate\Contracts\View\View;
7use Illuminate\Http\RedirectResponse;
8use Illuminate\Http\Request;
9use Illuminate\Http\Response;
10
11class PostController extends Controller
12{
13 . . .
14
15 /**
16 * Display the specified resource.
17 */
18 public function show(string $id): View
19 {
20 $post = Post::all()->find($id);
21
22 return view('posts.show', [
23 'post' => $post,
24 ]);
25 }
26
27 . . .
28}
29
views/post/show.blade.php
1@extends('layout')
2
3@section('title')
4<title>{{ $post->title }}</title>
5@endsection
6
7@section('content')
8<div class="max-w-screen-lg mx-auto my-8">
9 <h2 class="text-2xl font-semibold underline mb-2">{{ $post->title }}</h2>
10 <p class="mb-4">{{ $post->content }}</p>
11
12 <div class="grid grid-cols-2 gap-x-2">
13 <a
14 href="{{ route('posts.edit', ['post' => $post->id]) }}"
15 class="font-sans text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-md text-sm w-full px-5 py-2.5 text-center"
16 >Update</a
17 >
18 <form
19 action="{{ route('posts.destroy', ['post' => $post->id]) }}"
20 method="POST">
21 {{ csrf_field() }} {{ method_field('DELETE') }}
22 <button
23 type="submit"
24 class="font-sans text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-md text-sm w-full px-5 py-2.5 text-center">
25 Delete
26 </button>
27 </form>
28 </div>
29</div>
30@endsection
There are a few things we must note here. First, notice how the Update button is a simple link, but the Delete button is a form. This is because a link sends a GET request to the server, but we need something else for the delete action.
Next, if you look inside the form, you will find that this form has method="POST"
, but we need a DELETE method for the delete action. This is because by default, HTML only supports GET and POST methods, and if you need something else, you must set method="POST"
, and then specify the desired HTTP method using method_field()
instead.
The update action
Next, for the update operation, we have the edit()
method that displays an HTML form, and the update()
method that make changes to the database.
PostController.php
1<?php
2
3namespace App\Http\Controllers;
4
5use App\Models\Post;
6use Illuminate\Contracts\View\View;
7use Illuminate\Http\RedirectResponse;
8use Illuminate\Http\Request;
9use Illuminate\Http\Response;
10
11class PostController extends Controller
12{
13 . . .
14
15 /**
16 * Show the form for editing the specified resource.
17 */
18 public function edit(string $id): View
19 {
20 $post = Post::all()->find($id);
21
22 return view('posts.edit', [
23 'post' => $post,
24 ]);
25 }
26
27 /**
28 * Update the specified resource in storage.
29 */
30 public function update(Request $request, string $id): RedirectResponse
31 {
32 // Get the data from the request
33 $title = $request->input('title');
34 $content = $request->input('content');
35
36 // Find the requested post and put the requested data to the corresponding column
37 $post = Post::all()->find($id);
38 $post->title = $title;
39 $post->content = $content;
40
41 // Save the data
42 $post->save();
43
44 return redirect()->route('posts.show', ['post' => $id]);
45 }
46
47 . . .
48}
49
views/post/edit.blade.php
1@extends('layout')
2
3@section('title')
4<title>Edit</title>
5@endsection
6
7@section('content')
8<div class="w-96 mx-auto my-8">
9 <h2 class="text-2xl font-semibold underline mb-4">Edit post</h2>
10 <form
11 action="{{ route('posts.update', ['post' => $post->id]) }}"
12 method="POST">
13 {{ csrf_field() }} {{ method_field('PUT') }}
14 <label for="title">Title:</label><br />
15 <input
16 type="text"
17 id="title"
18 name="title"
19 value="{{ $post->title }}"
20 class="p-2 w-full bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300" /><br />
21 <br />
22 <label for="content">Content:</label><br />
23 <textarea
24 type="text"
25 id="content"
26 name="content"
27 rows="15"
28 class="p-2 w-full bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300">
29{{ $post->content }}</textarea
30 ><br />
31 <br />
32 <button
33 type="submit"
34 class="font-sans text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-md text-sm w-full px-5 py-2.5 text-center">
35 Submit
36 </button>
37 </form>
38</div>
39@endsection
The delete action
And finally, the destroy()
method:
PostController.php
1<?php
2
3namespace App\Http\Controllers;
4
5use App\Models\Post;
6use Illuminate\Contracts\View\View;
7use Illuminate\Http\RedirectResponse;
8use Illuminate\Http\Request;
9use Illuminate\Http\Response;
10
11class PostController extends Controller
12{
13 . . .
14
15 /**
16 * Remove the specified resource from storage.
17 */
18 public function destroy(string $id): RedirectResponse
19 {
20 $post = Post::all()->find($id);
21
22 $post->delete();
23
24 return redirect()->route('posts.index');
25 }
26}
27
This action does not require a view, since it just redirects you to posts.index
after the action is completed.
Initialize a new Laravel project
Congratulations, you officially completed your first Laravel project! So now, are you ready for yhe next challenge?
In this section, let's create a fully featured blog application, with posts, categories, as well as tags. Previously, we discussed the CRUD operations for posts, and now, 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:
1curl -s https://laravel.build/<app_name> | bash
Change into the app directory and start the server.
1cd <app_name>
1./vendor/bin/sail up
To make things easier, let's create an alias for sail
. Run the following command:
1alias sail='[ -f sail ] && sh sail || sh vendor/bin/sail'
From now on, you can run sail directly without specifying the entire path.
1sail up
User authentication with Laravel Breeze
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 installed and configured with minimum effort.
Use the following commands to install Laravel Breeze:
1sail composer require laravel/breeze --dev
1sail artisan breeze:install
1sail artisan migrate
1sail npm install
1sail npm run dev
This process will automatically generate the required controllers, middlewares 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 tutorial, 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.
Set up the database
Next, we need to have a grand picture on how our blog app looks like. 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 pivot table for the many-to-many relation between post and tag:
Post/tag
key | type |
post_id | bigInteger |
tag_id | bigInteger |
Implement database structure
To implement this design, generate models and migration files using the following commands:
1sail artisan make:model Post --migration
1sail artisan make:model Category --migration
1sail artisan make:model Tag --migration
And a separate migration file for the post_tag
table:
1sail artisan make:migration create_post_tag_table
database/migrations/create_posts_table.php
1<?php
2
3use Illuminate\Database\Migrations\Migration;
4use Illuminate\Database\Schema\Blueprint;
5use Illuminate\Support\Facades\Schema;
6
7return new class extends Migration
8{
9 /**
10 * Run the migrations.
11 */
12 public function up(): void
13 {
14 Schema::create('posts', function (Blueprint $table) {
15 $table->id();
16 $table->timestamps();
17 $table->string('title');
18 $table->string('cover');
19 $table->text('content');
20 $table->boolean('is_published');
21
22 $table->bigInteger('user_id');
23 $table->bigInteger('category_id');
24 });
25 }
26
27 /**
28 * Reverse the migrations.
29 */
30 public function down(): void
31 {
32 Schema::dropIfExists('posts');
33 }
34};
database/migrations/create_categories_table.php
1<?php
2
3use Illuminate\Database\Migrations\Migration;
4use Illuminate\Database\Schema\Blueprint;
5use Illuminate\Support\Facades\Schema;
6
7return new class extends Migration
8{
9 /**
10 * Run the migrations.
11 */
12 public function up(): void
13 {
14 Schema::create('categories', function (Blueprint $table) {
15 $table->id();
16 $table->timestamps();
17 $table->string('name');
18 });
19 }
20
21 /**
22 * Reverse the migrations.
23 */
24 public function down(): void
25 {
26 Schema::dropIfExists('categories');
27 }
28};
database/migrations/create_tags_table.php
1<?php
2
3use Illuminate\Database\Migrations\Migration;
4use Illuminate\Database\Schema\Blueprint;
5use Illuminate\Support\Facades\Schema;
6
7return new class extends Migration
8{
9 /**
10 * Run the migrations.
11 */
12 public function up(): void
13 {
14 Schema::create('tags', function (Blueprint $table) {
15 $table->id();
16 $table->timestamps();
17 $table->string('name');
18 });
19 }
20
21 /**
22 * Reverse the migrations.
23 */
24 public function down(): void
25 {
26 Schema::dropIfExists('tags');
27 }
28};
database/migrations/create_post_tag_table.php
1<?php
2
3use Illuminate\Database\Migrations\Migration;
4use Illuminate\Database\Schema\Blueprint;
5use Illuminate\Support\Facades\Schema;
6
7return new class extends Migration
8{
9 /**
10 * Run the migrations.
11 */
12 public function up(): void
13 {
14 Schema::create('post_tag', function (Blueprint $table) {
15 $table->id();
16 $table->timestamps();
17 $table->bigInteger('post_id');
18 $table->bigInteger('tag_id');
19 });
20 }
21
22 /**
23 * Reverse the migrations.
24 */
25 public function down(): void
26 {
27 Schema::dropIfExists('post_tag');
28 }
29};
Apply these changes with the following command:
1sail 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 previously. And we also need to define relations between these tables.
app/Models/Post.php
1<?php
2
3namespace App\Models;
4
5use Illuminate\Database\Eloquent\Factories\HasFactory;
6use Illuminate\Database\Eloquent\Model;
7use Illuminate\Database\Eloquent\Relations\BelongsTo;
8use Illuminate\Database\Eloquent\Relations\BelongsToMany;
9
10class Post extends Model
11{
12 use HasFactory;
13
14 protected $fillable = [
15 "title",
16 'content',
17 'cover',
18 'is_published'
19 ];
20
21 public function user(): BelongsTo
22 {
23 return $this->belongsTo(User::class);
24 }
25
26 public function category(): BelongsTo
27 {
28 return $this->belongsTo(Category::class);
29 }
30
31 public function tags(): BelongsToMany
32 {
33 return $this->belongsToMany(Tag::class);
34 }
35}
36
app/Models/Category.php
1<?php
2
3namespace App\Models;
4
5use Illuminate\Database\Eloquent\Factories\HasFactory;
6use Illuminate\Database\Eloquent\Model;
7use Illuminate\Database\Eloquent\Relations\HasMany;
8
9class Category extends Model
10{
11 use HasFactory;
12
13 protected $fillable = [
14 'name',
15 ];
16
17 public function posts(): HasMany
18 {
19 return $this->hasMany(Post::class);
20 }
21}
22
app/Models/Tag.php
1<?php
2
3namespace App\Models;
4
5use Illuminate\Database\Eloquent\Factories\HasFactory;
6use Illuminate\Database\Eloquent\Model;
7use Illuminate\Database\Eloquent\Relations\BelongsToMany;
8
9class Tag extends Model
10{
11 use HasFactory;
12
13 protected $fillable = [
14 'name',
15 ];
16
17 public function posts(): BelongsToMany
18 {
19 return $this->belongsToMany(Post::class);
20 }
21}
22
app/Models/User.php
1<?php
2
3namespace App\Models;
4
5// use Illuminate\Contracts\Auth\MustVerifyEmail;
6use Illuminate\Database\Eloquent\Factories\HasFactory;
7use Illuminate\Database\Eloquent\Relations\HasMany;
8use Illuminate\Foundation\Auth\User as Authenticatable;
9use Illuminate\Notifications\Notifiable;
10use Laravel\Sanctum\HasApiTokens;
11
12class User extends Authenticatable
13{
14 use HasApiTokens, HasFactory, Notifiable;
15
16 /**
17 * The attributes that are mass assignable.
18 *
19 * @var array<int, string>
20 */
21 protected $fillable = [
22 'name',
23 'email',
24 'password',
25 ];
26
27 /**
28 * The attributes that should be hidden for serialization.
29 *
30 * @var array<int, string>
31 */
32 protected $hidden = [
33 'password',
34 'remember_token',
35 ];
36
37 /**
38 * The attributes that should be cast.
39 *
40 * @var array<string, string>
41 */
42 protected $casts = [
43 'email_verified_at' => 'datetime',
44 ];
45
46 public function posts(): HasMany
47 {
48 return $this->hasMany(Post::class);
49 }
50}
51
Controllers and routes
As for the controllers, we need one resource controller for each resources (post, category and tag).
1php artisan make:controller PostController --resource
1php artisan make:controller CategoryController --resource
1php artisan make:controller TagController --resource
Then create routes for each of these controllers:
routes/web.php
1<?php
2
3use App\Http\Controllers\CategoryController;
4use App\Http\Controllers\PostController;
5use App\Http\Controllers\ProfileController;
6use App\Http\Controllers\TagController;
7use Illuminate\Support\Facades\Route;
8
9// Dashboard routes
10Route::prefix('dashboard')->group(function () {
11
12 // Dashboard homepage
13 Route::get('/', function () {
14 return view('dashboard');
15 })->name('dashboard');
16
17 // Dashboard category resource
18 Route::resource('categories', CategoryController::class);
19
20 // Dashboard tag resource
21 Route::resource('tags', TagController::class);
22
23 // Dashboard post resource
24 Route::resource('posts', PostController::class);
25
26})->middleware(['auth', 'verified']);
27
28Route::middleware('auth')->group(function () {
29 Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
30 Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
31 Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
32});
33
34require __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.
Category and tag controllers
The CategoryController
and the TagController
are fairly straightforward. You can set them up the same way we created the PostController
previously.
app/Http/Controllers/CategoryController.php
1<?php
2
3namespace App\Http\Controllers;
4
5use App\Models\Category;
6use Illuminate\Contracts\View\View;
7use Illuminate\Http\RedirectResponse;
8use Illuminate\Http\Request;
9use Illuminate\Http\Response;
10
11class CategoryController extends Controller
12{
13 /**
14 * Display a listing of the resource.
15 */
16 public function index(): View
17 {
18 $categories = Category::all();
19
20 return view('categories.index', [
21 'categories' => $categories
22 ]);
23 }
24
25 /**
26 * Show the form for creating a new resource.
27 */
28 public function create(): View
29 {
30 return view('categories.create');
31 }
32
33 /**
34 * Store a newly created resource in storage.
35 */
36 public function store(Request $request): RedirectResponse
37 {
38 // Get the data from the request
39 $name = $request->input('name');
40
41 // Create a new Post instance and put the requested data to the corresponding column
42 $category = new Category();
43 $category->name = $name;
44
45 // Save the data
46 $category->save();
47
48 return redirect()->route('categories.index');
49 }
50
51 /**
52 * Display the specified resource.
53 */
54 public function show(string $id): View
55 {
56 $category = Category::all()->find($id);
57 $posts = $category->posts();
58
59 return view('categories.show', [
60 'category' => $category,
61 'posts' => $posts
62 ]);
63 }
64
65 /**
66 * Show the form for editing the specified resource.
67 */
68 public function edit(string $id): View
69 {
70 $category = Category::all()->find($id);
71
72 return view('categories.edit', [
73 'category' => $category
74 ]);
75 }
76
77 /**
78 * Update the specified resource in storage.
79 */
80 public function update(Request $request, string $id): RedirectResponse
81 {
82 // Get the data from the request
83 $name = $request->input('name');
84
85 // Find the requested category and put the requested data to the corresponding column
86 $category = Category::all()->find($id);
87 $category->name = $name;
88
89 // Save the data
90 $category->save();
91
92 return redirect()->route('categories.index');
93 }
94
95 /**
96 * Remove the specified resource from storage.
97 */
98 public function destroy(string $id): RedirectResponse
99 {
100 $category = Category::all()->find($id);
101
102 $category->delete();
103
104 return redirect()->route('categories.index');
105 }
106}
app/Http/Controllers/TagController.php
1<?php
2
3namespace App\Http\Controllers;
4
5use App\Models\Tag;
6use Illuminate\Contracts\View\View;
7use Illuminate\Http\RedirectResponse;
8use Illuminate\Http\Request;
9use Illuminate\Http\Response;
10
11class TagController extends Controller
12{
13 /**
14 * Display a listing of the resource.
15 */
16 public function index(): View
17 {
18 $tags = Tag::all();
19
20 return view('tags.index', [
21 'tags' => $tags
22 ]);
23 }
24
25 /**
26 * Show the form for creating a new resource.
27 */
28 public function create(): View
29 {
30 return view('tags.create');
31 }
32
33 /**
34 * Store a newly created resource in storage.
35 */
36 public function store(Request $request): RedirectResponse
37 {
38 // Get the data from the request
39 $name = $request->input('name');
40
41 // Create a new Post instance and put the requested data to the corresponding column
42 $tag = new Tag();
43 $tag->name = $name;
44
45 // Save the data
46 $tag->save();
47
48 return redirect()->route('tags.index');
49 }
50
51 /**
52 * Display the specified resource.
53 */
54 public function show(string $id): View
55 {
56 $tag = Tag::all()->find($id);
57 $posts = $tag->posts();
58
59 return view('tags.show', [
60 'tag' => $tag,
61 'posts' => $posts
62 ]);
63 }
64
65 /**
66 * Show the form for editing the specified resource.
67 */
68 public function edit(string $id): View
69 {
70 $tag = Tag::all()->find($id);
71
72 return view('tags.edit', [
73 'tag' => $tag
74 ]);
75 }
76
77 /**
78 * Update the specified resource in storage.
79 */
80 public function update(Request $request, string $id): RedirectResponse
81 {
82 // Get the data from the request
83 $name = $request->input('name');
84
85 // Find the requested category and put the requested data to the corresponding column
86 $tag = Tag::all()->find($id);
87 $tag->name = $name;
88
89 // Save the data
90 $tag->save();
91
92 return redirect()->route('tags.index');
93 }
94
95 /**
96 * Remove the specified resource from storage.
97 */
98 public function destroy(string $id): RedirectResponse
99 {
100 $tag = Tag::all()->find($id);
101
102 $tag->delete();
103
104 return redirect()->route('tags.index');
105 }
106}
Remember that you can check the name of the routes using the following command:
1sail artisan route:list
Post controller
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:
app/Http/Controllers/PostController.php
1<?php
2
3namespace App\Http\Controllers;
4
5use App\Models\Category;
6use App\Models\Post;
7use App\Models\Tag;
8use Illuminate\Contracts\View\View;
9use Illuminate\Http\RedirectResponse;
10use Illuminate\Http\Request;
11use Illuminate\Http\Response;
12use Illuminate\Support\Facades\Auth;
13use Illuminate\Database\Eloquent\Builder;
14
15class PostController extends Controller
16{
17 . . .
18
19 /**
20 * Store a newly created resource in storage.
21 */
22 public function store(Request $request): RedirectResponse
23 {
24 // Get the data from the request
25 $title = $request->input('title');
26 $content = $request->input('content');
27
28 if ($request->input('is_published') == 'on') {
29 $is_published = true;
30 } else {
31 $is_published = false;
32 }
33
34 // Create a new Post instance and put the requested data to the corresponding column
35 $post = new Post();
36 $post->title = $title;
37 $post->content = $content;
38 $post->is_published = $is_published;
39
40 // Save the cover image
41 $path = $request->file('cover')->store('cover', 'public');
42 $post->cover = $path;
43
44 // Set user
45 $user = Auth::user();
46 $post->user()->associate($user);
47
48 // Set category
49 $category = Category::find($request->input('category'));
50 $post->category()->associate($category);
51
52 // Save post
53 $post->save();
54
55 //Set tags
56 $tags = $request->input('tags');
57
58 foreach ($tags as $tag) {
59 $post->tags()->attach($tag);
60 }
61
62 return redirect()->route('posts.index');
63 }
64
65 . . .
66}
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 value is 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 the 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.
1$post->tags()->detach();
Views
When building a view system, always remember to be organized. This is the structure we are going with:
1resources/views
2├── auth
3├── categories
4│ ├── create.blade.php
5│ ├── edit.blade.php
6│ ├── index.blade.php
7│ └── show.blade.php
8├── components
9├── layouts
10├── posts
11│ ├── create.blade.php
12│ ├── edit.blade.php
13│ ├── index.blade.php
14│ └── show.blade.php
15├── profile
16├── tags
17│ ├── create.blade.php
18│ ├── edit.blade.php
19│ ├── index.blade.php
20│ └── show.blade.php
21├── dashboard.blade.php
22└── welcome.blade.php
We'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, we are 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.
Post create view
resources/views/posts/create.blade.php
1<x-app-layout>
2 <x-slot name="header">
3 <div class="flex justify-between">
4 <h2
5 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
6 {{ __('Posts') }}
7 </h2>
8 <a href="{{ route('posts.create') }}">
9 <x-primary-button>{{ __('New') }}</x-primary-button>
10 </a>
11 </div>
12
13 <script
14 src="https://cdn.tiny.cloud/. . ./tinymce.min.js"
15 referrerpolicy="origin"></script>
16 </x-slot>
17
18 <div class="py-12">
19 <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
20 <div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
21 <div class="">
22 <form
23 action="{{ route('posts.store') }}"
24 method="POST"
25 class="mt-6 space-y-3"
26 enctype="multipart/form-data">
27 {{ csrf_field() }}
28 <input type="checkbox" name="is_published" id="is_published" />
29 <x-input-label for="is_published"
30 >Make this post public</x-input-label
31 >
32 <br />
33 <x-input-label for="title">{{ __('Title') }}</x-input-label>
34 <x-text-input
35 id="title"
36 name="title"
37 type="text"
38 class="mt-1 block w-full"
39 required
40 autofocus
41 autocomplete="name" />
42 <br />
43 <x-input-label for="content">{{ __('Content') }}</x-input-label>
44 <textarea
45 name="content"
46 id="content"
47 cols="30"
48 rows="30"></textarea>
49 <br />
50 <x-input-label for="cover">{{ __('Cover Image') }}</x-input-label>
51 <x-text-input
52 id="cover"
53 name="cover"
54 type="file"
55 class="mt-1 block w-full"
56 required
57 autofocus
58 autocomplete="cover" />
59 <br />
60 <x-input-label for="category">{{ __('Category') }}</x-input-label>
61 <select id="category" name="category">
62 @foreach($categories as $category)
63 <option value="{{ $category->id }}">{{ $category->name }}</option>
64 @endforeach
65 </select>
66 <br />
67 <x-input-label for="tags">{{ __('Tags') }}</x-input-label>
68 <select id="tags" name="tags[]" multiple>
69 @foreach($tags as $tag)
70 <option value="{{ $tag->id }}">{{ $tag->name }}</option>
71 @endforeach
72 </select>
73 <br />
74 <x-primary-button>{{ __('Save') }}</x-primary-button>
75 </form>
76 <script>
77 tinymce.init({. . .});
78 </script>
79 </div>
80 </div>
81 </div>
82 </div>
83</x-app-layout>
We are using TinyMCE as the rich text editor, you can replace it with something else, or simply use a <textarea></textarea>
if you wish.
Line 22 to 26, this form must have enctype="multipart/form-data"
since we are not just transferring files as well.
Line 54, remember to use type="file"
here since we are uploading an image.
Line 61 to 65, the value of the option will be transferred to the backend.
Line 68 to 72, 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.
Post edit view
resources/views/posts/edit.blade.php
1<x-app-layout>
2 <x-slot name="header">
3 <div class="flex justify-between">
4 <h2
5 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
6 {{ __('Posts') }}
7 </h2>
8 <a href="{{ route('posts.create') }}">
9 <x-primary-button>{{ __('New') }}</x-primary-button>
10 </a>
11 </div>
12
13 <script
14 src="https://cdn.tiny.cloud/. . ./tinymce.min.js"
15 referrerpolicy="origin"></script>
16 </x-slot>
17
18 <div class="py-12">
19 <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
20 <div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
21 <div class="">
22 <form
23 action="{{ route('posts.update', ['post' => $post->id]) }}"
24 method="POST"
25 class="mt-6 space-y-3"
26 enctype="multipart/form-data">
27 {{ csrf_field() }} {{ method_field('PUT') }}
28 <input
29 type="checkbox"
30 name="is_published"
31 id="is_published"
32 @checked($post- />is_published)/>
33 <x-input-label for="is_published"
34 >Make this post public</x-input-label
35 >
36 <br />
37 <x-input-label for="title">{{ __('Title') }}</x-input-label>
38 <x-text-input
39 id="title"
40 name="title"
41 type="text"
42 class="mt-1 block w-full"
43 required
44 autofocus
45 autocomplete="name"
46 value="{{ $post->title }}" />
47 <br />
48 <x-input-label for="content">{{ __('Content') }}</x-input-label>
49 <textarea name="content" id="content" cols="30" rows="30">
50{{ $post->content }}</textarea
51 >
52 <br />
53 <x-input-label for="cover"
54 >{{ __('Update Cover Image') }}</x-input-label
55 >
56 <img
57 src="{{ Illuminate\Support\Facades\Storage::url($post->cover) }}"
58 alt="cover image"
59 width="200" />
60 <x-text-input
61 id="cover"
62 name="cover"
63 type="file"
64 class="mt-1 block w-full"
65 autofocus
66 autocomplete="cover" />
67 <br />
68 <x-input-label for="category">{{ __('Category') }}</x-input-label>
69 <select id="category" name="category">
70 @foreach($categories as $category)
71 <option value="{{ $category->id }}" @selected($post->
72 category->id == $category->id)>{{ $category->name }}
73 </option>
74 @endforeach
75 </select>
76 <br />
77 <x-input-label for="tags">{{ __('Tags') }}</x-input-label>
78 <select id="tags" name="tags[]" multiple>
79 @foreach($tags as $tag)
80 <option value="{{ $tag->id }}" @selected($post->
81 tags->contains($tag))>{{ $tag->name }}
82 </option>
83 @endforeach
84 </select>
85 <br />
86 <x-primary-button>{{ __('Save') }}</x-primary-button>
87 </form>
88 <script>
89 tinymce.init({. . .});
90 </script>
91 </div>
92 </div>
93 </div>
94 </div>
95</x-app-layout>
Line 27, by default, HTML doesn't support PUT method, so what we can do is set method="POST"
, and then tell Laravel to use PUT method with {{ method_field('PUT') }}
.
Post index view
resources/views/posts/index.blade.php
1<x-app-layout>
2 <x-slot name="header">
3 <div class="flex justify-between">
4 <h2
5 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
6 {{ __('Posts') }}
7 </h2>
8 <a href="{{ route('posts.create') }}">
9 <x-primary-button>{{ __('New') }}</x-primary-button>
10 </a>
11 </div>
12 </x-slot>
13
14 <div class="py-12">
15 <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
16 @foreach($posts as $post)
17 <div
18 class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg mb-4 px-4 h-20 flex justify-between items-center">
19 <div class="text-gray-900 dark:text-gray-100">
20 <p>{{ $post->title }}</p>
21 </div>
22 <div class="space-x-2">
23 <a href="{{ route('posts.edit', ['post' => $post->id]) }}">
24 <x-primary-button>{{ __('Edit') }}</x-primary-button></a
25 >
26 <form
27 method="post"
28 action="{{ route('posts.destroy', ['post' => $post->id]) }}"
29 class="inline">
30 {{ csrf_field() }} {{ method_field('DELETE') }}
31 <x-danger-button> {{ __('Delete') }} </x-danger-button>
32 </form>
33 </div>
34 </div>
35 @endforeach
36 </div>
37 </div>
38</x-app-layout>
Notice the delete button, instead of a regular button, it must be a form with DELETE method, since a regular link sends a GET method.
With this demonstration, you should be able to build the rest of the view system with ease.
Screenshots
Last but not least, here are some screenshots for the dashboard we've created.
Create the routes
Lastly, we are going to create the frontend part of the app. First of all, let's make a plan. For this blog application, we are going to have:
- A homepage displaying a list of all recent posts.
- A category page displaying a list of posts under a specific category.
- A tag page displaying a list of posts with a specific tag.
- A post page displaying the content of a specific post.
- A search page displaying a list of posts based on a search query.
All of these pages will have a sidebar with a search box, a list of categories and tags. The post page will also have a related posts section at the bottom.
We've dealt with the database and the models in the previous section, so here we'll start with the routes.
routes/web.php
1. . .
2
3// Homepage
4Route::get('/', [PostController::class, 'home'])->name('home');
5
6// A list of posts under this category
7Route::get('/category/{category}', [CategoryController::class, 'category'])->name('category');
8
9// A list of posts with this tag
10Route::get('/tag/{tag}', [TagController::class, 'tag'])->name('tag');
11
12// Display a single post
13Route::get('/post/{post}', [PostController::class, 'post'])->name('post');
14
15// A list of posts based on search query
16Route::post('/search', [PostController::class, 'search'])->name('search');
17
18. . .
The homepage
Each of these routes has a corresponding controller method. We'll start with the home()
controller:
app/Http/Controllers/PostController.php
1<?php
2
3namespace App\Http\Controllers;
4
5use App\Models\Category;
6use App\Models\Post;
7use App\Models\Tag;
8use Illuminate\Contracts\View\View;
9use Illuminate\Http\RedirectResponse;
10use Illuminate\Http\Request;
11use Illuminate\Http\Response;
12use Illuminate\Support\Facades\Auth;
13use Illuminate\Database\Eloquent\Builder;
14
15class PostController extends Controller
16{
17 /**
18 * Display the home page
19 */
20 public function home(): View
21 {
22 $posts = Post::where('is_published', true)->paginate(env('PAGINATE_NUM'));
23 $categories = Category::all();
24 $tags = Tag::all();
25
26 return view('home', [
27 'posts' => $posts,
28 'categories' => $categories,
29 'tags' => $tags
30 ]);
31 }
32
33 . . .
34}
Line 22, there are two things you need to note here.
First, where('is_published', true)
makes sure that only published articles are retrieved.
And second, paginate()
method is one of Laravel's built-in method, allowing you to easily create pagination in your app. The paginate()
takes an integer as input. For example, paginate(10)
means 10 items will be displayed on each page.
Since this input variable will be used on many pages, you can create an environmental variable in the .env
file, and then you can retrieve it anywhere using env()
method.
.env
1. . .
2PAGINATE_NUM=12
Next, let's create the corresponding homepage view. Here is the template we've created, and this is the view structure:
1resources/views
2├── category.blade.php
3├── home.blade.php
4├── layout.blade.php
5├── post.blade.php
6├── search.blade.php
7├── tag.blade.php
8├── vendor
9│ ├── list.blade.php
10│ └── sidebar.blade.php
11└── welcome.blade.php
We'll start with the layout.blade.php
:
1<!doctype html>
2<html lang="en">
3 <head>
4 <meta charset="UTF-8" />
5 <meta http-equiv="X-UA-Compatible" content="IE=edge" />
6 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7 @vite(['resources/css/app.css', 'resources/js/app.js'])
8
9 @yield('title')
10 </head>
11
12 <body class="container mx-auto font-serif">
13 <nav class="flex flex-row justify-between h-16 items-center border-b-2">
14 <div class="px-5 text-2xl">
15 <a href="/"> My Blog </a>
16 </div>
17 <div class="hidden lg:flex content-between space-x-10 px-10 text-lg">
18 <a
19 href="https://github.com/thedevspacehq"
20 class="hover:underline hover:underline-offset-1"
21 >GitHub</a
22 >
23 <a
24 href="{{ route('dashboard') }}"
25 class="hover:underline hover:underline-offset-1"
26 >Dashboard</a
27 >
28 <a href="#" class="hover:underline hover:underline-offset-1">Link</a>
29 </div>
30 </nav>
31
32 @yield('content')
33
34 <footer class="bg-gray-700 text-white">
35 <div
36 class="flex justify-center items-center sm:justify-between flex-wrap lg:max-w-screen-2xl mx-auto px-4 sm:px-8 py-10">
37 <p class="font-serif text-center mb-3 sm:mb-0">
38 Copyright ©
39 <a href="https://www.thedevspace.io/" class="hover:underline"
40 >Eric Hu</a
41 >
42 </p>
43
44 <div class="flex justify-center space-x-4">. . .</div>
45 </div>
46 </footer>
47 </body>
48</html>
Line 7, by default, Laravel uses Vite for asset bundling. Previously, we installed Laravel Breeze, which uses TailwindCSS by default. This line of code will automatically import the corresponding app.css
and app.js
for you.
You can use a different framework of course, but you'll have to consult the documentations for details on how to use them with Laravel or Vite.
Next, for the homepage:
resources/views/home.blade.php
1@extends('layout')
2
3@section('title')
4<title>Home</title>
5@endsection
6
7@section('content')
8<div class="grid grid-cols-4 gap-4 py-10">
9 <div class="col-span-3 grid grid-cols-1">@include('vendor.list')</div>
10 @include('vendor.sidebar')
11</div>
12@endsection
And the post list:
resources/views/vendor/list.blade.php
1<!-- List of posts -->
2<div class="grid grid-cols-3 gap-4">
3 @foreach ($posts as $post)
4 <!-- post -->
5 <div class="mb-4 ring-1 ring-slate-200 rounded-md h-fit hover:shadow-md">
6 <a href="{{ route('post', ['post' => $post->id]) }}"
7 ><img
8 class="rounded-t-md object-cover h-60 w-full"
9 src="{{ Storage::url($post->cover) }}"
10 alt="..."
11 /></a>
12 <div class="m-4 grid gap-2">
13 <div class="text-sm text-gray-500">
14 {{ \Carbon\Carbon::parse($post->created_at)->format('M d, Y') }}
15 </div>
16 <h2 class="text-lg font-bold">{{ $post->title }}</h2>
17 <p class="text-base">
18 {{ Str::limit(strip_tags($post->content), 150, '...') }}
19 </p>
20 <a
21 class="bg-blue-500 hover:bg-blue-700 rounded-md p-2 text-white uppercase text-sm font-semibold font-sans w-fit focus:ring"
22 href="{{ route('post', ['post' => $post->id]) }}"
23 >Read more →</a
24 >
25 </div>
26 </div>
27 @endforeach
28</div>
29
30{{ $posts->links() }}
Line 9, Storage::url($post->cover)
generates the URL that points to the cover image.
Line 14, the way timestamps are stored in the database is not user friendly, so here we are using Carbon to reformat the timestamps.
Line 18, here we are using strip_tags()
to remove the HTML tags, and then limit()
sets the maximum length of the string, the excessive part will be replaced with ...
.
Line 30, remember that we the used paginate()
method to create paginator in the controller? This is how wit can be displayed in the view.
And then we have the sidebar:
resources/views/vendor/sidebar.blade.php
1<div class="col-span-1">
2 <div class="border rounded-md mb-4">
3 <div class="bg-slate-200 p-4">Search</div>
4 <div class="p-4">
5 <form
6 action="{{ route('search') }}"
7 method="POST"
8 class="grid grid-cols-4 gap-2">
9 {{ csrf_field() }}
10 <input
11 type="text"
12 name="q"
13 id="search"
14 class="border rounded-md w-full focus:ring p-2 col-span-3"
15 placeholder="Search something..." />
16 <button
17 type="submit"
18 class="bg-blue-500 hover:bg-blue-700 rounded-md p-2 text-white uppercase font-semibold font-sans w-full focus:ring col-span-1">
19 Search
20 </button>
21 </form>
22 </div>
23 </div>
24 <div class="border rounded-md mb-4">
25 <div class="bg-slate-200 p-4">Categories</div>
26 <div class="p-4">
27 <ul class="list-none list-inside">
28 @foreach ($categories as $category)
29 <li>
30 <a
31 href="{{ route('category', ['category' => $category->id]) }}"
32 class="text-blue-500 hover:underline"
33 >{{ $category->name }}</a
34 >
35 </li>
36 @endforeach
37 </ul>
38 </div>
39 </div>
40 <div class="border rounded-md mb-4">
41 <div class="bg-slate-200 p-4">Tags</div>
42 <div class="p-4">
43 @foreach ($tags as $tag)
44 <span class="mr-2"
45 ><a
46 href="{{ route('tag', ['tag' => $tag->id]) }}"
47 class="text-blue-500 hover:underline"
48 >{{ $tag->name }}</a
49 ></span
50 >
51 @endforeach
52 </div>
53 </div>
54 <div class="border rounded-md mb-4">
55 <div class="bg-slate-200 p-4">More Card</div>
56 <div class="p-4">
57 <p>
58 Lorem ipsum dolor sit amet consectetur adipisicing elit. Placeat,
59 voluptatem ab tempore recusandae sequi libero sapiente autem! Sit hic
60 reprehenderit pariatur autem totam, voluptates non officia accusantium
61 rerum unde provident!
62 </p>
63 </div>
64 </div>
65 <div class="border rounded-md mb-4">
66 <div class="bg-slate-200 p-4">...</div>
67 <div class="p-4">
68 <p>
69 Lorem ipsum dolor sit amet consectetur adipisicing elit. Placeat,
70 voluptatem ab tempore recusandae sequi libero sapiente autem! Sit hic
71 reprehenderit pariatur autem totam, voluptates non officia accusantium
72 rerum unde provident!
73 </p>
74 </div>
75 </div>
76</div>
The category page
app/Http/Controllers/CategoryController.php
1<?php
2
3namespace App\Http\Controllers;
4
5use App\Models\Category;
6use App\Models\Tag;
7use Illuminate\Contracts\View\View;
8use Illuminate\Http\RedirectResponse;
9use Illuminate\Http\Request;
10use Illuminate\Http\Response;
11
12class CategoryController extends Controller
13{
14 /**
15 * Display a list of posts belong to the category
16 */
17 public function category(string $id): View
18 {
19 $category = Category::find($id);
20 $posts = $category->posts()->where('is_published', true)->paginate(env('PAGINATE_NUM'));
21 $categories = Category::all();
22 $tags = Tag::all();
23
24 return view('category', [
25 'category' => $category,
26 'posts' => $posts,
27 'categories' => $categories,
28 'tags' => $tags
29 ]);
30 }
31
32 . . .
33}
resources/views/category.blade.php
1@extends('layout')
2
3@section('title')
4<title>Category - {{ $category->name }}</title>
5@endsection
6
7@section('content')
8<div class="grid grid-cols-4 gap-4 py-10">
9 <div class="col-span-3 grid grid-cols-1">@include('vendor.list')</div>
10 @include('vendor.sidebar')
11</div>
12@endsection
The tag page
app/Http/Controllers/TagController.php
1<?php
2
3namespace App\Http\Controllers;
4
5use App\Models\Category;
6use App\Models\Tag;
7use Illuminate\Contracts\View\View;
8use Illuminate\Http\RedirectResponse;
9use Illuminate\Http\Request;
10use Illuminate\Http\Response;
11
12class TagController extends Controller
13{
14 /**
15 * Display a list of posts belong to the category
16 */
17 public function tag(string $id): View
18 {
19 $tag = Tag::find($id);
20 $posts = $tag->posts()->where('is_published', true)->paginate(env('PAGINATE_NUM'));
21 $categories = Category::all();
22 $tags = Tag::all();
23
24 return view('tag', [
25 'tag' => $tag,
26 'posts' => $posts,
27 'categories' => $categories,
28 'tags' => $tags
29 ]);
30 }
31 . . .
32}
resources/views/tag.blade.php
1@extends('layout')
2
3@section('title')
4<title>Tag - {{ $tag->name }}</title>
5@endsection
6
7@section('content')
8<div class="grid grid-cols-4 gap-4 py-10">
9 <div class="col-span-3 grid grid-cols-1">@include('vendor.list')</div>
10 @include('vendor.sidebar')
11</div>
12@endsection
The post page
app/Http/Controllers/PostController.php
1<?php
2
3namespace App\Http\Controllers;
4
5use App\Models\Category;
6use App\Models\Post;
7use App\Models\Tag;
8use Illuminate\Contracts\View\View;
9use Illuminate\Http\RedirectResponse;
10use Illuminate\Http\Request;
11use Illuminate\Http\Response;
12use Illuminate\Support\Facades\Auth;
13use Illuminate\Database\Eloquent\Builder;
14
15class PostController extends Controller
16{
17 . . .
18
19 /**
20 * Display requested post
21 */
22 public function post(string $id): View
23 {
24 $post = Post::find($id);
25 $categories = Category::all();
26 $tags = Tag::all();
27 $related_posts = Post::where('is_published', true)->whereHas('tags', function (Builder $query) use ($post) {
28 return $query->whereIn('name', $post->tags->pluck('name'));
29 })->where('id', '!=', $post->id)->take(3)->get();
30
31 return view('post', [
32 'post' => $post,
33 'categories' => $categories,
34 'tags' => $tags,
35 'related_posts' => $related_posts
36 ]);
37 }
38
39 . . .
40}
Line 27-29, this is how we are able to retrieve related posts. The idea is to get the posts with the same tags. This chain of methods looks kind of scary, but don't worry, let's analyze them one by one.
The first method is easy, where('is_published', true)
returns all the posts that are published.
The whereHas()
method is where things get complicated. To understand whereHas()
we need to first talk about has()
. has()
is a Laravel Eloquent method that allows us to check the existence of a relationship. For example:
1$posts = Post::has('comments', '>', 3)->get();
This code will retrieve all the posts that have more than 3 comments. Notice that you cannot use where()
to do this because comments
is not a column in the posts
table, it is another table that has a relation with posts
.
whereHas()
works just like has()
, only it offers a little more power. Its second parameter is a function that allows us to inspect the content in "another table", which in our case, is the tags
table. We can access the tags
table through the variable $q
.
Line 28, the whereIn()
method takes two parameters, the first one is the specified column, the second is an array of acceptable values. The method will return the records with only the acceptable values and exclude the rest.
The rest should be very easy to understand. where('id', '!=', $post->id)
exclude the current post, and take(3)
takes the first three records.
resources/views/post.blade.php
1@extends('layout')
2
3@section('title')
4<title>Page Title</title>
5@endsection
6
7@section('content')
8<div class="grid grid-cols-4 gap-4 py-10">
9 <div class="col-span-3">
10
11 <img class="rounded-md object-cover h-96 w-full" src="{{ Storage::url($post->cover) }}" alt="..." />
12 <h1 class="mt-5 mb-2 text-center text-2xl font-bold">{{ $post->title }}</h1>
13 <p class="mb-5 text-center text-sm text-slate-500 italic">By {{ $post->user->name }} | {{ \Carbon\Carbon::parse($post->created_at)->format('M d, Y') }}</p>
14
15 <div>{!! $post->content !!}</div>
16
17 <div class="my-5">
18 @foreach ($post->tags as $tag)
19 <a href="{{ route('tag', ['tag' => $tag->id]) }}" class="text-blue-500 hover:underline" mr-3">#{{ $tag->name }}</a>
20 @endforeach
21 </div>
22
23 <hr>
24
25 <!-- Related posts -->
26
27 <div class="grid grid-cols-3 gap-4 my-5">
28 @foreach ($related_posts as $post)
29 <!-- post -->
30 <div class="mb-4 ring-1 ring-slate-200 rounded-md h-fit hover:shadow-md">
31 <a href="{{ route('post', ['post' => $post->id]) }}"><img class="rounded-t-md object-cover h-60 w-full" src="{{ Storage::url($post->cover) }}" alt="..." /></a>
32 <div class="m-4 grid gap-2">
33 <div class="text-sm text-gray-500">
34 {{ \Carbon\Carbon::parse($post->created_at)->format('M d, Y') }}
35 </div>
36 <h2 class="text-lg font-bold">{{ $post->title }}</h2>
37 <p class="text-base">
38 {{ Str::limit(strip_tags($post->content), 150, '...') }}
39 </p>
40 <a class="bg-blue-500 hover:bg-blue-700 rounded-md p-2 text-white uppercase text-sm font-semibold font-sans w-fit focus:ring" href="{{ route('post', ['post' => $post->id]) }}">Read more →</a>
41 </div>
42 </div>
43 @endforeach
44 </div>
45
46 </div>
47 @include('vendor.sidebar')
48</div>
49@endsection
Line 15, {!! $post->content !!}
is Laravel's safe mode. It tells Laravel to render HTML tags instead of displaying them as plain text.
The search page
app/Http/Controllers/PostController.php
1<?php
2
3namespace App\Http\Controllers;
4
5use App\Models\Category;
6use App\Models\Post;
7use App\Models\Tag;
8use Illuminate\Contracts\View\View;
9use Illuminate\Http\RedirectResponse;
10use Illuminate\Http\Request;
11use Illuminate\Http\Response;
12use Illuminate\Support\Facades\Auth;
13use Illuminate\Database\Eloquent\Builder;
14
15class PostController extends Controller
16{
17 . . .
18
19 /**
20 * Display search result
21 */
22 public function search(Request $request): View
23 {
24 $key = $request->input('q');
25 $posts = Post::where('title', 'like', "%{$key}%")->orderBy('id', 'desc')->paginate(env('PAGINATE_NUM'));
26 $categories = Category::all();
27 $tags = Tag::all();
28
29 return view('search', [
30 'key' => $key,
31 'posts' => $posts,
32 'categories' => $categories,
33 'tags' => $tags,
34 ]);
35 }
36
37 . . .
38}
resources/views/search.blade.php
1@extends('layout')
2
3@section('title')
4<title>Search - {{ $key }}</title>
5@endsection
6
7@section('content')
8<div class="grid grid-cols-4 gap-4 py-10">
9 <div class="col-span-3 grid grid-cols-1">@include('vendor.list')</div>
10 @include('vendor.sidebar')
11</div>
12@endsection