Courses

Laravel 11: Small Reservation Project Step-By-Step

Laravel Breeze and Companies CRUD

So now that we have some plan let's start implementing it. We will start by installing Laravel Breeze starter kit for quick authentication scaffolding and a simple layout. Then, we will create the first CRUD for companies.


Install Breeze and Assign Default Role

So first, we will install Breeze.

composer require laravel/breeze --dev
php artisan breeze:install blade

During the planning phase, we added a Role table and a role_id column to the User table. Because of this, if you try to register, you will get an error:

SQLSTATE[HY000]: General error: 1364 Field 'role_id' doesn't have a default value

So, we need to add the default roles and assign a role when a user registers.

For adding roles, we will create a Seeder.

php artisan make:seeder RoleSeeder

database/seeders/RoleSeeder.php:

use App\Models\Role;
 
class RoleSeeder extends Seeder
{
public function run(): void
{
Role::create(['name' => 'administrator']);
Role::create(['name' => 'company owner']);
Role::create(['name' => 'customer']);
Role::create(['name' => 'guide']);
}
}

And add this Seeder to the DatabaseSeeder.

database/seeders/DatabaseSeeder.php:

class DatabaseSeeder extends Seeder
{
public function run(): void
{
$this->call([
RoleSeeder::class,
]);
}
}

When the user registers, we need to assign a role. We could add the ID of the customer role in the RegisteredUserController of Laravel Breeze. But if a new developer would join this project sometime in the future, he wouldn't know what that number means. For this, we will use the PHP Enums feature.

Enum can be created using an Artisan command make:enum.

php artisan make:enum Enums\Role --int

Inside app/Enums/Role.php, we need to add all the roles; their value will be the ID.

app/Enums/Role.php:

enum Role: int
{
case ADMINISTRATOR = 1;
case COMPANY_OWNER = 2;
case CUSTOMER = 3;
case GUIDE = 4;
}

So now, we can use Role Enum where we need it.

app/Http/Controllers/Auth/RegisteredUserController.php:

use App\Enums\Role;
 
class RegisteredUserController extends Controller
{
public function store(Request $request): RedirectResponse
{
// ...
 
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
'role_id' => Role::CUSTOMER->value,
]);
 
// ...
}
}

Now we can register, great!

Ok, now we can move to the actual functionality, and looking at the plan, we'll start with managing companies.


Show Table of All Companies

Next, we can create the companies CRUD. For now, it will be available to everyone, and in the next lesson, we will restrict this functionality to administrators only.

In general, my approach: first focus on making the feature itself work and then add more validation and restrictions.

So, first, we need a Controller and a Route.

php artisan make:controller CompanyController

routes/web.php:

use App\Http\Controllers\CompanyController;
 
Route::middleware('auth')->group(function () {
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
 
// ...
 
Route::resource('companies', CompanyController::class);
});

And let's add a navigation link in the menu, just next to the dashboard. We will copy-paste the existing Laravel Breeze x-nav-link component for that.

resources/views/layouts/navigation.blade.php:

// ...
<!-- 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>
<x-nav-link :href="route('companies.index')" :active="request()->routeIs('companies.index')">
{{ __('Companies') }}
</x-nav-link>
</div>
// ...

Next, in the Controller, we need to get all the companies and show them.

We will save all Blade files related to companies in the resources/views/companies directory: it's a common practice to have files like this, often corresponding to the Controller methods:

  • resources/views/[feature-name]/index.blade.php
  • resources/views/[feature-name]/create.blade.php
  • resources/views/[feature-name]/edit.blade.php
  • etc.

app/Http/Controllers/CompanyController.php

use App\Models\Company;
use Illuminate\View\View;
 
class CompanyController extends Controller
{
public function index(): View
{
$companies = Company::all();
 
return view('companies.index', compact('companies'));
}
}

And here's the Blade View file to show all the companies.

resources/views/companies/index.blade.php:

<x-app-layout>
<x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800">
{{ __('Companies') }}
</h2>
</x-slot>
 
<div class="py-12">
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
<div class="overflow-hidden bg-white shadow-sm sm:rounded-lg">
<div class="overflow-hidden overflow-x-auto border-b border-gray-200 bg-white p-6">
 
<a href="{{ route('companies.create') }}"
class="mb-4 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-xs font-semibold uppercase tracking-widest text-gray-700 shadow-sm transition duration-150 ease-in-out hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-25">
Create
</a>
 
