The next step is to add users with the role company owner
: the ones who would later manage reservations and assign guides to them. Before implementing this feature, we asked the client a few questions.
Question: Can a company have more than one user with the company owner
role?
Answer: Yes.
What it means to us: No extra work, the belongsTo
relationship is enough, no many-to-many
here.
Question: Who can manage company owners? Only the super administrator
or the company itself.
Answer: The company itself.
What it means to us: Extra unplanned work. We didn't initially plan to build User management for Company Owner roles themselves.
Question: Can one "company owner" user belong to more than one company?
Answer: No.
What it means to us: No extra work, the belongsTo
relationship is enough, no many-to-many
here.
Important at this stage: if we discover new functionality along the way, we need to tell the client that some new features will take longer to implement and/or will cost more.
Ideally, these questions would have been asked before even starting to code to avoid such misunderstandings.
First, we will implement this User management feature for the users with the administrator
role. But instead of doing CRUD for /users
, we will immediately divide it by company, so URL will be /companies/[ID]/users
.
For that, we will use the Nested Resources feature.
So, first, let's create a Controller and a Route.
php artisan make:controller CompanyUserController
routes/web.php:
use App\Http\Controllers\CompanyUserController; Route::middleware('auth')->group(function () { // ... Route::resource('companies', CompanyController::class)->middleware('isAdmin'); Route::resource('companies.users', CompanyUserController::class)->except('show'); });
Notice: I specifically didn't add
isAdmin
middleware for this route because later it can be reused for users with thecompany owner
role. We will only need to restrict access so that users couldn't see other companies. My plan for this is to use Policies.
So now we can add a new action to the companies list page.
resources/views/companies/index.blade.php:
// ... <td class="px-6 py-4 text-sm leading-5 text-gray-900 whitespace-no-wrap"> <a href="{{ route('companies.users.index', $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"> Users </a> {{-- ... Edit/Delete buttons --}}</td> // ...
Next, in the Controller we need to get all the users that belong to the company. But first, we need a users
relation in the Company
Model.
app/Models/Company.php:
use Illuminate\Database\Eloquent\Relations\HasMany; class Company extends Model{ // ... public function users(): HasMany { return $this->hasMany(User::class); }}
Similar to how we saved all Blade files for the companies in the resources/views/companies
directory, it's very logical to create a new directory inside it called users
. This way when someone would check the resources/views/companies
directory he would know that users
belong to companies.
So, the directory structure for nested resources would be like this:
resources/views/[parent]/[child]/index.blade.php
resources/views/[parent]/[child]/create.blade.php
resources/views/[parent]/[child]/edit.blade.php
app/Http/Controllers/CompanyUserController.php:
use App\Models\Company; class CompanyUserController extends Controller{ public function index(Company $company) { $users = $users = $company->users()->where('role_id', Role::COMPANY_OWNER->value)->get(); return view('companies.users.index', compact('company', 'users')); }}
And here's the Blade View file to show all the users that belong to the selected company.
<x-app-layout> <x-slot name="header"> <h2 class="text-xl font-semibold leading-tight text-gray-800"> {{ __('Company users') }} </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.users.create', $company) }}" 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($users as $user) <tr class="bg-white"> <td class="px-6 py-4 text-sm leading-5 text-gray-900 whitespace-no-wrap"> {{ $user->name }} </td> <td class="px-6 py-4 text-sm leading-5 text-gray-900 whitespace-no-wrap"> <a href="{{ route('companies.users.edit', [$company, $user]) }}" 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.users.destroy', [$company, $user]) }}" 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>
This Blade file is very similar to the one we had for listing the companies. The main difference is that because this is a nested View, for every action we also need to pass the company as a Route parameter.
Now that we can show users for a specific company, let's add the create and edit forms.
For the validation, we will use Form Requests. Let's generate them immediately, so we would use them in the Controller.
php artisan make:request StoreUserRequestphp artisan make:request UpdateUserRequest
app/Http/Requests/StoreUserRequest.php:
use App\Models\User;use Illuminate\Validation\Rules; class StoreUserRequest extends FormRequest{ public function authorize(): bool { return true; } public function rules(): array { return [ 'name' => ['required', 'string'], 'email' => ['required', 'email', 'unique:users,email'], 'password' => ['required', Rules\Password::defaults()], ]; }}
app/Http/Requests/UpdateUserRequest.php:
class UpdateUserRequest extends FormRequest{ public function authorize(): bool { return true; } public function rules(): array { return [ 'name' => ['required', 'string'], 'email' => ['required', 'email', 'unique:users,email,' . $this->user->id], ]; }}
The Controller code for creating and updating:
app/Http/Controllers/CompanyUserController.php:
use App\Enums\Role;use App\Models\User;use App\Http\Requests\StoreUserRequest;use App\Http\Requests\UpdateUserRequest; class CompanyUserController extends Controller{ // ... public function create(Company $company) { return view('companies.users.create', compact('company')); } public function store(StoreUserRequest $request, Company $company) { $company->users()->create([ 'name' => $request->input('name'), 'email' => $request->input('email'), 'password' => bcrypt($request->input('password')), 'role_id' => Role::COMPANY_OWNER->value, ]); return to_route('companies.users.index', $company); } public function edit(Company $company, User $user) { return view('companies.users.edit', compact('company', 'user')); } public function update(UpdateUserRequest $request, Company $company, User $user) { $user->update($request->validated()); return to_route('companies.users.index', $company); }}
And here are both create and edit forms.
resources/views/companies/users/create.blade.php:
<x-app-layout> <x-slot name="header"> <h2 class="text-xl font-semibold leading-tight text-gray-800"> {{ __('Create User for Company') }}: {{ $company->name }} </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.users.store', $company) }}" 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-input-label for="email" value="Email" /> <x-text-input id="email" name="email" value="{{ old('email') }}" type="text" class="block mt-1 w-full" /> <x-input-error :messages="$errors->get('email')" class="mt-2" /> </div> <div class="mt-4"> <x-input-label for="password" value="Password" /> <x-text-input id="password" name="password" value="{{ old('password') }}" type="password" class="block mt-1 w-full" /> <x-input-error :messages="$errors->get('password')" 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/users/edit.blade.php:
<x-app-layout> <x-slot name="header"> <h2 class="text-xl font-semibold leading-tight text-gray-800"> {{ __('Edit User') }}: {{ $user->name }} </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.users.update', [$company, $user]) }}" method="POST"> @csrf @method('PUT') <div> <x-input-label for="name" value="Name" /> <x-text-input id="name" name="name" value="{{ old('name', $user->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-input-label for="email" value="Email" /> <x-text-input id="email" name="email" value="{{ old('email', $user->email) }}" type="text" class="block mt-1 w-full" /> <x-input-error :messages="$errors->get('email')" class="mt-2" /> </div> <div class="mt-4"> <x-primary-button> Save </x-primary-button> </div> </form> </div> </div> </div> </div></x-app-layout>
When creating the list page we already added the Delete button. All that's left is to add a method to the Controller.
app/Http/Controllers/CompanyUserController.php:
class CompanyUserController extends Controller{ // ... public function destroy(Company $company, User $user) { $user->delete(); return to_route('companies.users.index', $company); }}
Now let's add tests. The plan is to check that user with the administrator
role can perform every CRUD action.
First, we need to create a Factory for the Company
Model.
php artisan make:factory CompanyFactory
app/database/factories/CompanyFactory.php:
class CompanyFactory extends Factory{ public function definition(): array { return [ 'name' => fake()->words(3, true), ]; }}
Now the test.
php artisan make:test CompanyUserTest
tests/Feature/CompanyUserTest.php:
use App\Models\User;use App\Models\Company;use Illuminate\Foundation\Testing\RefreshDatabase;use Tests\TestCase; class CompanyUserTest extends TestCase{ use RefreshDatabase; public function test_admin_can_access_company_users_page() { $company = Company::factory()->create(); $user = User::factory()->admin()->create(); $response = $this->actingAs($user)->get(route('companies.users.index', $company->id)); $response->assertOk(); } public function test_admin_can_create_user_for_a_company() { $company = Company::factory()->create(); $user = User::factory()->admin()->create(); $response = $this->actingAs($user)->post(route('companies.users.store', $company->id), [ 'name' => 'test user', 'email' => 'test@test.com', 'password' => 'password', ]); $response->assertRedirect(route('companies.users.index', $company->id)); $this->assertDatabaseHas('users', [ 'name' => 'test user', 'email' => 'test@test.com', ]); } public function test_admin_can_edit_user_for_a_company() { $company = Company::factory()->create(); $user = User::factory()->admin()->create(['company_id' => $company->id]); $response = $this->actingAs($user)->put(route('companies.users.update', [$company->id, $user->id]), [ 'name' => 'updated user', 'email' => 'test@update.com', ]); $response->assertRedirect(route('companies.users.index', $company->id)); $this->assertDatabaseHas('users', [ 'name' => 'updated user', 'email' => 'test@update.com', ]); } public function test_admin_can_delete_user_for_a_company() { $company = Company::factory()->create(); $user = User::factory()->admin()->create(['company_id' => $company->id]); $response = $this->actingAs($user)->delete(route('companies.users.update', [$company->id, $user->id])); $response->assertRedirect(route('companies.users.index', $company->id)); $this->assertDatabaseMissing('users', [ 'name' => 'updated user', 'email' => 'test@update.com', ]); }}
In the next lesson, we will expand the tests, after adding the User management feature to another role.