Courses

Laravel Travel Agency API From Scratch

Editor Endpoint: Update Travel

Another short lesson will be about the endpoint of updating the Travel record by the editor user role. We will mostly re-use the things we had done already in previous lessons.


Task Description from Client

A private (editor) endpoint to update a travel.

That's it. That's all we have.


Controller Method and Route

We already have a method to create Travel by admin user. We will use the same Controller and add the update() method.

app/Http/Controllers/Api/V1/Admin/TravelController.php:

class TravelController extends Controller
{
public function store(TravelRequest $request)
{
$travel = Travel::create($request->validated());
 
return new TravelResource($travel);
}
 
public function update(Travel $travel, TravelRequest $request)
{
$travel->update($request->validated());
 
return new TravelResource($travel);
}
}

We use the same TravelRequest for the validation because the rules are identical.

The difference in using those methods will be in the Middleware:

  • store() should be accessed by the admin role
  • update() should be accessed by both admin and editor roles

Here's the updated Route:

routes/api.php:

Route::prefix('admin')->middleware(['auth:sanctum'])->group(function () {
Route::middleware('role:admin')->group(function () {
Route::post('travels', [Admin\TravelController::class, 'store']);
Route::post('travels/{travel}/tours', [Admin\TourController::class, 'store']);
});
 
Route::put('travels/{travel}', [Admin\TravelController::class, 'update']);
});

As you can see, for the update() method, we use a PUT request with Route::put(). This is a standard practice in REST API systems.

Notice: in this case, we rely on the fact that there are only admin and editor roles, so we can assume that the Travel Update endpoint is accessible to any logged-in user. If the system adds more roles in the future, we will need to add something like role:admin,editor and modify the Middleware to accept a comma-separated role list as a parameter.

We launch it in Postman, and the record is successfully updated!


Automated Test

This one is also simple. We just add one more method to the existing AdminTravelTest file.

tests/Feature/AdminTravelTest.php:

class AdminTravelTest extends TestCase
{
use RefreshDatabase;
 
// ... other methods
 
public function test_updates_travel_successfully_with_valid_data(): void
{
$this->seed(RoleSeeder::class);
$user = User::factory()->create();
$user->roles()->attach(Role::where('name', 'editor')->value('id'));
$travel = Travel::factory()->create();
 
$response = $this->actingAs($user)->putJson('/api/v1/admin/travels/'.$travel->id, [
'name' => 'Travel name',
]);
$response->assertStatus(422);
 
$response = $this->actingAs($user)->putJson('/api/v1/admin/travels/'.$travel->id, [
'name' => 'Travel name updated',
'is_public' => 1,
'description' => 'Some description',
'number_of_days' => 5,
]);
 
$response->assertStatus(200);
 
$response = $this->get('/api/v1/travels');
$response->assertJsonFragment(['name' => 'Travel name updated']);
}
}

We launch the test suite, and it's green! We've finished writing the functionality of the project, which is covered by 19 tests, in total.

As a few final steps, we need to perform a cleanup and write the documentation.


GitHub commit for this lesson:

Previous: Admin Endpoint: Create Tours
avatar

Hi Povilas || Anyone else who sees this and can help,

I have a project where I am trying to use the comma seperated value to determine where multiple roles ('super-admin' and 'admin' roles, in this case) should have access to a particular route. Unfortunately, only one role (at a time) is going through, not more than one.

My test for this feature, keeps failing.

Here are my codes below.

Kindly help urgently. Thanks in advance.

C:...\tests\Feature\CategoryTest.php

public function test_admin_can_retrieve_categories()
{
			// $ADMIN_ROLE = 2;
    $admin = User::factory()->create(['role_id' => Role::ADMIN_ROLE]);

    $response = $this->actingAs($admin)->getJson('/api/v1/admin/categories');

    $response->assertStatus(200);
}
	
	

C:...\app\Http\Middleware\RoleMiddleware.php

public function handle(Request $request, Closure $next, string $roles): Response
{
    if (!auth()->check()) {
        abort(401);
    }

    $roles_array = explode('|', $roles);

    foreach ($roles_array as $role) {
        if (!auth()->user()->role()->where('name', $role)->exists()) {
            abort(403);
        }
    }

    return $next($request);
}

C:...\routes\api.php

Route::middleware(['auth:sanctum'])->group(function () {

		Route::prefix('admin')->group(function () {

				...

				Route::middleware(['role:super-admin|admin'])->group(function () {
						Route::apiResource('categories', Admin\CategoryController::class)->except(['store', 'update', 'destroy']);
				});

				...
		});
});
avatar

I think the problem is in your middleware. Since you use foreach - you're basically saying "if user doesn't have one of the roles in the array - abort".

I'd rather compare the arrays of current user role and the allowed roles :

				$roles_array = explode('|', $roles);

        $userRoles = auth()->user()->roles()->pluck('name')->toArray();

        $allowedRoles = array_intersect($userRoles, $roles_array);

        if ($allowedRoles === []) {
                abort(403);
        }
avatar

Thanks for your reply. I have already concluded the app using a verbose pattern.

I will apply your pattern in my next project that is coming up this month.

avatar
You can use Markdown
avatar

Povilas, is there a way for the slug value to also be updated whenever we update the name value?

avatar

Yeah, you could create an Observer with updating() or updated() method for this, I guess.

avatar
You can use Markdown
avatar

Also, since this is the requirement from the client:

A private (editor) endpoint to update a travel;

Why is admin also able to update a travel? shouldn't it be only the editor who's allowed to update?

avatar

Tell that to the client :)

avatar
You can use Markdown
avatar
You can use Markdown