Laravel 9: Building a blog application with Bootstrap 5

Laravel 9: Building a blog application with Bootstrap 5

Exploring the features of Laravel 9 by building a blog application using bootstrap 5

In this article, we'll look into the new laravel version and use it to build a blog application (create, read, update and delete) with bootstrap 5.

Laravel is the most widely used and popular PHP framework for building custom, robust, and scalable web applications.

The Laravel 9 was released on the 8th of February 2022 by the awesome Laravel team led by Taylor Otwell with minimum support for PHP 8.0 - 8.1.

Laravel v9 is the first LTS(Long-Term Support) to be introduced following the 12 months release cycle and will be stable for 12 months until another release most likely on January 24th, 2023.

Laravel 9 continues the improvements made in Laravel 8.x by introducing support for Symfony 6.0 components, Symfony Mailer, Flysystem 3.0, improved route:list output, a Laravel Scout database driver, new Eloquent accessor/mutator syntax, implicit route bindings via Enums, and a variety of other bug fixes and usability improvements. source

Here's the link to the laravel release note

In a bit to get on board with some of the features in Laravel 9, we'll be building a simple blog application.

Let's start with a fresh laravel 9 app installation

laravel new laravel9_blog_application

OR

composer create-project laravel/laravel9_blog_application

image.png

After successful installation change the directory to laravel9_blog_application

cd laravel9_blog_application

--Run

php artisan --version

image.png

Yoo!!! Laravel 9 app created

php artisan serve

image.png

Awesome

image.png

**Use the version constraint such as ^9.0 in referencing the laravel framework or components since major releases do include breaking changes. **

Next, let's set the environment variable for this blog application on the .env file

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel9_blog_application
DB_USERNAME=root
DB_PASSWORD=

Before we run migrate let's look at one of the new features in laravel 9

Anonymous Stub Migrations

The Laravel team released Laravel 8.37 with anonymous migration support, which solves a GitHub issue with migration class name collisions. The core of the problem is that if multiple migrations have the same class name, it'll cause issues when trying to recreate the database from scratch.

As you may have noticed in the example below, Laravel will automatically assign a class name to the migration that you generate using the make:migration command. However, if you wish, you may return an anonymous class from your migration file. This is primarily useful if your application accumulates many migrations and two of them have a class name collision:

The stub migration feature eliminates migration class name collisions when you run php artisan make:migration

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {}

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down(){ }
};

Next, run the migrate command

php artisan migrate

image.png

Install npm and Bootstrap 5 dependencies using the npm command below

npm install

npm install --save bootstrap jquery popper.js cross-env

bootstrap 5.jpg

Import packages in the resources/js/bootstrap.js file

try {
    window.Popper = require('popper.js').default;
    window.$ = window.jQuery = require('jquery');

    require('bootstrap');
} catch (e) {
}

Rename the css folder in the resource folder to sass

Import packages in the resources/sass/app.scss file

// bootstrap
@import "~bootstrap/scss/bootstrap";

image.png

Open the webpack.mix.js and update it

 mix.js('resources/js/app.js', 'public/js')
 .sass('resources/sass/app.scss', 'public/css');

Next, compile the installed assets

npm run dev

Kindly note: if this error occurs

ERROR in ./node_modules/bootstrap/dist/js/bootstrap.esm.js 6:0-41 Module not found: Error: Can't resolve '@popperjs/core' in

Run the command below and recompile

npm i @popperjs/core --save

image.png

Next, we'll add a basic authentication feature (Login and register) using laravel fortify.

The process for setting it up in Laravel 9 is the same as this article I published last year on Complete Laravel 8 Authentication Using Laravel Fortify and Bootstrap 4.

Laravel Fortify

Laravel Fortify is a frontend agnostic authentication backend implementation for Laravel. Fortify registers the routes and controllers needed to implement all of Laravel's authentication features, including login, registration, password reset, email verification, and more.

Laravel Fortify essentially takes the routes and controllers of Laravel Breeze and offers them as a package that does not include a user interface. This allows you to still quickly scaffold the backend implementation of your application's authentication layer without being tied to any particular frontend opinions. Read more about fortify

We're using fortify because, the Laravel UI in previous versions is still available with a new version but it's advisable that users migrate to recommended authentication features such as Jetstream, Breeze or Fortify because it might be removed in subsequent versions.

composer require laravel/fortify

image.png

Next, publish Fortify's resources using the vendor:publish command:

php artisan vendor:publish --provider="Laravel\Fortify\FortifyServiceProvider"

Register the file within the provider array of your app configuration file

  App\Providers\FortifyServiceProvider::class,

In this article, we'll stop at adding the auth views and setting up the register, verify and login logic only. You can get sample auth views from this repo or use your bootstrap custom snippets in each file as desired.

In the root of resources/views folder create auth folder layouts folder & home.blade.php

create the following files in the folders respectively

  • resources/views/layouts/app.blade.php

  • resources/views/auth/login.blade.php

  • resources/views/auth/register.blade.php

Add the following methods to Fortify Provider file

app/Providers/FortifyServiceProvider

        Fortify::loginView(function () {
            return view('auth.login');
        });

        Fortify::registerView(function () {
            return view('auth.register');
        });

We're simply telling fortify to render the views we've set up using these boot methods

Here's a screenshot of the login and register pages

  • Login page

image.png

  • Register

image.png

Improved Ignition Exception Page in Laravel 9

Ignition is developed by Spatie.

Ignition, the open-source exception debug page created by Spatie, has been redesigned from the ground up. The new, improved Ignition ships with Laravel 9.x and includes light / dark themes, customizable "open in editor" functionality, and more.

Here's an example of an exception page

image.png

Improved route:list CLI Output in Laravel 9

Improved route:list CLI output was contributed by Nuno Maduro.

The route:list CLI output has been significantly improved for the Laravel 9.x release, offering a beautiful new experience when exploring your route definitions.

image.png

Let's add email verification using mailtrap

Symfony Mailer support was contributed by Dries Vints, James Brooks, and Julius Kiekbusch.

In Laravel 9 Swift Mailer is no longer maintained and has been succeeded by Symfony mailer.

Let's look at the email verification feature .. uncomment the feature of the email verification method in fortify.php file

Features::emailVerification(),

it'll automatically add the three routes for email verification (send, notice and verify)

Add this to the fortify service provider

        Fortify::verifyEmailView(function () {
            return view('auth.verify');
        });

Next, go to User.php model under the Models folder and modify it by implementing the MustVerifyEmail interface

Next, configure your preferred email driver to deliver the email verification link to the user mail.

For testing purposes, you can create an account on mailtrap.io and configure the mail settings on the .env file

MAIL_DRIVER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS="your@gmail.com"
MAIL_FROM_NAME="${APP_NAME}"

Next, in web.php file specify the routes that are not accessible until a user is verified

Route::middleware(['auth', 'verified'])->group(function () {
//define the routes accessible only to verified user here
});

Next refresh the browser. Register as a user. Check the mailtrap inbox to see the mail for verifying the email.

Awesome!!! Everything works fine even with the new Swift Mailer

image.png

image.png

If you want to go further in using the other features of Fortify kindly check out this article

Next, let's create a posts table to keep a record of posts for our blog

We'll use a free blog template from startbootstrap for some of the posts pages

php artisan make:migration create_posts_table --create=posts

Add the following columns

            $table->foreignId('user_id');
            $table->string('title');
            $table->text('description');

image.png

php artisan migrate

image.png

Next, let's create the posts folder and the index, show, create and edit blade file. The home view will be used to list all the posts.

image.png

Next, we'll create a resource controller and model for Posts named PostController.

php artisan make:controller PostController --resource --model=Post

image.png

Here's the PostController. We have an additional method called all_posts to display all posts

