Back to Course |
Build Laravel API for Car Parking App: Step-By-Step

Manage User's Vehicles

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 the Accept: 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 with auth()->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.

Laravel API Vehicles

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:

Laravel API Vehicles

But if we try to get the Vehicle list with the Bearer Token defining our user, we get only our own Vehicle:

Laravel API Vehicles

Not only that, if we try to get someone else's Vehicle by guessing its ID, we will get a 404 Not Found response:

Laravel API Vehicles

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!