Laravel 11: CRUD and File Upload Tutorial With Laravel Breeze

Updated
featured-image.png

💡 I included the link to the example repository at the conclusion of this tutorial. And if you are looking for the Laravel 10 version, you can read it here.

In the dynamic web development landscape, Laravel has emerged as a standout PHP framework, renowned for its elegance, simplicity, and extensive ecosystem of tools and libraries. With each iteration, Laravel continues to evolve, introducing new features and enhancements to further simplify the development process. In Laravel 11, the framework reaches new heights, offering developers better tools for building powerful, scalable web applications.

One of the fundamental tasks in web development is implementing CRUD (Create, Read, Update, Delete) operations, which are the backbone of many applications. Additionally, handling file uploads is a common requirement in modern web development, whether it is uploading images, documents, or other types of files. In this comprehensive tutorial, we’ll learn to master CRUD operations and file uploads using Laravel 11, along with the lightweight authentication scaffolding provided by Laravel Breeze.

The CRUD application will allow us to manage task lists, including creating new tasks, updating tasks, deleting tasks, and marking tasks as complete/uncomplete. Apart from that, we will also learn how to upload files for the task.

By the end of this tutorial, you will have a solid understanding of how to leverage Laravel’s capabilities to create dynamic web applications with user authentication, database interaction, and file handling functionality.

So, buckle your seat belts as we embark on this journey to unlock the full potential of Laravel 11, empowering you to build modern, feature-rich web applications easily and efficiently.

Step 1: Installing Laravel 11

Before diving into Laravel 11’s powerful features, ensure that your system has PHP version 8.2.0 or higher and Composer installed. To kickstart your Laravel 11 project, execute the following command in your terminal:

composer create-project --prefer-dist laravel/laravel:^11 blog_laravel11_crud_file

This command swiftly creates a new Laravel 11 project housed in a directory named blog_laravel11_crud_file.

Step 2: Setting Up the Database

A crucial step in building any application is configuring the database. In this tutorial, MySQL will be our database engine of choice.

Begin by updating the .env file within your Laravel project directory. Change the DB_CONNECTION value to mysql to indicate the MySQL database driver. Subsequently, create a database named blog_laravel11_crud_file in your MySQL server.

Finally, adjust the database connection settings in the .env file to match your MySQL credentials, including the username and password.

Step 3: Testing Our Laravel Installation

With Laravel installed and our database set up, it’s time to ensure everything is running smoothly. Let’s test our Laravel installation by executing the following commands in your terminal:

php artisan migrate
php artisan serve

Upon running these commands, Laravel will migrate its necessary database tables and start a development server. Navigate to localhost:8000 in your web browser to witness the beautifully crafted home page, indicating a successful installation.

Step 4: Installing Laravel Breeze

To streamline authentication within our Laravel 11 application, we’ll incorporate Laravel Breeze, a lightweight authentication scaffold tailored for Laravel 8 and beyond. Begin by installing Laravel Breeze via Composer:

composer require laravel/breeze --dev

Once Laravel Breeze is downloaded, execute the following Artisan command to integrate it into your Laravel 11 application:

php artisan breeze:install

Upon execution, a prompt will appear, asking which stack we want to install. Answer “blade”, we will use Blade stack with Alpine. Another prompt may appear asking if we want to enable dark mode support. For now, we won’t use it, so just enter or answer “no”. Finally, a prompt may appear asking “Which testing framework do you prefer”. Currently, PHPUnit is enough so just enter or choose “1”.

Step 5: Creating Tasks Migration

With Laravel 11 and Laravel Breeze seamlessly integrated, it’s time to set up our database structure. We’ll start by creating a migration for our tasks table, which will serve as the foundation for our CRUD operations in this tutorial.

To generate a migration file for the tasks table, execute the following command in your terminal:

php artisan make:migration "create tasks table"

This command generates a new migration file within the database/migrations directory.

Now, open the newly created migration file and insert the following code to define the structure of the tasks table:

// database\migrations\xxxxxxxxxxxxx_create_tasks_table.php

