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:
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:
response()->json()
, Laravel will automatically transform the API resource or Eloquent Model result into JSON, if the client specifies the Accept: application/json
headerVehicleResource
in a few places - once to return a collection and three times for a single model$request->validated()
because this is returned from the Form Request classStoreVehicleRequest
in this case because validation rules are identical for store and updatedestroy()
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:
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:
vehicles.user_id
for new records with auth()->id()
;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!