Pavel Zaněk PavelZanek.com
Select language
Account

Example: How to create CRUD operations in Laravel 10 (including tests)

A tutorial that covers the process of creating CRUD operations in Laravel. The manual contains all the key steps, from creating migrations and models, to defining actions and validation requests, to creating templates, factories and tests.

Published at 2023-06-24 by Pavel Zaněk

Estimated Reading Time: 27 minutes

Example: How to create CRUD operations in Laravel 10 (including tests)

Table of Contents

Welcome to the beginner's guide for creating CRUD operations in Laravel. Laravel is one of the most popular PHP frameworks, and CRUD (Create, Read, Update, Delete) is the cornerstone of most web applications. In this guide, you will learn how to create basic Laravel CRUD operations/applications step by step. Whether you're a Laravel newbie or an experienced developer looking for a refresher, this guide will provide you with all the necessary information for successfully creating CRUD operations, including subsequent testing.

What is CRUD?

CRUD is a fundamental concept that is necessary for managing data in any web application. CRUD is an acronym for Create, Read, Update, and Delete. These four operations form the basic functions needed to interact with a database.

In Laravel, working with CRUD is made easier thanks to its ORM (Object-Relational Mapping) called Eloquent. Eloquent allows developers to work with database tables as objects and provides a simple interface (you could say API) for common database operations.

Creating (Create) in Laravel is done using the create() method, which creates a new record in the database. Reading (Read) is done using methods like get(), all(), find(), etc., which load records from the database. Updating (Update) is done using the update() method, which modifies an existing record in the database. And finally, deleting (Delete) is done using the delete() method, which removes a record from the database.

Introduction to our demonstration

Imagine that we want to create an ExampleItem model, which will contain these attributes:

  • title (string)
  • body (string / text)
  • is_active (boolean)
  • type (string)

Prerequisite is to have the Laravel framework installed. Since I tested this code on an already modified application, you will also find the localization of the application and the restriction of access to only users with the superadmin role.

The file structure is adapted for the demonstration, so in addition to the file names, I also specify the folder in which the file is located. However, the usage is entirely up to you, you can adjust the structure according to your needs.

The Pest package is then used as part of the testing.

Creating a migration

Migrations in Laravel are like a versioning system for your database. They allow you to change the database structure using PHP code instead of manually writing SQL. Migrations are also a great tool for sharing and updating the database structure among team members.

Creating a migration in Laravel is simple thanks to the Artisan command. The "php artisan make:migration" command creates a new migration file in the "database/migrations" directory. The file name contains the date and time of creation, which allows Laravel to determine in what order the migrations should be run.

Each migration file contains two methods: "up" and "down". The "up" method is used to add new tables, columns, or indexes to your database. The "down" method should perform the opposite operations to those performed in the "up" method.

After creating the migration, you can run the migration using the "php artisan migrate" command. This command runs all migrations that have not yet been run in the order they were created.

Migration (database/migrations):

<?php

use App\Enums\Examples\ExampleItemType;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('example_items', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->mediumText('body')->nullable();
            $table->boolean('is_active')->default(false);
            $table->string('type', 8)->default(ExampleItemType::TYPE1->value);
            $table->timestamps();
            $table->softDeletes();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('example_items');
    }
};

Creating a model

Models are one of the key aspects of the framework. They are representations of database tables and allow you to work with database records as objects. This means that you can perform CRUD operations on your models without having to write SQL.

Creating a model in Laravel is simple. You use the Artisan command "php artisan make:model", which creates a new model in the "app/Models" directory. The model name should be in singular and start with a capital letter.

Each model in Laravel extends the "Illuminate\Database\Eloquent\Model" class and contains methods that allow interaction with the database. You can also define your own methods and properties on your models.

Models also contain so-called Eloquent relationships, which allow you to define relationships between different models. For example, if you have a "User" model and a "Post"model, you can define a relationship that says that one user can have many posts.

"ExampleItem" Model (app\Models\Examples):

<?php

namespace App\Models\Examples;

use App\Enums\Examples\ExampleItemType;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class ExampleItem extends Model
{
    use HasFactory;
    use SoftDeletes;

    protected $fillable = [
        'title',
        'body',
        'is_active',
        'type',
    ];

    protected $casts = [
        'is_active' => 'boolean',
        'type' => ExampleItemType::class,
    ];
}

