Laravel 9 tutorial using Breeze

In this tutorial we’ll create Laravel 9 CRUD app using default Breeze starter kit. Laravel 9.x requires a minimum PHP version of 8.0. This tutorial assumes that you are using Linux, MacOS or WSL on Windows and Node.js, Composer and PHP 8 along with required modules i.e bcmatch, sqlite, mbstring, xml, zip, gd, mcrypt are properly installed. Also make sure that SQLite is installed as we’ll be using SQLite to keep things simple and quick; you may use MySQL or PostgreSQL as you like, however make sure the corresponding PHP module is installed too.

Using Composer, enter following command in terminal to install Laravel 9;

composer create-project laravel/laravel app 9.*

Now go inside newly created app directory by executing;

cd app

Using Composer, add Breeze package for basic authentication implementation;

composer require laravel/breeze:* --dev

Now install Breeze package by executing;

php artisan breeze:install

The above command will also install node.js modules and compile client-side assets (JS and CSS files) for the first time. Afterwards, whenever there is change in view files, the following will have to be executed:

npm run build

Next, setup database and Laravel environment file. Here, I’m configuring SQLite database for simplicity purposes.

Being in parent directory (app), execute following command in order to create new SQLite database file;

touch database/database.sqlite

Then find .env config file in the parent directory, open it and amend DB_CONNECTION line as follows;

DB_CONNECTION = sqlite

You can remove DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD from .env when using SQLite database.

Since, initial setup of Laravel app is complete, we can start building our CRUD. First enter the following command to create Post model along with the migration:

php artisan make:model Post -m

Open newly created migration file from database/migrations/ which is named something like …create_posts_table.php and then insert the desired table fields;

<?php

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

return new class extends Migration
{
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('body');
            $table->timestamps();
        });
    }
    public function down()
    {
        Schema::dropIfExists('posts');
    }
};

Enter following command in parent directory to migrate the above migration along with Breeze authentication migrations;

php artisan migrate

Since our database part is done, let’s move onto Model and Controller. First, open app/Models/Post.php model file and insert protected fillable array as follows;

<?php

namespace App\Models;

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

class Post extends Model
{
    use HasFactory;

    protected $fillable = [
        'title', 'body'
    ];
}

Having done with the Model part of our app, we move onto Controller part by executing following command in parent directory;

php artisan make:controller PostController

Go ahead and open this controller file which is located at app/Http/Controllers/PostController.php and amend it as follows in order to define basic CRUD (Create, Read, Update, Delete) functions;

<?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\Request;
use App\Models\Post;

class PostController extends Controller
{

    public function index()
    {
        $posts = Post::all();
        return view('posts.index', compact('posts'));
    }

    public function create()
    {
        return view('posts.create');
    }

    public function store()
    {
        Post::create(
            Request::validate([
            'title' => ['required', 'max:50'],
            'body' => ['required'],
            ])
        );
        return Redirect::route('posts')->with('success', 'Post created.');
    }

    public function edit(Post $post)
    {
        return view('posts.edit', compact('post'));
    }

    public function update(Post $post)
    {
        $post->update(
            Request::validate([
            'title' => ['required', 'max:50'],
            'body' => ['required'],
            ])
        );

        return Redirect::route('posts')->with('success', 'Post updated.');
    }

    public function destroy(Post $post)
    {
        $post->delete();
        return Redirect::back()->with('success', 'Post deleted.');
    }

}

In order to define routes, open routes/web.php and put the following line at the upper side of it;

use App\Http\Controllers\PostController;

And then insert following CRUD routes into this routes/web.php;

Route::get('posts', [PostController::class, 'index'])
    ->name('posts')
    ->middleware('auth');

Route::get('posts/create', [PostController::class, 'create'])
    ->name('posts.create')
    ->middleware('auth');

Route::post('posts', [PostController::class, 'store'])
    ->name('posts.store')
    ->middleware('auth');

Route::get('posts/{post}/edit', [PostController::class, 'edit'])
    ->name('posts.edit')
    ->middleware('auth');

Route::put('posts/{post}', [PostController::class, 'update'])
    ->name('posts.update')
    ->middleware('auth');

Route::delete('posts/{post}', [PostController::class, 'destroy'])
    ->name('posts.destroy')
    ->middleware('auth');

We’re done with the Model and Controller parts of our app. Now let’s create Views for this app using Blade templates.

Create directory named posts inside resources/views directory. If you are in parent directory, you may enter following command to make this directory;

mkdir resources/views/posts

Now create following three files inside resources/views/posts directory:

  1. index.blade.php
  2. create.blade.php
  3. edit.blade.php

You can execute following command inside parent directory (app) in order to create these files in one go;

touch resources/views/posts/{index,create,edit}.blade.php

Insert following code inside index.blade.php;