<div class="min-w-full align-middle">
<table class="min-w-full border divide-y divide-gray-200">
<thead>
<tr>
<th class="bg-gray-50 px-6 py-3 text-left">
<span class="text-xs font-medium uppercase leading-4 tracking-wider text-gray-500">Name</span>
</th>
<th class="w-56 bg-gray-50 px-6 py-3 text-left">
</th>
</tr>
</thead>
 
<tbody class="bg-white divide-y divide-gray-200 divide-solid">
@foreach($companies as $company)
<tr class="bg-white">
<td class="px-6 py-4 text-sm leading-5 text-gray-900 whitespace-no-wrap">
{{ $company->name }}
</td>
<td class="px-6 py-4 text-sm leading-5 text-gray-900 whitespace-no-wrap">
<a href="{{ route('companies.edit', $company) }}"
class="inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-xs font-semibold uppercase tracking-widest text-gray-700 shadow-sm transition duration-150 ease-in-out hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-25">
Edit
</a>
<form action="{{ route('companies.destroy', $company) }}" method="POST" onsubmit="return confirm('Are you sure?')" style="display: inline-block;">
@csrf
@method('DELETE')
<x-danger-button>
Delete
</x-danger-button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</x-app-layout>


Create and Edit Companies

Now that we can show companies, let's add the create and edit forms.

We will use Form Requests for the validation. Let's generate them immediately so we can use them in the Controller.

php artisan make:request StoreCompanyRequest
php artisan make:request UpdateCompanyRequest

Rules in the form request for both save and update are the same.

app/Http/Requests/StoreCompanyRequest.php:

class StoreCompanyRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
 
public function rules(): array
{
return [
'name' => ['required', 'string'],
];
}
}

app/Http/Requests/UpdateCompanyRequest.php:

class UpdateCompanyRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
 
public function rules(): array
{
return [
'name' => ['required', 'string'],
];
}
}

The Controller code for creating and updating:

app/Http/Controllers/CompanyController.php:

use Illuminate\Http\RedirectResponse;
use App\Http\Requests\StoreCompanyRequest;
use App\Http\Requests\UpdateCompanyRequest;
 
class CompanyController extends Controller
{
// ...
 
public function create(): View
{
return view('companies.create');
}
 
public function store(StoreCompanyRequest $request): RedirectResponse
{
Company::create($request->validated());
 
return to_route('companies.index');
}
 
public function edit(Company $company)
{
return view('companies.edit', compact('company'));
}
 
public function update(UpdateCompanyRequest $request, Company $company): RedirectResponse
{
$company->update($request->validated());
 
return to_route('companies.index');
}
}

And here are both create and edit forms.

resources/views/companies/create.blade.php:

<x-app-layout>
<x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800">
{{ __('Create Company') }}
</h2>
</x-slot>
 
<div class="py-12">
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
<div class="overflow-hidden bg-white shadow-sm sm:rounded-lg">
<div class="overflow-hidden overflow-x-auto border-b border-gray-200 bg-white p-6">
<form action="{{ route('companies.store') }}" method="POST">
@csrf
 
<div>
<x-input-label for="name" value="Name" />
<x-text-input id="name" name="name" value="{{ old('name') }}" type="text" class="block mt-1 w-full" />
<x-input-error :messages="$errors->get('name')" class="mt-2" />
</div>
 
<div class="mt-4">
<x-primary-button>
Save
</x-primary-button>
</div>
</form>
</div>
</div>
</div>
</div>
</x-app-layout>

resources/views/companies/edit.blade.php:

<x-app-layout>
<x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800">
{{ __('Edit Company') }}
</h2>
</x-slot>
 
<div class="py-12">
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
<div class="overflow-hidden bg-white shadow-sm sm:rounded-lg">
<div class="overflow-hidden overflow-x-auto border-b border-gray-200 bg-white p-6">
<form action="{{ route('companies.update', $company) }}" method="POST">
@csrf
@method('PUT')
 
<div>
<x-input-label for="name" value="Name" />
<x-text-input id="name" name="name" value="{{ old('name', $company->name) }}" type="text" class="block mt-1 w-full" />
<x-input-error :messages="$errors->get('name')" class="mt-2" />
</div>
 
