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.
Hello, Povilas! When creating a tour i get an error: "SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'travel_id' cannot be null I thought i'm doing something wrong for comparison i downloaded your code from github for comparison and it gives the same error. What might be the problem?
sorry after after some fixes in docker your code worked correctly but i still have this weird error
Travel_id cannot be null means you didn't pass travel_id? Please post the full request what parameters you passed, then I could maybe advise.
After dd(), the request contains all the fields that I enter in postman ({ "name": "New tour", "starting_date": "2023-07-18", "ending_date": "2023-07-19", "price": "250.00"} ), as I try to display the data with the help of dd, it is from travel, I do not receive any useful information. I understand that the route localhost/api/v1/admin/travels/{travel}/tours does not work correctly.
Sorry from the information you give me here, I don't see why it's not working for you, your case needs personal debugging, which I don't have time for, unfortunately. Please fully compare your code to mine and your requests to my requests in the course. No one else complained that it didn't work.