<x-app-layout>
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            {{ __('Posts') }}
        </h2>
    </x-slot>
    @if(session()->get('success'))
        <div class="p-2 bg-indigo-200 border-b border-indigo-400 rounded-md">
        {{ session()->get('success') }}
        </div>
    @endif
      <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
                <div class="p-6 bg-white border-b border-gray-200">
                <a href="{{ route('posts.create')}}" class="inline-flex items-center px-4 py-2 mb-4 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 active:bg-gray-900 focus:outline-none focus:border-gray-900 focus:ring ring-gray-300 disabled:opacity-25 transition ease-in-out duration-150">New Post</a>
                    <table class="shadow-lg">
                        <thead>
                            <tr class="bg-gray-400 text-white font-extrabold" >
                            <td class="px-4 py-1 text-center" >ID</td>
                            <td class="px-4 py-1">Title</td>
                            <td class="px-4 py-1">Body</td>
                            <td class="px-4 py-1 text-center w-2/6" colspan = '2'>Actions</td>
                            </tr>
                        </thead>
                        <tbody>
                            @foreach($posts as $post)
                            <tr>
                                <td class="border px-4 py-1 text-center">{{ $post->id }}</td>
                                <td class="border px-4 py-1">{{ $post->title }}</td>
                                <td class="border px-4 py-1">{{ $post->body }}</td>
                                <td class="border px-4 py-1">
                                    <a href="{{ route('posts.edit',$post->id)}}" class="inline-flex items-center px-4 py-1 bg-green-800 border border-transparent rounded-lg font-semibold text-xs text-white tracking-widest hover:bg-green-700 active:bg-green-900 focus:outline-none focus:border-green-900 focus:ring ring-green-300 disabled:opacity-25 transition ease-in-out duration-150">Edit</a>
                                </td>
                                <td class="border px-4 py-1">
                                    <form action="{{ route('posts.destroy', $post->id)}}" method="post">
                                    @csrf
                                    @method('DELETE')
                                    <button type="submit" class="inline-flex items-center px-4 py-1 bg-red-800 border border-transparent rounded-lg font-semibold text-xs text-white tracking-widest hover:bg-red-700 active:bg-red-900 focus:outline-none focus:border-red-900 focus:ring ring-red-300 disabled:opacity-25 transition ease-in-out duration-150">Delete</button>
                                    </form>
                                </td>
                            </tr>
                            @endforeach
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
    </div>
</x-app-layout>

Insert following code in create.blade.php;

<x-app-layout>
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            {{ __('Posts') }}
        </h2>
    </x-slot>
    <x-auth-validation-errors :errors="$errors" />
    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
                <div class="p-6 bg-white border-b border-gray-200">
                    <form method="post" action="{{ route('posts.store') }}">
                        @csrf
                        <div class="p-2 flex items-center">
                            <label for="title" class="w-20">Title:</label>
                            <input type="text" class="rounded-md border-gray-300 hover:border-gray-600 flex-1" name="title"/>
                        </div>
                        <div class="p-2 flex items-center">
                            <label for="body" class="w-20">Body:</label>
                            <input type="text" class="rounded-md border-gray-300 hover:border-gray-600 flex-1" name="body"/>
                        </div>
                        <div class="p-6">
                            <x-button> {{ __('Add Post') }} </x-button>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</x-app-layout>

Insert following code in edit.blade.php;

<x-app-layout>
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            {{ __('Posts') }}
        </h2>
    </x-slot>
    <x-auth-validation-errors :errors="$errors" />
    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
                <div class="p-6 bg-white border-b border-gray-200">
                    <form method="post" action="{{ route('posts.update',$post->id) }}">
                    @method('PUT')
                    @csrf
                    <div class="p-2 flex items-center">
                        <label for="title" class="w-20">Title:</label>
                        <input type="text" class="rounded-md border-gray-300 hover:border-gray-600 flex-1" name="title" value="{{ $post->title }}"/>
                    </div>
                    <div class="p-2 flex items-center">
                        <label for="body" class="w-20">Body:</label>
                        <input type="text" class="rounded-md border-gray-300 hover:border-gray-600 flex-1" name="body" value="{{ $post->body }}"/>
                    </div>
                    <div class="p-6">
                        <x-button> {{ __('Edit Post') }} </x-button>
                    </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</x-app-layout>

Now execute following command in parent directory to compile UI assets (make habit of running this command every time you make changes to client-side views):

npm run build

Finally, execute following command in parent directory to run PHP development server;

php artisan serve

Open browser and enter following URL in address bar;

http://localhost:8000/posts

If you haven’t created a user yet, then do it first and then enter the above URL.

That’s it for CRUD part in Laravel 9 using Breeze; you can stop reading here if you came for CRUD. However, down below we’ll discuss couple of other things which will be helpful in Laravel 9 Breeze based app development.

