Laravel Fundamentals

Laravel Fundamentals

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.

bash
1mkdir <work_dir>
bash
1cd <work_dir>

And then execute the following command to create a fresh Laravel project. Make sure Docker in up and running.

bash
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.

bash
1cd <app_name>
bash
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.

laravel welcome

Exploring Laravel application structure

And now, let's take a look inside the project directory.

text
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 bootstrapdirectory: This directory contains the app.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: Contains index.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.

env
1APP_URL=http://127.0.0.1
env
1APP_URL=http://localhost
  • Database configurations

By default, Laravel Sail will use MySQL as our database, and the connection settings are defined as follows:

env
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.

env
1CUSTOM_VARIABLE=true

This variable can be accessed like this:

php
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

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:

Laravel welcome 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

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.

php
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.

php
1Route::match(['get', 'post'], '/', function () {
2    . . .
3});
4
5Route::any('/', function () {
6    . . .
7});

Passing data to view

Now, look inside the get() method.

php
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.

php
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

php
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.

php
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:

php
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:

php
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.

Router Parameters

It is also possible to match multiple parameters in a URL:

php
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 (?).

php
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.

php
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.

php
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.

php
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:

php
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:

php
1php artisan make:middleware EnsureTokenIsValid

This will create a new EnsureTokenIsValid class under the app/Http/Middleware directory.

php
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.

php
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.

php
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 MVC structure

The view layer

Let's start with views. Recall that in the previous section, we defined this route:

php
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

html
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:

php
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

html
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 /.

php
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.

html
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:

php
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

text
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:

text
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.

html
1@foreach ($users as $user)
2<p>This is user {{ $user->id }}</p>
3@endforeach

Or a while loop:

html
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:

html
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.

text
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.

php
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

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

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.

php
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

html
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.

html
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.

html
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.

html
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:

env
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.

bash
1php artisan make:migration create_flights_table

We can apply the migration file like this:

bash
1php artisan migrate

If you want to roll back the previous migration:

bash
1php artisan migrate:rollback

Or reset the migrations completely:

bash
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.

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('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:

php
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:

php
1Schema::rename($from, $to);

Or drop a table completely:

php
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:

bash
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

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:

bash
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.

php
1use Illuminate\Support\Facades\DB;
2
3$users = DB::table('users')->get();

We can add more constraints by chaining a where() method.

php
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.

bash
1php artisan make:model Post

If you'd like to generate a corresponding migration file, use the --migration option.

bash
1php artisan make:model Post --migration

app/Models/Post.php

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:

php
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.

php
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.

php
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).

php
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.

php
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.

php
1$flight = Post::create([
2    'title' => '. . .',
3    'content' => '. . .',
4]);

Or, you can update existing records with the update() method.

php
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.

php
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.

php
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.

php
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.

php
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 Posts. Similar to the one-to-one relation, it can be defined by putting a posts method in the Category model.

php
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.

php
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.

php
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}
php
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_idrole_id
11
21
32
12
23

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?

php
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.

php
1use App\Http\Controllers\UserController;
2
3Route::get('/users/{name}', [UserController::class, 'show']);

To create a new controller, run the following command:

php
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.

php
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:

php
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.

php
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:

bash
1php artisan make:controller PostController --resource

The generated controller will contain the following methods:

php
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.

php
1Route::resource('posts', PostController::class);

The following routes with different HTTP methods will be automatically created.

HTTP MethodURIActionRoute Name
GET/postsindexposts.index
GET/posts/createcreateposts.create
POST/postsstoreposts.store
GET/posts/{post}showposts.show
GET/posts/{post}/editeditposts.edit
PUT/PATCH/posts/{post}updateposts.update
DELETE/posts/{post}destroyposts.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:

bash
1php artisan make:migration 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->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 an id column, which is commonly used for indexing.
  • timestamps() creates two columns, created_at and uptated_at. These two columns will be automatically updated when the record is created or updated.
  • string('title') creates a column title with type VARCHAR, whose default length is 255 bytes.
  • string('content') creates the content column.

To apply the changes, run the following command:

bash
1php artisan migrate

And a new posts table should be generated:

database

Now, we can create the corresponding model for this table.

bash
1php artisan make:model Post

app/Models/Post.php

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:

bash
1php artisan make:controller PostController --resource

Finally, register this controller in the router:

php
1use App\Http\Controllers\PostController;
2
3Route::resource('posts', PostController::class);

You may check what routes are registered by running the following command:

bash
1php artisan route:list
text
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

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

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

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.

the create page

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

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.

database new record

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

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

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

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

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

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

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

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:

bash
1curl -s https://laravel.build/<app_name> | bash

Change into the app directory and start the server.

bash
1cd <app_name>
bash
1./vendor/bin/sail up

To make things easier, let's create an alias for sail. Run the following command:

bash
1alias sail='[ -f sail ] && sh sail || sh vendor/bin/sail'

From now on, you can run sail directly without specifying the entire path.

bash
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:

bash
1sail composer require laravel/breeze --dev
bash
1sail artisan breeze:install
bash
1sail artisan migrate
bash
1sail npm install
bash
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.

Laravel Breeze 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

keytype
idbigInteger
created_at
updated_at
titlestring
coverstring
contenttext
is_publishedboolean

Categories

keytype
idbigInteger
created_at
updated_at
namestring

Tags

keytype
idbigInteger
created_at
updated_at
namestring

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

keytype
idbigInteger
created_at
updated_at
titlestring
coverstring
contenttext
is_publishedboolean
user_idbigInteger
category_idbigInteger

And we also need a separate pivot table for the many-to-many relation between post and tag:

Post/tag

keytype
post_idbigInteger
tag_idbigInteger

Implement database structure

To implement this design, generate models and migration files using the following commands:

bash
1sail artisan make:model Post --migration
bash
1sail artisan make:model Category --migration
bash
1sail artisan make:model Tag --migration

And a separate migration file for the post_tag table:

bash
1sail artisan make:migration create_post_tag_table

database/migrations/create_posts_table.php

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

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

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

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:

bash
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

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

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

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

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).

bash
1php artisan make:controller PostController --resource
bash
1php artisan make:controller CategoryController --resource
bash
1php artisan make:controller TagController --resource

Then create routes for each of these controllers:

routes/web.php

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

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

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:

bash
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

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.

php
1$post->tags()->detach();

Views

When building a view system, always remember to be organized. This is the structure we are going with:

text
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

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

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

html
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.

Dashboard home

Categories list

Create category

Update post

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

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

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

text
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:

text
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:

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

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

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

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

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

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

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

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

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:

php
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

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

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

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

Interested in Learning Web Development?

Starting from HTML, CSS to JavaScript, from language syntaxes to frameworks, from the frontend to the backend, we'll guide you through every step of your coding journey.

Get started from here 🎉