Creating actions

In Laravel, you can define actions as separate classes that represent individual operations that your application can perform. This is an alternative approach to the traditional model where actions are defined as methods in controllers.

Creating an action as a separate file in Laravel is simple. First, you need to create a new class in the "app/Actions" directory. The class, for example, can have an "execute()" method that performs the desired operation.

This approach has several advantages. One of them is that your actions are well isolated, easily extendable, and testable. Each action has a clearly defined responsibility and it's easy to test it independently of other parts of your application.

"CreateExampleItemAction" Action (app\Actions\Examples\ExampleItems):

<?php

namespace App\Actions\Examples\ExampleItems;

use App\Models\Examples\ExampleItem;

class CreateExampleItemAction
{
    public function execute(array $validatedData): ExampleItem
    {
        return ExampleItem::create($validatedData);
    }
}

"UpdateExampleItemAction" Action (app\Actions\Examples\ExampleItems):

<?php

namespace App\Actions\Examples\ExampleItems;

use App\Models\Examples\ExampleItem;

class UpdateExampleItemAction
{
    public function execute(ExampleItem $exampleItem, array $validatedData): ExampleItem
    {
        $exampleItem->update($validatedData);

        return $exampleItem;
    }
}

"RemoveExampleItemAction" Action (app\Actions\Examples\ExampleItems):

<?php

namespace App\Actions\Examples\ExampleItems;

use App\Models\Examples\ExampleItem;

class RemoveExampleItemAction
{
    public function execute(ExampleItem $exampleItem): void
    {
        $exampleItem->delete();
    }
}

Creating enum

Creating Enums in Laravel is a powerful way to define a type of variable that allows for many different, but specific values. This feature is particularly useful when you want to limit the possibilities of a variable's value, ensuring that it only holds a value from a specific set. In Laravel, Enums are not built-in, but they can be easily implemented using PHP classes.

Creating Enums in Laravel can be a great way to make your code more readable and maintainable, especially when dealing with variables that should only have a specific set of values. They can be used in various parts of your Laravel application, such as in models, controllers, and even in your database migrations.

In addition to making your code cleaner and easier to understand, Enums also help to reduce errors in your code. By limiting the possible values of a variable, you can ensure that it only ever holds a value that it's supposed to, reducing the likelihood of unexpected values causing bugs in your code.

Remember, Enums are just one of the many advanced features that Laravel offers to make your PHP development easier and more efficient. Whether you're building a small personal project or a large enterprise application, Laravel has the tools and features you need to build robust, scalable, and maintainable PHP applications.

"ExampleItemType" Enum (app\Enums\Examples):

<?php

namespace App\Enums\Examples;

use Illuminate\Support\Collection;

enum ExampleItemType: string
{
    case TYPE1 = 'Type 1';
    case TYPE2 = 'Type 2';

    public static function all(): Collection
    {
        return collect(ExampleItemType::cases())->map(
            fn(ExampleItemType $code) => $code->details()
        );
    }

    public function details(): array
    {
        return match($this) 
        {
            self::TYPE1 => [
                'name'  => 'Type Name 1',
                'value' => 'Type 1',
            ],

            self::TYPE2 => [
                'name'  => 'Type Name 2',
                'value' => 'Type 2',
            ],
        };
    }
}

Creating validation / validation requests

Validation requests are special classes that serve to validate input data before they are processed by your application. Using validation requests, you can ensure that the data your application receives is always in the expected format and meets all the rules you have set.

Creating a validation request in Laravel is simple. First, you need to create a validation class using the Artisan command "php artisan make:request", which creates a new validation request in the "app/Http/Requests" directory.

The validation class contains two methods: "authorize()" and "rules()". The "authorize()" method determines whether the user is authorized to make the request. The "rules()" method then returns an array of validation rules that should be applied to the input data.

"StoreXYZRequest" and "UpdateXYZRequest" are two common types of validation requests in Laravel. "StoreXYZRequest" is used when creating a new record and "UpdateXYZRequest" is used when updating an existing record. However, you can, for example, create only one validation request that will be used when creating and editing a record - which is handy if the validation conditions for creating and editing are the same.

"StoreExampleItemRequest" Validation Request (app\Http\Requests\Examples\ExampleItems):

<?php

namespace App\Http\Requests\Examples\ExampleItems;