On the front-end, there’s been subtle transition within Laravel landscape for last couple of years. If you are coming from background with Laravel 5, 6 or 7, you’ll have to consider moving away from the authentication modules which were based on UI libraries like Bootstrap. As of late, Laravel has inducted Alpine.js and Tailwind CSS as first class citizens to its ecosystem in order to handle look and feel and interactivity. That’s why when we incorporate default Breeze starter kit in Laravel 9 app (as elaborated above), Alpine.js and Tailwind CSS get installed as dev dependencies in package.json and the generated UI components make use of Alpine.js along with Tailwind CSS. Alpine.js is simple and modern (Vue like) JS framework which can be used as an alternative to JQuery; and Tailwind CSS is probably the best CSS library out there. So the change is good.

Let’s explore UI components generated by Breeze when we executed php artisan breeze:install as above; go into resources/views directory and find auth, components and layouts directories along with dashboard.blade.php, open the dashboard.blade.php and have a good look at it. The first tag is <x-app-layout> which is calling in app.blade.php component from resources/views/layouts folder. Let’s open app.blade.php from layouts folder. This is a base file which defines the basic layout for the logged in users. There’s another layout template guest.blade.php which is used as base file for all views where the user is not logged in e.g. login or register view pages (login or register view files are present in resources/views/auth folder).

Once the user is logged in, the navigation bar is presented to access the app. This is accomplished by inserting @include(‘layouts.navigation’) in the body part of app.blade.php. So, let’s open resources/views/layouts/navigation.blade.php. This navigation.blade.php contains the app’s menu items both in normal view as well as responsive view. let’s find the following in this file:

                   <x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
                        {{ __('Dashboard') }}
                    </x-nav-link>

That’s the first item in app’s menu. Let’s copy and paste this code snippet and change the copied code as follows:

                   <x-nav-link :href="route('posts')" :active="request()->routeIs('posts')">
                        {{ __('Posts') }}
                    </x-nav-link>

Similarly, find the following code snippet, a bit further down in navigation.blade.php:

            <x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
                {{ __('Dashboard') }}
            </x-responsive-nav-link>

Copy paste it and change the copied code snippet as follows:

            <x-responsive-nav-link :href="route('posts')" :active="request()->routeIs('posts')">
                {{ __('Posts') }}
            </x-responsive-nav-link>

Save navigation.blade.php and reload the app. Now we have two items in menu i.e. ‘Dashboard’ and ‘Posts’. Nice! One more thing to note here – <x-nav-link> and <x-responsive-nav-link> tags were represented by nav-link.blade.php and responsive-nav-link.blade.php files in resources/views/components folder.

Let’s move on to create our own component which will display the ‘success’ flash message (right now our ‘success’ flash message is presented in very ugly manner). Here we’ll utilize Alpine.js in order to make things eye-catching.

Execute the following command to create a component named flash-success:

php artisan make:component flash-success --view

Now open flash-success.blade.php located at resources/views/components folder and insert following into flash-success.blade.php:

<div>
@if ($msg = session('success'))
<div class="fixed inset-x-0 bottom-0 flex items-end justify-center px-4 py-6 pointer-events-none sm:p-6 sm:items-start sm:justify-end z-50">
    <div 
      x-data="{ show: true }"
      x-show="show" 
      x-transition:enter="transition ease-out duration-300"
      x-transition:enter-start="opacity-0 transform scale-90"
      x-transition:enter-end="opacity-100 transform scale-100"
      x-transition:leave="transition ease-in duration-300"
      x-transition:leave-start="opacity-100 transform scale-100"
      x-transition:leave-end="opacity-0 transform scale-90"
        class="max-w-sm w-full bg-white shadow-lg rounded-lg pointer-events-auto">
      <div class="bg-indigo-300 rounded-lg shadow-xs overflow-hidden">
        <div class="p-2">
          <div class="flex items-start">
            <div class="ml-3 w-0 flex-1 pt-0.5">
              <p class="text-lg font-extrabold text-indigo-900">
                {{ $msg }}
              </p>
            </div>
            <div class="ml-3 mr-3 flex-shrink-0 flex">
              <button @click="show = false" class="inline-flex font-extrabold text-indigo-900 focus:outline-none focus:text-indigo-500 transition ease-in-out duration-150">
                x
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
</div>
@endif
</div>

A quick note on Alpine.js; we’ve used x-data attribute to define a data variable show as true. Then used x-show attribute to define the element’s visibility (which is set true thru show data). On @click event in <button>, show is set as false making message div hidden. x-transition attribute is used for nice animation effects which is self explanatory. For official tutorial on Alpine.js follow this link. Finally, remove following flash message code from index.blade.php;

    @if(session()->get('success'))
        <div class="p-2 bg-indigo-200 border-b border-indigo-400 rounded-md">
        {{ session()->get('success') }}
        </div>
    @endif

and put following line in place of above lines:

<x-flash-success/>

now execute following command to compile UI assets:

npm run build

Go ahead run PHP development server if not already running and enter http://localhost:8000/posts in browser. Enter or delete some data and observe ‘success’ flash message. Cool!

Leave a Comment