public function up()
{
    // Create the 'tasks' table with specified schema
        Schema::create('tasks', function (Blueprint $table) {
            // Primary key 'id' field auto-incremented
            $table->id();

            // 'content' column to store the main content of the task
            $table->string('content');

            // 'info_file' column to store information about attached files (nullable)
            $table->string('info_file')->nullable();

            // 'is_completed' column to track whether the task is completed or not (default: 0)
            $table->unsignedTinyInteger('is_completed')->default(0);

            // 'created_at' and 'updated_at' columns to store timestamps of creation and updates
            $table->timestamps();
        });
}

public function down()
{
    // Drop the 'tasks' table if it exists
    Schema::dropIfExists('tasks');
}

This code will create a tasks table with an id, content, info_file, is_completed, and timestamps columns (created_at and updated_at columns).

Read also:

Step 6: Migrating the Migration

With the tasks table schema defined, it’s time to execute the migration and bring our database structure to life. Execute the following command in your terminal:

php artisan migrate

This command triggers the execution of all pending migrations located in the database/migrations directory, including the one we just created.

Step 7: Creating Routes

Now that our tasks table is established, we need routes to handle the CRUD (Create, Read, Update, Delete) operations for our tasks. Let’s define these routes in the routes/web.php file.

Open the routes/web.php file in your Laravel project and update it with the following code:

<?php

use App\Http\Controllers\ProfileController;
use App\Http\Controllers\TaskController;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return view('welcome');
});

Route::get('/dashboard', function () {
    return view('dashboard');
})->middleware(['auth', 'verified'])->name('dashboard');

Route::middleware('auth')->group(function () {
    Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
    Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
    Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');

    // Add the following route to the existing routes because we want the tasks route accessible to authenticated users only.
    // We'll use a resource route because it contains all the exact routes we need for a typical CRUD application.
    Route::resource('tasks', TaskController::class);
    Route::get('/tasks/{task}/mark-completed', [TaskController::class, 'markCompleted'])->name('tasks.mark-completed');
    Route::get('/tasks/{task}/mark-uncompleted', [TaskController::class, 'markUncompleted'])->name('tasks.mark-uncompleted');
});

require __DIR__.'/auth.php';

This adds the tasks resource route to the existing routes and applies the auth middleware to restrict access to authenticated users only. The TaskController is set up as a resource controller to handle all the typical CRUD operations.

Step 8: Creating TaskController

To manage the CRUD operations for our tasks, we’ll create a dedicated controller named TaskController. This controller will handle actions such as creating, reading, updating, and deleting tasks. Execute the following command in your terminal to generate the controller along with resource methods:

php artisan make:controller TaskController --resource

This command swiftly generates a new TaskController within the app/Http/Controllers directory, equipped with all the necessary resource methods.

Now, let’s enhance the TaskController by adding the following code:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
use App\Models\Task; // Importing the Task model
use App\Http\Requests\Task\StoreRequest; // Importing the StoreRequest form request
use App\Http\Requests\Task\UpdateRequest; // Importing the UpdateRequest form request

class TaskController extends Controller
{
    /**
     * Display a listing of the resource.
     */
    public function index(): Response
    {
        // Retrieve uncompleted and completed tasks from the database and pass them to the view
        return response()->view('tasks.index', [
            'unCompletedTasks' => Task::where('is_completed', 0)->orderBy('updated_at', 'desc')->get(),
            'completedTasks' => Task::where('is_completed', 1)->orderBy('updated_at', 'desc')->get(),
        ]);
    }