use App\Enums\Examples\ExampleItemType;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Enum;

class StoreExampleItemRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
     */
    public function rules(): array
    {
        return [
            'title' => 'required|string|max:255',
            'body' => 'nullable|string',
            'is_active' => 'required|boolean',
            'type' => [
                'required',
                'string',
                'max:8',
                new Enum(ExampleItemType::class),
            ],
        ];
    }

    /**
     * Prepare inputs for validation.
     *
     * @return void
     */
    protected function prepareForValidation()
    {
        $this->merge([
            'is_active' => $this->has('is_active') ? true : false,
        ]);
    }
}

"UpdateExampleItemRequest" Validation Request (app\Http\Requests\Examples\ExampleItems):

<?php

namespace App\Http\Requests\Examples\ExampleItems;

use App\Enums\Examples\ExampleItemType;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Enum;

class UpdateExampleItemRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
     */
    public function rules(): array
    {
        return [
            'title' => 'required|string|max:255',
            'body' => 'nullable|string',
            'is_active' => 'required|boolean',
            'type' => [
                'required',
                'string',
                'max:8',
                new Enum(ExampleItemType::class),
            ],
        ];
    }

    /**
     * Prepare inputs for validation.
     *
     * @return void
     */
    protected function prepareForValidation()
    {
        $this->merge([
            'is_active' => $this->has('is_active') ? true : false,
        ]);
    }
}

Creating a controller

Controllers are a key part of any application built on this framework. They are classes that handle all HTTP requests that your application receives and control how your application should respond.

Creating a controller in Laravel is simple again thanks to the Artisan tool, which is part of Laravel. The "php artisan make:controller" command creates a new controller in the "app/Http/Controllers" directory. The controller name should be in singular and start with a capital letter.

Controllers in Laravel can either be simple, which are controllers with one action (uses the "__invoke" method), or resource, which are controllers that handle a whole set of CRUD operations. Resource controllers are ideal for working with database records, such as users, blog posts, or any other entities in your application. Of course, a controller can also be created according to your ideas, so it does not have to be either with one method, nor a resource one.

"ExampleItemController" Controller (app\Http\Controllers\Examples):

<?php

namespace App\Http\Controllers\Examples;

use App\Actions\Examples\ExampleItems\CreateExampleItemAction;
use App\Actions\Examples\ExampleItems\RemoveExampleItemAction;
use App\Actions\Examples\ExampleItems\UpdateExampleItemAction;
use App\Enums\Examples\ExampleItemType;
use App\Http\Controllers\Controller;
use App\Http\Requests\Examples\ExampleItems\StoreExampleItemRequest;
use App\Http\Requests\Examples\ExampleItems\UpdateExampleItemRequest;
use App\Models\Examples\ExampleItem;

class ExampleItemController extends Controller
{
    public function index()
    {
        $exampleItems = ExampleItem::orderBy('created_at', 'desc')->paginate(20);

        return view('examples.example-items.index', [
            'exampleItems' => $exampleItems,
        ]);
    }

    public function create()
    {
        return view('examples.example-items.create', [
            'exampleItemTypes' => ExampleItemType::all()
        ]);
    }

    public function store(StoreExampleItemRequest $request)
    {
        (new CreateExampleItemAction())->execute($request->validated());

        return redirect()->route('example-items.index')->with([
            'flashType' => 'success',
            'flashMessage' => __('Example Item was successfully created.'),
        ]);
    }

    public function show(ExampleItem $exampleItem)
    {
        abort_if(!$exampleItem->is_active, 404, "Example Item is not active.");

        return view('examples.example-items.show', [
            'exampleItem' => $exampleItem
        ]);
    }

    public function edit(ExampleItem $exampleItem)
    {
        return view('examples.example-items.edit', [
            'exampleItem' => $exampleItem,
            'exampleItemTypes' => ExampleItemType::all()
        ]);
    }

    public function update(UpdateExampleItemRequest $request, ExampleItem $exampleItem)
    {
        (new UpdateExampleItemAction())->execute($exampleItem, $request->validated());

        return redirect()->route('example-items.index')->with([
            'flashType' => 'success',
            'flashMessage' => __('Example Item was successfully updated.'),
        ]);
    }

