Back to Course |
Laravel Travel Agency API From Scratch

Admin Endpoint: Create Tours

This lesson will be a short one, like a "relax" session: we will just repeat almost the same thing as in the last lesson, just for a different endpoint of Tours. And we've done all the auth-related work in the previous lesson, too.


Task Description from Client

A private (admin) endpoint to create new tours for a travel.

We don't have more information: for the public endpoint, the Travel was identified by slug, so should we use it here, too? Not sure. Probably not.


Controller, Request, Route

We generate another Controller in the Admin namespace:

php artisan make:controller Api/V1/Admin/TourController

Also, we generate the Form Request class for the validation:

php artisan make:request TourRequest

Similar to the previous TravelRequest, we will not create separate CreateTourRequest and UpdateTourRequest, as validation rules will be the same.

Here are the validation rules:

app/Http/Requests/TourRequest.php:

class TourRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
 
public function rules(): array
{
return [
'name' => ['required'],
'starting_date' => ['required', 'date'],
'ending_date' => ['required', 'date', 'after:starting_date'],
'price' => ['required', 'numeric'],
];
}
}

Now, inside the Controller, we add this code:

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

namespace App\Http\Controllers\Api\V1\Admin;
 
use App\Http\Controllers\Controller;
use App\Http\Requests\TourRequest;
use App\Http\Resources\TourResource;
use App\Models\Travel;
 
class TourController extends Controller
{
public function store(Travel $travel, TourRequest $request)
{
$tour = $travel->tours()->create($request->validated());
 
return new TourResource($tour);
}
}

We use the Route Model Binding to get the $travel object and then can use the hasMany relationship to create a new child object of Tour.

We also re-use the same TourResource as in the public endpoint.

Finally, we assign that store() method to the Route.

routes/api.php:

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

If we launch this in Postman with the correct Bearer Token, we get the result:


Automated Tests

Similarly to the previous AdminTravelTest, we will test the same cases for adding the Tour.

php artisan make:test AdminTourTest

And here's the code:

tests/Feature/AdminTourTest.php:

namespace Tests\Feature;
 
use App\Models\Role;
use App\Models\Travel;
use App\Models\User;
use Database\Seeders\RoleSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
 
class AdminTourTest extends TestCase
{
use RefreshDatabase;
 
public function test_public_user_cannot_access_adding_tour(): void
{
$travel = Travel::factory()->create();
$response = $this->postJson('/api/v1/admin/travels/'.$travel->id.'/tours');
 
$response->assertStatus(401);
}
 
public function test_non_admin_user_cannot_access_adding_tour(): 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)->postJson('/api/v1/admin/travels/'.$travel->id.'/tours');
 
$response->assertStatus(403);
}
 
public function test_saves_tour_successfully_with_valid_data(): void
{
$this->seed(RoleSeeder::class);
$user = User::factory()->create();
$user->roles()->attach(Role::where('name', 'admin')->value('id'));
$travel = Travel::factory()->create();
 
$response = $this->actingAs($user)->postJson('/api/v1/admin/travels/'.$travel->id.'/tours', [
'name' => 'Tour name',
]);
$response->assertStatus(422);
 
$response = $this->actingAs($user)->postJson('/api/v1/admin/travels/'.$travel->id.'/tours', [
'name' => 'Tour name',
'starting_date' => now()->toDateString(),
'ending_date' => now()->addDay()->toDateString(),
'price' => 123.45,
]);
 
$response->assertStatus(201);
 
$response = $this->get('/api/v1/travels/'.$travel->slug.'/tours');
$response->assertJsonFragment(['name' => 'Tour name']);
}
}

And... it works!

As I mentioned, this lesson was quite short. We just repeated what we had learned in the previous lesson.


GitHub commit for this lesson: