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.
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.
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:
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.