    public function destroy(ExampleItem $exampleItem)
    {
        (new RemoveExampleItemAction())->execute($exampleItem);

        return redirect()->route('example-items.index')->with([
            'flashType' => 'success',
            'flashMessage' => __('Example Item was successfully removed.'),
        ]);
    }
}

Creating a template

Templates are a key tool for creating dynamic web pages. Laravel uses the "Blade" template engine, which allows you to insert PHP code directly into HTML templates.

Creating a template in Laravel is simple. First, you need to create a template file in the "resources/views" directory. Template files have a ".blade.php" extension.

Templates can contain any valid HTML code as well as special Blade directives for inserting PHP code. For example, you can use the directive "{{ $variable }}" to output the value of a variable, or directives @if, @foreach, and others to control the flow of the application.

In addition, you can use the template inheritance system, which allows you to define "base" templates with a general page structure and then create "inherited" templates that extend this structure and add specific content.

"Index" Template (resources/views/examples/example-items):

<x-app-layout>
    <x-slot name="header">

        <div class="flex justify-between items-center">
            <div>
                <h1 class="font-semibold text-xl text-neutral-800 dark:text-neutral-50 leading-tight">
                    {{ __('Example Items') }}
                </h1>
            </div>
            <div class="ml-2">
                <a href="{{ route('example-items.create') }}"
                    class="transition-all ease-in-out duration-500 px-4 py-2 rounded-md bg-blue-600 text-blue-100 hover:bg-blue-700">
                    {{ __('Create') }}
                </a>
            </div>
        </div>
    </x-slot>

    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            <div class="bg-white rounded-lg shadow dark:bg-gray-800 overflow-hidden shadow-xl sm:rounded-lg">

                <!-- Index Example Items -->
                <div class="relative overflow-x-auto">
                    <table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
                        <thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
                            <tr>
                                <th scope="col" class="px-6 py-3">
                                    {{ __('ID') }}
                                </th>
                                <th scope="col" class="px-6 py-3">
                                    {{ __('Title') }}
                                </th>
                                <th scope="col" class="px-6 py-3">
                                    {{ __('Is active') }}
                                </th>
                                <th scope="col" class="px-6 py-3">
                                    {{ __('Type') }}
                                </th>
                                <th scope="col" class="px-6 py-3">
                                    {{ __('Created at') }}
                                </th>
                                <th scope="col" class="px-6 py-3">
                                    {{ __('Updated at') }}
                                </th>
                                <th scope="col" class="px-6 py-3">
                                    {{ __('Action') }}
                                </th>
                            </tr>
                        </thead>
                        <tbody>
                            @foreach ($exampleItems as $exampleItem)
                                <tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
                                    <th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
                                        {{ $exampleItem->id }}
                                    </th>
                                    <td class="px-6 py-4">
                                        <a href="{{ route('example-items.show', $exampleItem) }}" class="transition-all ease-in-out duration-500 text-blue-700 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300">
                                            {{ $exampleItem->title }}
                                        </a>
                                    </td>
                                    <td class="px-6 py-4">
                                        {{ $exampleItem->is_active ? __('Yes') : __('No') }}
                                    </td>
                                    <td class="px-6 py-4">
                                        {{ $exampleItem->type->value }}
                                    </td>
                                    <td class="px-6 py-4">
                                        {{ $exampleItem->created_at->diffForHumans() }}
                                    </td>
                                    <td class="px-6 py-4">
                                        {{ $exampleItem->updated_at?->diffForHumans() }}
                                    </td>
                                    <td class="px-6 py-4">
                                        <div class="flex justify-start items-center gap-2">
                                            <a href="{{ route('example-items.edit', $exampleItem->id) }}"
                                                class="transition-all ease-in-out duration-500 text-gray-400 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300">
                                                <i class="fas fa-edit"></i>
                                            </a>
                                            <form action="{{ route('example-items.destroy', $exampleItem->id) }}" method="POST" onsubmit="return confirm('{{ __('Are You Sure?') }}');">
                                                <input type="hidden" name="_method" value="DELETE">
                                                <input type="hidden" name="_token" value="{{ csrf_token() }}">
                                                <button type="submit" class="flex items-center">
                                                    <i class="transition-all ease-in-out duration-500 fas fa-trash text-red-600 hover:text-red-900 dark:hover:text-red-400"></i>
                                                </button>
                                            </form>
                                        </div>
                                    </td>
                                </tr>
                            @endforeach
                        </tbody>
                    </table>
                </div>

                <div class="mt-5 mb-2 mx-3">
                    {!! $exampleItems->withQueryString()->links() !!}
                </div>

            </div>
        </div>
    </div>