<?php

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class PostController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        $posts = Post::where('user_id', Auth::user()->id)->latest()->get();

        return view('posts.index', compact('posts'));
    }

    /**
     * Show the form for creating a new resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function create()
    {
        return view('posts.create');
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        $request->validate([
            'title' => 'required|string|max:255',
            'description' => 'required|string',
        ]);

        $post = new Post($request->all());
        $user = Auth::user();

        $user->posts()->save($post);

        return redirect()->route('posts.create')
                        ->with('success','Post created successfully.');
    }

    /**
     * Display the specified resource.
     *
     * @param  \App\Models\Post  $post
     * @return \Illuminate\Http\Response
     */
    public function show(Post $post)
    {
        return view('posts.show',compact('post'));
    }

    /**
     * Show the form for editing the specified resource.
     *
     * @param  \App\Models\Post  $post
     * @return \Illuminate\Http\Response
     */
    public function edit(Post $post)
    {
        return view('posts.edit', compact('post'));
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \App\Models\Post  $post
     * @return \Illuminate\Http\Response
     */
    public function update(Request $request, Post $post)
    {
        $request->validate([
            'title' => 'required|string|max:255',
            'description' => 'required|string',
        ]);

        $post->update($request->all());

        return redirect()->route('posts.index')
                        ->with('success','Post updated successfully');
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  \App\Models\Post  $post
     * @return \Illuminate\Http\Response
     */
    public function destroy(Post $post)
    {
        $post->delete();

        return redirect()->route('posts.index')
                        ->with('success','Post deleted!!!');
    }


    public function all_posts()
    {
        $posts = Post::latest()->get();
        return view('home', compact('posts'));
    }
}

Post Model

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;


class Post extends Model
{
    use HasFactory;

    protected $fillable = [
        'title', 'description'
    ];


//relationship to retrieve the user associated with a post
    public function user()
    {
        return $this->belongsTo(User::class);
    }


//new laravel 9  single non-prefix method
    public function created_at(): Attribute
    {
        return new Attribute(
            get: fn ($value) => $value->diffForHumans(),
            // set: fn ($value) => $value,
        );
    }
}

In Laravel 9.x you may define an accessor and mutator using a single, non-prefixed method by type-hinting a return type of Illuminate\Database\Eloquent\Casts\Attribute:

use Illuminate\Database\Eloquent\Casts\Attribute;

   //let's format the created_at timestamp to be readable by readers

    public function created_at(): Attribute
    {
        return new Attribute(
            get: fn ($value) => $value->diffForHumans(),
            //set: fn ($value) => $value, you can also set the value when creating the record
        );
    }

image.png

let's update the user model to associate posts to the author(user)

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

Route setup on web.php

Laravel 9 Controller Route Groups

In Laravel 9, you may now use the controller method to define the common controller for all of the routes within the group. Then, when defining the routes, you only need to provide the controller method that they invoke:

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\PostController;


//Controller Route Groups
Route::controller(PostController::class)->group(function () {
    Route::get('/', 'all_posts');
    Route::get('/all/posts', 'all_posts')->name('home');
});

Route::middleware(['auth', 'verified'])->group(function () {
    Route::resource('posts', PostController::class);
});

Post pages (index, create, edit, show), Including form validations, Including the home & welcome page views are available on this tutorial repository

The tutorial repository contains the views.

Welcome Page

image.png

Add post

image.png

User's blog posts

image.png

Show post details

image.png

If logged in user is the author show edit and delete

image.png

In real-world applications ensure to make use of the soft delete for a delete action and also confirm the action from the user just in a case of unintended click.

Support for String Functions in PHP 8

The most recent string functions str_contains(), str_starts_with(), and str_ends_with() internally in the IlluminateSupportStr class are incorporated for use in laravel 9.

You got to this point awesome!!! Congratulations!!! You can now build an app of your choice using laravel 9 with bootstrap 5.

Conclusion

In this article, we've covered some new features introduced in Laravel 9. Worthy of note is that there's no difficulty in migrating to Laravel 9 from previous Laravel versions.

💡
Follow me on Hashnode: Alemsbaja X: Alemsbaja | Youtube: Tech with Alemsbaja to stay updated on more articles

Find this helpful or resourceful?? kindly share and feel free to use the comment section for questions, answers, and contributions.

Did you find this article valuable?

Support Alemoh Rapheal Baja by becoming a sponsor. Any amount is appreciated!