Skip to main content
Laravel, shipping fast.
// app/Http/Controllers/ConsumerController.php
public function show(Consumer $consumer): ConsumerResource
{
    return ConsumerResource::make($consumer);
}

There is no query in that method. The parameter is named $consumer, the route segment is {consumer}, and the type is an Eloquent model, so Laravel looks the record up by its key before the method runs. If it doesn’t exist, the consumer of your API gets a 404 and your code is never called.

This is the framework doing what Chapter 3 had to build by hand. There, a key arrived as a string and a driver took it to the provider. Here the model is the lookup. (Laravel documentation: Routing › Route Model Binding.)

Write Consumer $consumer and never int $id followed by Consumer::findOrFail($id). They do the same thing, but only the second depends on someone remembering to write it.

Creating and Updating

Two requests, because the two operations have different rules. As in Chapter 2, authorize() returns true in both. Who may call these endpoints is decided on the controller, later in this chapter.

// app/Http/Requests/StoreConsumerRequest.php
public function rules(): array
{
    return [
        'name' => [
            'bail', 'required', 'string', 'max:100',
            Rule::unique('consumers', 'name'),
        ],
        'contact_email' => ['nullable', 'email', 'max:254'],
        'rate_limit' => [
            'nullable', 'integer', 'between:1,6000',
        ],
        'license_limit' => ['integer', 'between:1,1000'],
    ];
}
// app/Http/Requests/UpdateConsumerRequest.php
public function rules(): array
{
    return [
        'name' => [
            'sometimes', 'string', 'max:100',
            Rule::unique('consumers', 'name')
                ->ignore($this->route('consumer')),
        ],
        'contact_email' => [
            'sometimes', 'nullable', 'email', 'max:254',
        ],
        'rate_limit' => [
            'sometimes', 'nullable', 'integer',
            'between:1,6000',
        ],
        'license_limit' => [
            'sometimes', 'integer', 'between:1,1000',
        ],
    ];
}

Three details separate an update from a create.

sometimes means “validate this field only if it was sent.” An update that changes the contact address shouldn’t have to send the name again. That is what makes this endpoint a PATCH: the client sends what changes.

ignore() on the uniqueness rule excludes the record being updated. Without it, saving a consumer under its own existing name fails with “the name has already been taken.”

A database constraint backs the rule. The migration declared name unique as well. The validation rule gives a friendly 422. The index is what makes it true when two requests arrive in the same millisecond. Always have both.

The controller methods stay as thin as they were in Chapter 2:

// app/Http/Controllers/ConsumerController.php
public function store(
    StoreConsumerRequest $request,
): ConsumerResource {
    $consumer = Consumer::create($request->validated());

    return ConsumerResource::make($consumer);
}

public function update(
    UpdateConsumerRequest $request,
    Consumer $consumer,
): ConsumerResource {
    $consumer->update($request->validated());

    return ConsumerResource::make($consumer);
}

$request->validated() returns only the fields that have rules. A client that adds "settings": {...} to the body achieves nothing: the key isn’t in the rules, so it isn’t in validated(), and it isn’t fillable either. Never pass $request->all() to a model. That one habit rules out mass assignment as a way in.

Laravel 13 can go one step further and refuse the request. Put the #[FailOnUnknownFields] attribute on a FormRequest and a body with a key that has no rule is a 422, which tells a client about its typo where the default would ignore it. (Laravel documentation: Validation › Failing on Unknown Fields.)

store also sets no status code. Chapter 2 had to ask for a 201 explicitly, because a license isn’t a model. A Resource wrapping a model that was just created answers 201 on its own.

Laravel 13 can shorten the return as well: $consumer->toResource() finds ConsumerResource by its name. This book keeps the explicit form, because it names the class a reader should open next.

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