</x-app-layout>

"Create" Template (resources/views/examples/example-items):

<x-app-layout>
    <x-slot name="header">
        <h1 class="font-semibold text-xl text-neutral-800 dark:text-neutral-50 leading-tight">
            {{ __('Create Example Item') }}
        </h1>
    </x-slot>

    <!-- Create Example Item -->
    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            <div class="bg-white rounded-lg shadow dark:bg-gray-800 overflow-hidden shadow-xl sm:rounded-lg">

                <div class="w-full px-6 py-4">
                    <form method="POST" action="{{ route('example-items.store') }}">
                        @csrf

                        <!-- Title -->
                        <div>
                            <label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white" for="title">
                                {{ __('Title') }}
                            </label>

                            <input
                                id="title"
                                class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5  dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
                                type="text" name="title" value="{{old('title')}}">

                            @error('title')
                                <p class="mt-2 text-sm text-red-600 dark:text-red-500">{{ $message }}</p>
                            @enderror
                        </div>

                        <!-- Body -->
                        <div class="mt-4">
                            <label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white" for="body">
                                {{ __('Body') }}
                            </label>

                            <textarea name="body"
                                id="body"
                                class="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
                                rows="4">{{old('body')}}</textarea>

                            @error('body')
                                <p class="mt-2 text-sm text-red-600 dark:text-red-500">{{ $message }}</p>
                            @enderror
                        </div>

                        <!-- Is active -->
                        <div class="mt-4">
                            <div class="flex items-center mb-4">
                                <input name="is_active" id="is-active" type="checkbox" value="" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" >
                                <label for="is-active" class="ml-2 text-sm font-medium text-gray-900 dark:text-gray-300">{{ __('Active') }}</label>
                            </div>

                            @error('is_active')
                                <p class="mt-2 text-sm text-red-600 dark:text-red-500">{{ $message }}</p>
                            @enderror
                        </div>

                        <!-- Type -->
                        <div class="mt-4">
                            <label for="type" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">{{ __('Select an option') }}</label>
                            <select name="type" id="type" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
                                <option selected>{{ __('Choose a type') }}</option>
                                @foreach ($exampleItemTypes as $exampleItemType)
                                    <option value="{{ $exampleItemType['value'] }}">
                                        {{ $exampleItemType['name'] }}
                                    </option>
                                @endforeach
                            </select>

                            @error('type')
                                <p class="mt-2 text-sm text-red-600 dark:text-red-500">{{ $message }}</p>
                            @enderror
                        </div>

                        <div class="flex items-center justify-start mt-4">
                            <button type="submit"
                                class="transition-all ease-in-out duration-500 inline-flex items-center px-6 py-2 text-sm font-semibold rounded-md text-blue-100 bg-blue-600 hover:bg-blue-700 focus:outline-none focus:border-gray-900 focus:ring ring-gray-300">
                                {{ __('Save') }}
                            </button>
                        </div>

                    </form>
                </div>

            </div>
        </div>
    </div>
</x-app-layout>

"Edit" Template (resources/views/examples/example-items):

