The licenses in this API live at a provider. That made Chapters 2 and 3 a good lesson in boundaries, and an unusual one. Most endpoints you will ever write sit on top of a table in your own database.
The License API has one of those too. Chapter 4 introduced the Consumer, and somebody has to create consumers, list them, rename them, issue their tokens, and remove them. Until now that happened in a console. This chapter gives it an API, and in doing so shows the same patterns on the case Laravel was designed for: a resource backed by Eloquent.
It needs none of the machinery Chapter 3 built: no contract, no driver, no typed object, no HTTP client. When the data is yours, the framework carries almost all of it.
The Table and the Model
php artisan make:model Consumer -mf
Chapter 4 ran that command and left the migration nearly empty. Here it is in full:
// database/migrations/..._create_consumers_table.php
Schema::create('consumers', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->string('contact_email')->nullable();
$table->text('settings')->nullable();
$table->unsignedInteger('rate_limit')->nullable();
$table->unsignedInteger('license_limit')->default(50);
$table->softDeletes();
$table->timestamps();
});
Three columns deserve a comment. settings is text, not json, because Chapter 4 encrypts it, and ciphertext is not JSON. license_limit is how many licenses this consumer may hold, which Chapter 7 enforces. softDeletes() adds a deleted_at column, for a reason the end of this chapter explains.
// app/Models/Consumer.php
#[Fillable([
'name', 'contact_email', 'rate_limit', 'license_limit',
])]
#[Hidden(['settings'])]
class Consumer extends Authenticatable
{
use HasApiTokens, HasFactory, SoftDeletes;
// ... casts() and providerToken() from Chapter 4
}
Fillable is the list of attributes a request is allowed to set in bulk. settings is not on it: a consumer’s provider credentials are never set by passing an array from a request straight into the model. (Laravel documentation: Eloquent: Getting Started › Mass Assignment.)
Hidden keeps settings out of the model’s array and JSON form. The Resource later in this chapter is the real guard, but a model that can’t serialize its own secrets is a second lock on the same door, and it costs one line.
The Routes
// routes/api.php
Route::middleware('auth:sanctum')->group(function () {
Route::apiResource(
'consumers',
ConsumerController::class,
);
Route::apiResource(
'consumers.tokens',
TokenController::class,
)->only(['index', 'store', 'destroy'])->scoped();
});
The first call declares five endpoints: index, store, show, update, and destroy. Run php artisan route:list --path=consumers and read them. If the framework can name a route, this book never writes it by hand.
The second declares a nested resource, tokens that belong to a consumer, at /consumers/{consumer}/tokens/{token}. scoped() is the part that matters. It tells Laravel that the {token} in the URL must belong to the {consumer} in the URL. Without it, /consumers/1/tokens/99 would happily load token 99 even if it belongs to consumer 2, and you would have written an endpoint that deletes other people’s credentials. (Laravel documentation: Controllers › Scoping Nested Resources.)