    /**
     * Show the form for creating a new resource.
     */
    public function create(): Response
    {
        // Return the view for creating a new task
        return response()->view('tasks.form');
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(StoreRequest $request): RedirectResponse
    {
        // Validate the incoming request
        $validated = $request->validated();

        // If a file is uploaded, store it in the public storage
        if ($request->hasFile('info_file')) {
            $filePath = Storage::disk('public')->put('files/tasks/info-files', request()->file('info_file'));
            $validated['info_file'] = $filePath;
        }

        // Create a new task with the validated data
        $create = Task::create($validated);

        if($create) {
            // Flash a success notification and redirect to the task index page
            session()->flash('notif.success', 'Task created successfully!');
            return redirect()->route('tasks.index');
        }

        return abort(500); // Return a server error if the task creation fails
    }

    /**
     * Display the specified resource.
     */
    public function show(string $id): Response
    {
        // Retrieve and display the specified task
        return response()->view('tasks.show', [
            'task' => Task::findOrFail($id),
        ]);
    }

    /**
     * Show the form for editing the specified resource.
     */
    public function edit(string $id): Response
    {
        // Retrieve the task with the specified ID and pass it to the view for editing
        return response()->view('tasks.form', [
            'task' => Task::findOrFail($id),
        ]);
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(UpdateRequest $request, string $id): RedirectResponse
    {
        // Find the task with the specified ID
        $task = Task::findOrFail($id);
        
        // Validate the incoming request
        $validated = $request->validated();

        // If an info file is uploaded, update the file path and delete the old file if exists
        if ($request->hasFile('info_file')) {
            if (isset($task->info_file)) {
                Storage::disk('public')->delete($task->info_file);
            }
            $filePath = Storage::disk('public')->put('files/tasks/info-files', request()->file('info_file'), 'public');
            $validated['info_file'] = $filePath;
        }

        // Update the task with the validated data
        $update = $task->update($validated);

        if($update) {
            // Flash a success notification and redirect to the task index page
            session()->flash('notif.success', 'Task updated successfully!');
            return redirect()->route('tasks.index');
        }

        return abort(500); // Return a server error if the task update fails
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy(string $id): RedirectResponse
    {
        // Find the task with the specified ID
        $task = Task::findOrFail($id);

        // If an info file exists, delete it from storage
        if (isset($task->info_file)) {
            Storage::disk('public')->delete($task->info_file);
        }
        
        // Delete the task
        $delete = $task->delete($id);

        if($delete) {
            // Flash a success notification and redirect to the task index page
            session()->flash('notif.success', 'Task deleted successfully!');
            return redirect()->route('tasks.index');
        }

        return abort(500); // Return a server error if the task deletion fails
    }

    /**
     * Mark the specified task as completed.
     */
    public function markCompleted(string $id): RedirectResponse
    {
        // Find the task with the specified ID and update its completion status
        $task = Task::findOrFail($id);
        $isCompleted = $task->update(['is_completed' => 1]);

        if($isCompleted) {
            // Flash a success notification and redirect to the task index page
            session()->flash('notif.success', 'Task marked as completed!');
            return redirect()->route('tasks.index');
        }

        return abort(500); // Return a server error if updating the task fails
    }

    /**
     * Mark the specified task as uncompleted.
     */
    public function markUncompleted(string $id): RedirectResponse
    {
        // Find the task with the specified ID and update its completion status
        $task = Task::findOrFail($id);
        $isCompleted = $task->update(['is_completed' => 0]);

        if($isCompleted) {
            // Flash a success notification and redirect to the task index page
            session()->flash('notif.success', 'Task marked as uncompleted!');
            return redirect()->route('tasks.index');
        }

        return abort(500); // Return a server error if updating the task fails
    }
}

Step 9: Create Form Request for store() method

We can run the following command to create a form request for store():

php artisan make:request Task/StoreRequest

And then add the following code to the class:

// app\Http\Requests\Task\StoreRequest.php
    public function authorize(): bool
    {
        // dont' forget to set this as true
        return true;
    }

    public function rules(): array
    {
        // make all of the fields required, set info file to accept only files
        return [
            'content' => 'required|string|min:3|max:255', // minimum length is 3 characters, maximum length is 255 characters
            'info_file' => 'nullable|file|max:1024|mimes:pdf,docx,doc,xlsx,xls', // optional, file only, max size is 1024 KB, with some allowed mime types
        ];
    }

Step 10: Create Form Request for update() method

We can run the following command to create a form request for update():

php artisan make:request Task/UpdateRequest

The code for this class is similar to the code for StoreRequest:

// app\Http\Requests\Task\UpdateRequest.php
    public function authorize(): bool
    {
        // dont' forget to set this as true
        return true;
    }

    public function rules(): array
    {
        return [
            'content' => 'required|string|min:3|max:255',
            'info_file' => 'nullable|file|max:1024|mimes:pdf,docx,doc,xlsx,xls',
        ];
    }

Step 11: Creating Task Model

To facilitate communication with the database, let’s create a model named Task. Run the following command in your terminal:

php artisan make:model Task

This command generates a new model file named Task.php within the app/Models directory.

Step 12: Add $fillable to Task Model

In the app/Models/Task.php file, add the fillable property to specify which fields can be mass-assigned. Here’s an example:

class Task extends Model
{
    use HasFactory;

    protected $fillable = [
        'content',
        'info_file',
        'is_completed',
    ];
}

To ensure that files stored in the public disk are accessible from the web, we need to create a symbolic link from the public/storage folder to the storage/app/public folder. Since the public disk uses the local driver by default, no changes are required to the public storage configuration.

To create the symbolic link, execute the following command in your terminal:

php artisan storage:link

This command establishes a symbolic link between the public/storage folder and the storage/app/public folder.

Read also:

Step 14: Setting Up the Layout

In order to handle flash notifications for success messages, we’ll update the header section inside our layout’s body. Replace the existing code with the following:

<!-- resources\views\layouts\app.blade.php -->

<!-- Page Heading -->
@if (isset($header))
    <header class="bg-white shadow">
        <div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
            {{ $header }}

            {{-- check if there is a notif.success flash session --}}
            @if (Session::has('notif.success'))
            <div class="bg-blue-300 mt-2 p-4">
                {{-- if it's there then print the notification --}}
                <span class="text-white">{{ Session::get('notif.success') }}</span>
            </div>
            @endif
        </div>
    </header>
@endif

Step 15: Create Index View

Create a new file resources/views/tasks/index.blade.php with the following contents:

{{-- we are using AppLayout Component located in app\View\Components\AppLayout.php which use resources\views\layouts\app.blade.php view --}}
<x-app-layout>
    <!-- Define a slot named "header" -->
    <x-slot name="header">
        <!-- Flex container with space between elements -->
        <div class="flex justify-between">
            <!-- Title for the page -->
            <h2 class="font-semibold text-xl text-gray-800 leading-tight">
                {{ 'Tasks' }} <!-- Static title -->
            </h2>
            <!-- Link to add a new task -->
            <a href="{{ route('tasks.create') }}" class="bg-blue-500 text-white px-4 py-2 rounded-md">ADD</a>
        </div>
    </x-slot>

    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8 mb-4">
            <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
                <div class="p-6 text-gray-900">
                     <!-- Title for uncompleted tasks -->
                    <h3 class="font-semibold text-lg text-gray-800 leading-tight mb-4">Uncompleted</h3>
                    <!-- Table to display uncompleted tasks -->
                    <table class="border-collapse table-auto w-full text-sm">
                        <thead>
                            <tr>
                                <!-- Table header for task and action -->
                                <th class="border-b font-medium p-4 pl-8 pt-0 pb-3 text-slate-400 text-left">Task</th>
                                <th class="border-b font-medium p-4 pl-8 pt-0 pb-3 text-slate-400 text-left">Action</th>
                            </tr>
                        </thead>
                        <tbody class="bg-white">
                            {{-- Loop through uncompleted tasks --}}
                            @forelse ($unCompletedTasks as $task)
                            <tr>
                                <!-- Display task content -->
                                <td class="border-b border-slate-100 dark:border-slate-700 p-4 pl-8 text-slate-500 dark:text-slate-400">
                                    <!-- Checkbox to mark task as completed -->
                                    <a class="mr-1 text-lg" href="{{ route('tasks.mark-completed', $task->id) }}">
                                        🔲
                                    </a>
                                    <!-- Task content -->
                                    <span>
                                        {{ $task->content }}
                                    </span>
                                    <!-- Display info file if exists -->
                                    @isset ($task->info_file)
                                    <span>
                                        <small> | <a href="{{ Storage::url($task->info_file) }}">File</a></small>
                                    </span>
                                    @endisset
                                    <!-- Display last update time -->
                                    <span>
                                        <small>{{ ' | ' . $task->updated_at->diffForHumans() }}</small>
                                    </span>
                                </td>
                                <!-- Actions for the task -->
                                <td class="border-b border-slate-100 dark:border-slate-700 p-4 pl-8 text-slate-500 dark:text-slate-400">
                                    <!-- Link to edit task -->
                                    <a href="{{ route('tasks.edit', $task->id) }}" class="border border-yellow-500 hover:bg-yellow-500 hover:text-white px-4 py-2 rounded-md">EDIT</a>
                                    <!-- Form to delete task -->
                                    <form method="post" action="{{ route('tasks.destroy', $task->id) }}" class="inline">
                                        @csrf
                                        @method('delete')
                                        <button type="submit" class="border border-red-500 hover:bg-red-500 hover:text-white px-4 py-2 rounded-md h-[35px] relative top-[1px]">DELETE</button>
                                    </form>
                                </td>
                            </tr>
                            @empty
                            <!-- Display message if no uncompleted tasks -->
                            <tr>
                                <td class="border-b border-slate-100 dark:border-slate-700 p-4 pl-8 text-slate-500 dark:text-slate-400" colspan="2">
                                    No data can be shown.
                                </td>
                            </tr>
                            @endforelse
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
        <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 text-gray-900">
                     <!-- Title for completed tasks -->
                    <h3 class="font-semibold text-lg text-gray-800 leading-tight mb-4">Completed</h3>
                    <!-- Table to display completed tasks -->
                    <table class="border-collapse table-auto w-full text-sm">
                        <thead>
                            <tr>
                                <th class="border-b font-medium p-4 pl-8 pt-0 pb-3 text-slate-400 text-left">Task</th>
                            </tr>
                        </thead>
                        <tbody class="bg-white">
                            {{-- populate our task data --}}
                            @forelse ($completedTasks as $task)
                            <tr>
                                <td class="border-b border-slate-100 dark:border-slate-700 p-4 pl-8 text-slate-500 dark:text-slate-400 justify-center items-center">
                                    <a class="mr-1 text-lg" href="{{ route('tasks.mark-uncompleted', $task->id) }}">
                                        ✅
                                    </a>
                                    <span>
                                        {{ $task->content }}
                                    </span>
                                    @isset ($task->info_file)
                                    <span>
                                        <small> | <a href="{{ Storage::url($task->info_file) }}">File</a></small>
                                    </span>
                                    @endisset
                                    <span>
                                        <small>{{ ' | ' . $task->updated_at->diffForHumans() }}</small>
                                    </span>
                                </td>
                            </tr>
                            @empty
                            <tr>
                                <td class="border-b border-slate-100 dark:border-slate-700 p-4 pl-8 text-slate-500 dark:text-slate-400" colspan="2">
                                    No data can be shown.
                                </td>
                            </tr>
                            @endforelse
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
    </div>
</x-app-layout>

Step 16: Creating Form View

Create a new file resources/views/tasks/form.blade.php. Here we will use some already defined components we get from Laravel Breeze. Let’s write the following code:

<x-app-layout>
    {{-- Header section with 'Edit' or 'Create' depending on the existence of $task --}}
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            {{-- Use 'Edit' for edit mode and 'Create' for create mode --}}
            {{ isset($task) ? 'Edit' : 'Create' }} 
        </h2>
    </x-slot>

    <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 text-gray-900">
                    {{-- Form for task creation/updation with file upload --}}
                    <form method="post" action="{{ isset($task) ? route('tasks.update', $task->id) : route('tasks.store') }}" class="mt-6 space-y-6" enctype="multipart/form-data">
                        @csrf {{-- CSRF protection --}}
                        {{-- Use PUT method for edit mode --}}
                        @isset($task)
                            @method('put')
                        @endisset

                        {{-- Task Content Input --}}
                        <div>
                            <x-input-label for="content" value="Task" /> {{-- Label for task content --}}
                            <x-text-input id="content" name="content" type="text" class="mt-1 block w-full" :value="$task->content ?? old('content')" required autofocus /> {{-- Input field for task content --}}
                            <x-input-error class="mt-2" :messages="$errors->get('content')" /> {{-- Display validation errors for task content --}}
                        </div>

                        {{-- Info File Input --}}
                        <div>
                            <x-input-label for="info_file" value="Info File" /> {{-- Label for info file --}}
                            <label class="block mt-2">
                                <span class="sr-only">Choose file</span> {{-- Screen reader text --}}
                                <input type="file" id="info_file" name="info_file" accept=".pdf,.docx,.doc,.xlsx,.xls" class="block w-full text-sm text-slate-500
                                    file:mr-4 file:py-2 file:px-4
                                    file:rounded-full file:border-0
                                    file:text-sm file:font-semibold
                                    file:bg-violet-50 file:text-violet-700
                                    hover:file:bg-violet-100
                                " /> {{-- File input field --}}
                            </label>
                            {{-- Display existing file if it exists --}}
                            @isset($task->info_file)
                                <div class="shrink-0 my-2">
                                    <span>File Exists: </span> {{-- Display text indicating file existence --}}
                                    <a href="{{ Storage::url($task->info_file) }}">{{ explode('/', $task->info_file)[3] }}</a> {{-- Display file name with link --}}
                                </div>
                            @endisset
                            <x-input-error class="mt-2" :messages="$errors->get('info_file')" /> {{-- Display validation errors for info file --}}
                        </div>

                        {{-- Save and Cancel Buttons --}}
                        <div class="flex items-center gap-2">
                            <x-primary-button>{{ __('Save') }}</x-primary-button> {{-- Primary button for saving --}}
                            <x-secondary-button onclick="history.back()">{{ __('Cancel') }}</x-secondary-button> {{-- Secondary button for canceling --}}
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</x-app-layout>

Step 17: Update Navigation Component

To update the navigation component, use the following code:

{{-- resources\views\layouts\navigation.blade.php --}}

<nav x-data="{ open: false }" class="bg-white border-b border-gray-100">
    <!-- Primary Navigation Menu -->
    <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
        <div class="flex justify-between h-16">
            <div class="flex">
                <!-- Logo -->
                <div class="shrink-0 flex items-center">
                    <a href="{{ route('dashboard') }}">
                        <x-application-logo class="block h-9 w-auto fill-current text-gray-800" />
                    </a>
                </div>

                <!-- Navigation Links -->
                <div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
                    <x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
                        {{ __('Dashboard') }}
                    </x-nav-link>
                    <!-- add this -->
                    <x-nav-link :href="route('tasks.index')" :active="request()->routeIs('tasks.index')">
                        {{'Tasks' }}
                    </x-nav-link>
                </div>
            </div>

            <!-- Settings Dropdown -->
            <div class="hidden sm:flex sm:items-center sm:ml-6">
                <x-dropdown align="right" width="48">
                    <x-slot name="trigger">
                        <button class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150">
                            <div>{{ Auth::user()->name }}</div>

                            <div class="ml-1">
                                <svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
                                    <path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
                                </svg>
                            </div>
                        </button>
                    </x-slot>

                    <x-slot name="content">
                        <x-dropdown-link :href="route('profile.edit')">
                            {{ __('Profile') }}
                        </x-dropdown-link>

                        <!-- Authentication -->
                        <form method="POST" action="{{ route('logout') }}">
                            @csrf

                            <x-dropdown-link :href="route('logout')" onclick="event.preventDefault();
                                                this.closest('form').submit();">
                                {{ __('Log Out') }}
                            </x-dropdown-link>
                        </form>
                    </x-slot>
                </x-dropdown>
            </div>

            <!-- Hamburger -->
            <div class="-mr-2 flex items-center sm:hidden">
                <button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out">
                    <svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
                        <path :class="{'hidden': open, 'inline-flex': ! open }" class="inline-flex" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
                        <path :class="{'hidden': ! open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
                    </svg>
                </button>
            </div>
        </div>
    </div>

    <!-- Responsive Navigation Menu -->
    <div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
        <div class="pt-2 pb-3 space-y-1">
            <x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
                {{ __('Dashboard') }}
            </x-responsive-nav-link>
            <!-- add this -->
            <x-responsive-nav-link :href="route('tasks.index')" :active="request()->routeIs('tasks.index')">
                {{ 'Tasks' }}
            </x-responsive-nav-link>
        </div>

        <!-- Responsive Settings Options -->
        <div class="pt-4 pb-1 border-t border-gray-200">
            <div class="px-4">
                <div class="font-medium text-base text-gray-800">{{ Auth::user()->name }}</div>
                <div class="font-medium text-sm text-gray-500">{{ Auth::user()->email }}</div>
            </div>

            <div class="mt-3 space-y-1">
                <x-responsive-nav-link :href="route('profile.edit')">
                    {{ __('Profile') }}
                </x-responsive-nav-link>

                <!-- Authentication -->
                <form method="POST" action="{{ route('logout') }}">
                    @csrf

                    <x-responsive-nav-link :href="route('logout')" onclick="event.preventDefault();
                                        this.closest('form').submit();">
                        {{ __('Log Out') }}
                    </x-responsive-nav-link>
                </form>
            </div>
        </div>
    </div>
</nav>

Here we add our Tasks link to the desktop and mobile version navigation.

Read also:

Step 18: Testing Our Laravel App

Before proceeding, ensure you’ve installed all dependencies by running npm install after installing Laravel Breeze. If you’ve already done this, you can skip this step. Next, compile your frontend assets using Vite by running npm run dev.

After setting up your frontend dependencies, start your local development server with the following command:

php artisan serve

Welcome Page

You will see the following Laravel 11 Welcome Page. You will also see Log in and Register link at the navbar because we already installed Laravel Breeze.

welcome.png

Proceed to register yourself and navigate to the Dashboard Page.

register.png

Dashboard Page

Here is how the Dashboard page looks like. You’ll notice an additional link in the navbar labeled Tasks.

dashboard.png

Task List Page

The /tasks page displays a table listing tasks.

tasks-index.png

Create and Edit Task Page

Here is how the Create Task page should look like. This is also similar to the Edit Task page.

create-task.png

Notification Display

Upon successfully creating, editing, or deleting a task, a notification will appear.

create-update-notif.png

Mark Task as Completed

To mark a task as complete, click the 🔲 emoji (white square). Tasks marked complete will appear in the Completed task list.

mark-completed.png

To uncomplete a task, click the ✅ emoji (green check mark), and it will move to the Uncompleted section.

You can click the “File” to view or download the info file.

With these visual cues, you can confirm the successful implementation of CRUD operations and file uploads in your Laravel 11 application with Laravel Breeze.

Conclusion

This tutorial has provided a comprehensive guide on implementing CRUD functionality and file uploads in a Laravel 11 application using Laravel Breeze. We began by setting up our Laravel environment, including installation and database initialization.

We then integrated Laravel Breeze for authentication scaffolding, enabling seamless user registration and authentication. Following this, we created migrations and models for our tasks, facilitating data management within the application.

Through the creation of routes and controllers, we established endpoints for handling CRUD operations, ensuring efficient manipulation of task data. Additionally, we leveraged Laravel’s form request validation for secure and validated data submission.

Next, we’ve leveraged Laravel Blade components and layouts for a consistent and structured look. We also explored implementing file uploads, which allow users to attach files to their tasks.

With the application setup complete, we tested our Laravel app, examining various functionalities such as task creation, editing, deletion, and marking tasks as completed. Additionally, we observed the display of notifications to provide feedback on user actions.

By following these steps, developers can build robust web applications with Laravel 11, incorporating essential features like CRUD operations and file uploads seamlessly. With Laravel’s intuitive syntax and Laravel Breeze’s streamlined authentication, creating powerful applications has never been easier.

💻 The repository for this example can be found at fajarwz/blog-laravel11-crud-file.

Fajarwz's photo Fajar Windhu Zulfikar

I'm a full-stack web developer who loves to share my software engineering journey and build software solutions to help businesses succeed.

Email me
Ads
  • Full-Stack Laravel: Forum Web App (Complete Guide 2024)
  • Flexible and powerful review system for Laravel, let any model review and be reviewed.

Share

Subscribe

Sign up for my email newsletter and never miss a beat in the world of web development. Stay up-to-date on the latest trends, techniques, and tools. Don't miss out on valuable insights. Subscribe now!

Comments

comments powered by Disqus