<x-app-layout>
    <x-slot name="header">
        <h1 class="font-semibold text-xl text-neutral-800 dark:text-neutral-50 leading-tight">
            {{ __('Edit Example Item') }}
        </h1>
    </x-slot>

    <!-- Edit Example Item -->
    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            <div class="bg-white rounded-lg shadow dark:bg-gray-800 overflow-hidden shadow-xl sm:rounded-lg">

                <div class="w-full px-6 py-4">
                    <form method="POST" action="{{ route('example-items.update', $exampleItem->id) }}">
                        @csrf
                        @method('PUT')

                        <!-- Title -->
                        <div>
                            <label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white" for="title">
                                {{ __('Title') }}
                            </label>

                            <input
                                id="title"
                                class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5  dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
                                type="text" name="title" value="{{ old('title', $exampleItem->title) }}">

                            @error('title')
                                <p class="mt-2 text-sm text-red-600 dark:text-red-500">{{ $message }}</p>
                            @enderror
                        </div>

                        <!-- Body -->
                        <div class="mt-4">
                            <label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white" for="body">
                                {{ __('Body') }}
                            </label>

                            <textarea name="body"
                                id="body"
                                class="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
                                rows="4">{{ old('body', $exampleItem->body) }}</textarea>

                            @error('body')
                                <p class="mt-2 text-sm text-red-600 dark:text-red-500">{{ $message }}</p>
                            @enderror
                        </div>

                        <!-- Is active -->
                        <div class="mt-4">
                            <div class="flex items-center mb-4">
                                <input {{ $exampleItem->is_active ? 'checked' : ''}} name="is_active" id="is-active" type="checkbox" value="{{ $exampleItem->is_active }}" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" >
                                <label for="is-active" class="ml-2 text-sm font-medium text-gray-900 dark:text-gray-300">{{ __('Active') }}</label>
                            </div>

                            @error('is_active')
                                <p class="mt-2 text-sm text-red-600 dark:text-red-500">{{ $message }}</p>
                            @enderror
                        </div>

                        <!-- Type -->
                        <div class="mt-4"><label for="type" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">{{ __('Select an option') }}</label>
                            <select name="type" id="type" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
                                <option selected>{{ __('Choose a type') }}</option>
                                @foreach ($exampleItemTypes as $exampleItemType)
                                    <option value="{{ $exampleItemType['value'] }}" {{ $exampleItemType['value'] == $exampleItem->type->value ? 'selected' : '' }}>
                                        {{ $exampleItemType['name'] }}
                                    </option>
                                @endforeach
                            </select>

                            @error('type')
                                <p class="mt-2 text-sm text-red-600 dark:text-red-500">{{ $message }}</p>
                            @enderror
                        </div>

                        <div class="flex items-center justify-start mt-4">
                            <button type="submit"
                                class="transition-all ease-in-out duration-500 inline-flex items-center px-6 py-2 text-sm font-semibold rounded-md text-blue-100 bg-blue-600 hover:bg-blue-700 focus:outline-none focus:border-gray-900 focus:ring ring-gray-300">
                                {{ __('Update') }}
                            </button>
                        </div>

                    </form>
                </div>

            </div>
        </div>
    </div>
</x-app-layout>

"Show" Template (resources/views/examples/example-items):

<x-app-layout>
    <x-slot name="header">
        <h1 class="font-semibold text-xl text-neutral-800 dark:text-neutral-50 leading-tight">
            {{ $exampleItem->title }}
        </h1>
    </x-slot>

    <!-- Show Example Item -->
    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            <div class="bg-white rounded-lg shadow dark:bg-gray-800 overflow-hidden shadow-xl sm:rounded-lg">

                <div class="sm:p-6 lg:p-8 text-gray-900 dark:text-gray-200">
                    <div class="mb-4">
                        {{ $exampleItem->body }}
                    </div>
                    <div>
                        {{ __('Type') }}: {{ $exampleItem->type->details()['name'] }} ({{ $exampleItem->type->value }})
                    </div>
                </div>

            </div>
        </div>
    </div>
</x-app-layout>

Routing

Routing is one of the most important aspects of any web application, and Laravel provides a powerful and flexible routing system that makes it easy to define routes for your application.

Routes in Laravel are defined in route files, which are located in the "routes" directory of your application. Laravel provides several different route files for different types of routes, including web routes, API routes, and console routes.

Each route is defined using an HTTP method (such as GET, POST, PUT, or DELETE), a path, and an action to be performed when the route is invoked. The action can be either a function (closure function), or a reference to a controller method.

Laravel also supports advanced routing features such as wildcard routing, named routing, middleware routing, parameter constraint routing, and so on.

Routing for "web" (routes):

...
Route::group([
    'prefix' => "{language}", 
    'where' => ['language' => '[a-zA-Z]{2}']
    ], function () {

    // Admin
    Route::group(['middleware' => ['role:superadmin']], function () {
        ...
        Route::resource('/examples/example-items', ExampleItemController::class);
    });

});
...

Creating a factory

Factories are tools that make it easy to generate test data for your database. Factories allow you to define "templates" (not to be confused with blade templates) for creating new instances of your models with random or predefined data.

Creating a factory in Laravel is simple again thanks to Artisan. The "php artisan make:factory" command creates a new factory in the "database/factories" directory. The name of the factory should correspond to the name of the model for which the factory is intended.