<div class="mt-4">
<x-primary-button>
Save
</x-primary-button>
</div>
</form>
</div>
</div>
</div>
</div>
</x-app-layout>


Delete Companies

Now we need to implement the delete method. Not sure if you noticed, but I've already added the Delete button before, when creating the list page.

All that's left is to add a method to the Controller.

app/Http/Controllers/CompanyController.php:

class CompanyController extends Controller
{
// ...
 
public function destroy(Company $company)
{
$company->delete();
 
return to_route('companies.index');
}
}

That's it! We have fully working companies CRUD. Let's move on to the next lesson.

avatar

Finally someone explained Enums to me :-) constants would need instance of User to be called. Enums do not.

👍 1
avatar

Enums are doing way more then in above example. They may be combined with with Attributes, so each value in enum would have it's own description. This is very handly when generating dropdown lists and ensures that descriptions of enum value used in front end will be consistent.

Have a look here for laravel example

Additionally they can be combined with i18n and gettext.

avatar

This also explains enums https://laracasts.com/series/whats-new-in-laravel-9/episodes/10

avatar

I have noticed here that you haven't handled success or failure messages to show to user on screen. Ex. Company created succesfully!!!

avatar

That would be nice; however I am not able to save anything to the database even after copying line for line from the lesson to my Php Storm pages. I did create a Company Seeder page that is working fine but I am unable to update the name on it, it does fine on the edit page but it is not saved to the database.

avatar

Hi there! Got an error when I tried to log in, due to the fact that the Role model was not seeded. I might be overlooking, but it seems that the instruction " php artisan db:seed " is missing...

avatar

And also a question: a new user gets a role_id of 3 (customer). Where in the program is this arranged? Can someone tell me please, I cannot find it anywhere...

avatar

It's in this lesson and is assigned in the app/Http/Controllers/Auth/RegisteredUserController.php

avatar

Thanks very much for your quick reply! This series of lesson is very useful!

avatar

haveing a problem trying to register a new user I keep getting a error


array_key_first(): Argument #1 ($array) must be of type array, int given

        $request->validate([

            'name' => ['required', 'string', 'max:255'],

            'email' => ['required', 'string', 'email', 'max:255', 'unique:'.User::class],

            'password' => ['required', 'confirmed', Rules\Password::defaults()],

            'role_id' => Role::CLIENT->value,

        ]);
				
				namespace App\Enums;

						enum Role: int
						{
								case ADMINISTRATOR = 1;
								case VENDOR = 2;
								case CLIENT = 3;
					}


avatar

Because you add enum into validation

avatar

my bad thanks

avatar

For me, there is a bit too much code duplication. A separate form request for Update and Store and a separate view for each is not necessary.

avatar

This is an ongoing debate for years. Some people prefer to have one form for create/edit and then struggle if there are changes to only create or only edit, other people prefer to have separate which may look like duplicate but is more flexible if you need to change something in one of them.

avatar

I am trying to run a test on the role and I am getting an error; I seem to recall a line of code you had us put in the RegistrationTest.php file but I just don’t remember what it was pleas remind me!

avatar

We must seed the database before registering a user.

Command to seed is php artisan db:seed

Only after seeding your database can you register a new user.

👍 1
avatar

You can do that when you migrate with php artisan migrate:fresh --seed

avatar

Trying to use Uuid on the companies Table and I am getting this error

SQLSTATE[HY000]: General error: 1364 Field 'id' doesn't have a default value

Can you help

avatar

Changing migrations isnt enough. You need to set primary key for models https://laravel.com/docs/10.x/eloquent#primary-keys

avatar

You need to run role seeder first of all php artisan db:seed --class=RoleSeeder

avatar

i think you forgot to show the code for the UpdateCompanyRequest file

avatar

This is probably because it is identical to the store method. Might have been a mistake in editing and it got removed, we will chat about this

avatar

In the lesson it is said Rules in the form request for both save and update are the same.

avatar

oh okay. thanks

avatar

How do you plan to ensure consistency between Enum and DB with this role system implementation?

Obviously RoleSeeder should be like that

class RoleSeeder extends Seeder
{
    public function run(): void
    {
        foreach (\App\Enums\Role::cases() as $role) {
            \App\Models\Role::firstOrCreate([
                'id' => $role->value,
                'name' => strtolower($role->name)
            ]);
        }
    }
}