Skip to main content
Laravel, shipping fast.

Chapter 5

Data You Own

Julian Beaujardin

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.)

The audio could not be loaded. Try again in a moment.