Each factory contains a "definition()" method, which returns an array of attributes to be used when creating a new instance of the model. You can use Laravel functions like "faker" to generate random data.

Factories are ideal for creating large amounts of data for testing or for populating your database before development or during development.

"ExampleItemFactory" Factory (database/factories/Examples):

<?php

namespace Database\Factories\Examples;

use App\Enums\Examples\ExampleItemType;
use App\Models\Examples\ExampleItem;
use Illuminate\Database\Eloquent\Factories\Factory;

class ExampleItemFactory extends Factory
{
    protected $model = ExampleItem::class;

    public function definition()
    {
        return [
            'title' => $this->faker->sentence(4),
            'body' => $this->faker->realText(500),
            'is_active' => $this->faker->boolean(),
            'type' => $this->faker->boolean() ? ExampleItemType::TYPE1 : ExampleItemType::TYPE2,
        ];
    }
}

Creating tests

Testing is a fundamental part of developing any application, and Laravel provides excellent tools for automated testing of your code. Laravel allows you to easily create and run tests for your application.

Creating a test in Laravel is simple, as you might expect, thanks to the Artisan tool. The "php artisan make:test" command creates a new test in the "tests/Feature" or "tests/Unit" directory, depending on what type of test you want to create.

Each test is a class that contains one or more test methods. These methods contain assertions that verify that your code is working as it should. Laravel provides a wide range of assertions to verify various aspects of your application, including HTTP responses, database interactions, and much more.

You can run tests using the "php artisan test" command, which runs all tests in your application and prints the results to the console.

Tests for the controller

"ExampleItemControllerTest" Test (tests/Feature/App/Controllers/Examples):

<?php

use App\Enums\Examples\ExampleItemType;
use App\Models\Examples\ExampleItem;
use App\Models\User;
use Spatie\Permission\Models\Role;

beforeEach(function () {
    $user = User::factory()
        ->withPersonalTeam()
        ->create();

    $superadminRole = Role::updateOrCreate(['name' => 'superadmin'], ['name' => 'superadmin']);
    $user->assignRole($superadminRole);

    $this->actingAs($user);
});

test('example item list can be rendered', function () {
    ExampleItem::factory()
        ->count(5)
        ->create();

    $this->get(route('example-items.index', ['language' => 'en']))
        ->assertStatus(200);
});

test('example item create view can be rendered', function () {
    $this->get(route('example-items.create', ['language' => 'en']))
        ->assertStatus(200);
});

test('example item can be created', function () {
    $data = [
        'title' => 'Example Title',
        'body' => 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Phasellus rhoncus. Aenean vel massa quis mauris vehicula lacinia. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur?',
        'is_active' => true,
        'type' => ExampleItemType::TYPE1->value,
    ];

    $this->post(route('example-items.store', ['language' => 'en']), $data)
        ->assertRedirect(route('example-items.index', ['language' => 'en']));

    expect(ExampleItem::first())
        ->title
            ->not->toBeNull()
            ->toEqual($data['title'])
        ->body
            ->not->toBeNull()
            ->toEqual($data['body'])
        ->is_active
            ->not->toBeNull()
            ->toBeTrue()
        ->type->toEqual(ExampleItemType::TYPE1);
});

test('example item edit view can be rendered', function () {
    $exampleItem = ExampleItem::factory()
        ->create();

    $this->get(route('example-items.edit', ['language' => 'en', 'example_item' => $exampleItem]))
        ->assertStatus(200);
});

test('example item can be updated', function () {
    $exampleItem = ExampleItem::factory()
        ->create([
            'is_active' => true
        ]);

    $data = [
        'title' => 'Title - Test',
        'type' => ExampleItemType::TYPE2->value,
    ];

    $this->put(route('example-items.update', ['language' => 'en', 'example_item' => $exampleItem]), $data)
        ->assertRedirect(route('example-items.index', ['language' => 'en']));

    expect($exampleItem->refresh())
        ->title
            ->not->toBeNull()
            ->toBeString()
            ->toEqual($data['title'])
        ->body
            ->not->toBeNull()
            ->toBeString()
        ->is_active
            ->not->toBeNull()
            ->toBeFalse()
        ->type->toEqual(ExampleItemType::TYPE2);
});

