Remember, in the very beginning, we had created a structure for the Vehicle model? Let me remind you:
Migration file:
Schema::create('vehicles', function (Blueprint $table) { $table->id(); $table->foreignId('user_id')->constrained(); $table->string('plate_number'); $table->timestamps(); $table->softDeletes();});
app/Models/Vehicle.php:
use Illuminate\Database\Eloquent\SoftDeletes; class Vehicle extends Model{ use HasFactory; use SoftDeletes; protected $fillable = ['user_id', 'plate_number'];}
So now we need API endpoints for a user to manage their vehicles. This should be a typical CRUD, with these 5 methods in the Controller:
- index
- store
- show
- update
- delete
So, let's generate it. This is our first Controller without the "Auth" namespace, and let's add a few Artisan flags to generate some skeleton for us:
php artisan make:controller Api/V1/VehicleController --resource --api --model=Vehicle
Also, before filling in the Controler code, let's generate the API Resource that would represent our Vehicle:
php artisan make:resource VehicleResource
For our API, we don't need to return the user_id
and timestamp fields, so we will shorten it to this:
app/Http/Resources/VehicleResource.php:
class VehicleResource extends JsonResource{ public function toArray($request) { return [ 'id' => $this->id, 'plate_number' => $this->plate_number, ]; }}
It may seem like a pointless operation, but now we can re-use the same API Resource in multiple places in our Controller.
The final thing we need to generate is the Form Request class for the validation:
php artisan make:request StoreVehicleRequest
And we fill it in with the validation rule and change it to authorize true
.
app/Http/Requests/StoreVehicleRequest.php:
class StoreVehicleRequest extends FormRequest{ public function authorize() { return true; } public function rules() { return [ 'plate_number' => 'required' ]; }}
Now, we finally get back to our Controller and fill it in, using the API Resource and Form Request from above:
app/Http/Controllers/Api/V1/VehicleController.php:
namespace App\Http\Controllers\Api\V1; use App\Http\Controllers\Controller;use App\Http\Requests\StoreVehicleRequest;use App\Http\Resources\VehicleResource;use App\Models\Vehicle;use Illuminate\Http\Response; class VehicleController extends Controller{ public function index() { return VehicleResource::collection(Vehicle::all()); } public function store(StoreVehicleRequest $request) { $vehicle = Vehicle::create($request->validated()); return VehicleResource::make($vehicle); } public function show(Vehicle $vehicle) { return VehicleResource::make($vehicle); } public function update(StoreVehicleRequest $request, Vehicle $vehicle) { $vehicle->update($request->validated()); return response()->json(VehicleResource::make($vehicle), Response::HTTP_ACCEPTED); } public function destroy(Vehicle $vehicle) { $vehicle->delete(); return response()->noContent(); }}
A few comments here:
- We don't need to specify
response()->json()
, Laravel will automatically transform the API resource or Eloquent Model result into JSON, if the client specifies theAccept: application/json
header - We use the
VehicleResource
in a few places - once to return a collection and three times for a single model - We use
$request->validated()
because this is returned from the Form Request class - We reuse the same
StoreVehicleRequest
in this case because validation rules are identical for store and update - We don't return anything from the
destroy()
method because, well, there's nothing to return if there's no vehicle anymore, right?
Finally, we add this Controller to the endpoint of the routes, within the same group restricted by auth:sanctum
middleware.
routes/api.php:
use App\Http\Controllers\Api\V1\VehicleController; // ... Route::middleware('auth:sanctum')->group(function () { // ... profile routes Route::apiResource('vehicles', VehicleController::class);});
Automatically, the Route::apiResource()
will generate 5 API endpoints:
- GET /api/v1/vehicles
- POST /api/v1/vehicles
- GET /api/v1/vehicles/{vehicles.id}
- PUT /api/v1/vehicles/{vehicles.id}
- DELETE /api/v1/vehicles/{vehicles.id}
Now, you probably want to ask one question...
What about user_id field?
And you're right, it's nowhere to be seen in the Controller.
What we'll do now can be called a "multi-tenancy" in its simple form. Essentially, every user should see only their vehicles. So we need to do two things:
- Automatically set
vehicles.user_id
for new records withauth()->id()
; - Filter all DB queries for the
Vehicle
model with->where('user_id', auth()->id())
.
The first one can be performed in a Model Observer:
php artisan make:observer VehicleObserver --model=Vehicle
Then we fill in the creating()
method. Important notice: it's creating()
, not created()
.
app/Observers/VehicleObserver.php:
namespace App\Observers; use App\Models\Vehicle; class VehicleObserver{ public function creating(Vehicle $vehicle) { if (auth()->check()) { $vehicle->user_id = auth()->id(); } }}
Then, we register our Observer.
app/Providers/AppServiceProvider.php:
use App\Models\Vehicle;use App\Observers\VehicleObserver; class AppServiceProvider extends ServiceProvider{ // ... other methods public function boot() { Vehicle::observe(VehicleObserver::class); }}
And now, we can try to POST a new vehicle! Remember, we still need to pass the same Auth Bearer token, as in the last examples! That will determine the auth()->id()
value for the Observer and any other parts of the code.
See, magic! It has auto-set the user_id
and returned only the needed fields for us. Great!
Now, we need to filter out the data while getting the Vehicles. For that, we will set up a Global Scope in Eloquent. It will help us to avoid the ->where()
statement every we would need it. Specifically, we will use the Anonymous Global Scope syntax and add this code to our Vehicle model:
app/Models/Vehicle.php:
use Illuminate\Database\Eloquent\Builder; class Vehicle extends Model{ protected static function booted() { static::addGlobalScope('user', function (Builder $builder) { $builder->where('user_id', auth()->id()); }); }}
To prove that it works, I manually added another user with their vehicle to the database:
But if we try to get the Vehicle list with the Bearer Token defining our user, we get only our own Vehicle:
Not only that, if we try to get someone else's Vehicle by guessing its ID, we will get a 404 Not Found
response:
You can try out the PUT/DELETE methods yourself, the code will be in the repository.
So, we're done with managing the Vehicles of the user, yay!
What is the best way to write a custom message when there are
no query results for model [App\\Models\\Vehicle] 3
for instance - I'd prefer to have a more friendly message like 'No vehicles found' without exposing names of my models etcExactly the article that I've published this week while answering a question from someone else: //post/laravel-api-override-404-error-message-route-model-binding
perfect thank you!
With the implementation in the article, in case we call the route which was not defined, the message will still be "Vehicle record not found.", therefore it is quite confusing because the root cause is the route could not be found in the routes file. Do you have any suggestions to solve this issue?
Well, you choose what you want to return - either a model default error, or global 404 default error. Not sure if any other way is logical.
Povilas question here, is there an alternative to using observers? i noticed observers tend to hide away the code and makes it harder when working with a team especially for new developers as they have to search around the codebase.
I don't know anything for now but I suggest that when working with a team, it may be good to outline that this app is using observers to simulate multi-tentancy. That's just my thought.
Good question, and it has always been a debate and a personal preference: which you prefer more - full clarity but bloated controllers, or hide the code in some Laravel specific layer?
Alternative is the same
booted()
method of the Model and put the logic there. But wouldn't it violate the same thing you're asking against?My personal opinion: better educate the developers about observers than "dumb down" the full codebase to be less maintainable.
thank you both for your inputs Darah and Povilas!
its a year old but I do agree it's always a debate on observers...I mean for a team member literally saying "we use observers" seems better to me in the long run than all the extra risk that comes by not having this handled in one place...for something like this use case anyway.
To reduce the "hideness" of observers, instead of activating them in
boot
method fromapp.php
, you can use in the newer versions of Laravel an attribute to the model class:W don't need to provide --resource or -r flag separately with --model flag while creating resource controller. When we provide --model flag it automatically create a resource controller.
Instead of adding global scope to all model with declaring booted function separatley we can create a traits and call the trait in our model. I think it will be simple , easier and cleaner.
App\Scopes\UserRecordScope.php:
App\Traits\UserRecord.php:
If you don't want to have scope file separately you can do it directly in trait function:
App\Models\Vehicle.php:
Also an option, yes, thanks for the comment and the code attached!
in my opinion, building seperate scope is fine cause it is a scope that we query specific record , but to use as a trait which is not really a trait (my point of view) , it is more confusing and the boot method is responsible for overriding the model boot method (laravel document said) and it should be inside model for getting more clear meaning that we are overriding it for specific global scope usage. ( just making a discussion comment :) )
Hi! I was curious, you organize the Controllers into API folders, V1, etc.
Is there a reason why we dont organize requests that way either? Dumb question, but I know Login ideally should never change.....
But maybe registration, or vehicle may change in the future revisions of API?
Im very new, so im curious what would be the standard?
Yes, you could structure those as well. But there's no "standard", it's your personal choice.
You might want to amend these lessons to add the
description
column to thevehicles
table since it is needed in the Vue.js front-end course. Unless, of course, you want to leave that as a surprise exercise for the student.This includes updating other files, naturally, including
VehicleFactory.php
,VehicleResource.php
,StoreVehicleRequest.php
, and, of course, the model itself,Vehicle.php
.Thanks, will think about it, since, as you said, it's quite a lot of work and actually doesn't add much direct value to understanding Laravel/Vue
Instead, you could just add a note to the Vue lesson in question, stating something like, "In order for the description to be saved, you'll need to add that column to the vehicles table, making sure to take into account all that will entail in the API."
When we add global scope in booted method and access all vehicles it will filter and provide the vehicles belong to logged in user but what if I want to list down all the vehicles to admin and only the relavant vehicles to user when they do login. Is there any work around for that in global scope? I know that I can add condition inside closure but I am looking for much pretty idea.
Yes, you can use
withoutGlobalScopes()
in the queries where you want those disabled. See the docs.in Vehicle resource controller , you didn't add http:created response to that store function as for it is created , did it on purpose for some reason or forgot to do it cause you explained we are going use specific response for this tutorial
Probably forgot to do it, sorry.
As it turns out, it's probably not necessary, since you should get a 201 (created) status code from calling that endpoint if it was successful. Maybe the
make()
method of a Json resource automatically makes it a 201.