test('example item can be removed', function () {
    $exampleItem = ExampleItem::factory()
        ->create();

    $this->delete(route('example-items.destroy', ['language' => 'en', 'example_item' => $exampleItem]))
        ->assertRedirect(route('example-items.index', ['language' => 'en']));

    expect(ExampleItem::count())
        ->toBe(0);
});

Tests for actions

"CreateExampleItemActionTest" Test (tests/Feature/App/Actions/Examples/ExampleItems):

<?php

use App\Actions\Examples\ExampleItems\CreateExampleItemAction;
use App\Enums\Examples\ExampleItemType;

it('creates example item', function () {
    $validatedData = [
        'title' => 'Example Title',
        'body' => 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Phasellus rhoncus. Aenean vel massa quis mauris vehicula lacinia. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur?',
        'is_active' => true,
        'type' => ExampleItemType::TYPE1,
    ];

    $exampleItem = (new CreateExampleItemAction())->execute($validatedData);

    expect($exampleItem->refresh())
        ->title
            ->not->toBeNull()
            ->toEqual($validatedData['title'])
        ->body
            ->not->toBeNull()
            ->toEqual($validatedData['body'])
        ->is_active
            ->not->toBeNull()
            ->toBeTrue()
        ->type->toEqual($validatedData['type']);
});

"UpdateExampleItemActionTest" Test (tests/Feature/App/Actions/Examples/ExampleItems):

<?php

use App\Actions\Examples\ExampleItems\UpdateExampleItemAction;
use App\Enums\Examples\ExampleItemType;
use App\Models\Examples\ExampleItem;

it('updates example item', function () {
    $exampleItem = ExampleItem::factory()
        ->create([
            'is_active' => true,
            'type' => ExampleItemType::TYPE1
        ]);

    $validatedData = [
        'title' => 'Title - Test',
        'is_active' => false,
        'type' => ExampleItemType::TYPE2,
    ];

    (new UpdateExampleItemAction())->execute($exampleItem, $validatedData);

    expect($exampleItem)
        ->title
            ->not->toBeNull()
            ->toBeString()
            ->toEqual($validatedData['title'])
        ->body
            ->not->toBeNull()
            ->toBeString()
        ->is_active
            ->not->toBeNull()
            ->toBeFalse()
        ->type->toEqual($validatedData['type']);
});

"RemoveExampleItemActionTest" Test (tests/Feature/App/Actions/Examples/ExampleItems):

<?php

use App\Actions\Examples\ExampleItems\RemoveExampleItemAction;
use App\Models\Examples\ExampleItem;

it('removes example item', function () {
    $exampleItem = ExampleItem::factory()
        ->create();

    expect(ExampleItem::count())->toEqual(1);

    (new RemoveExampleItemAction())->execute($exampleItem);

    expect(ExampleItem::count())->toEqual(0);
});

Conclusion

In this tutorial, we have thoroughly familiarized ourselves with the process of creating CRUD operations in Laravel. We have gone through all the key steps, from creating migrations and models, through defining actions and validation requests, to creating templates, factories, and tests. I hope this tutorial has helped you understand how to work with Laravel and has provided you with a solid foundation for further development of your own applications.

Share:
5 / 5
Total votes: 1
You have not rated yet.
Pavel Zaněk

Full-stack developer & SEO consultant

Pavel Zaněk is an experienced full-stack developer with expertise in SEO and programming in Laravel. His skills include website optimization, implementing effective strategies to increase traffic and improve search engine rankings. Pavel is an expert in Laravel and its related technologies, including Livewire, Vue.js, MariaDB, Redis, TailwindCSS/Bootstrap and much more. In addition to his programming skills, he also has a strong background in VPS management, enabling him to handle complex server-side challenges. Pavel is a highly motivated and dedicated professional who is committed to delivering exceptional results. His goal is to help clients achieve success in the online space and achieve their goals with the help of the best web technologies and SEO strategies.

Comments

No comment has been added yet.

Add a comment

Suggested Articles

How to create RSS feed in Laravel framework

Published at 2023-08-10 by Pavel Zaněk

Estimated Reading Time: 10 minutes

laravel

Guide to creating RSS feed in Laravel framework without external packages. From basic principles to advanced techniques. Ideal for developers looking for an efficient and secure solution.

Continue reading

Your experience on this site will be improved by allowing cookies. - Cookies Policy