// 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();
});
La primera llamada declara cinco endpoints: index, store, show, update y destroy. Ejecuta php artisan route:list --path=consumers y léelos. Si el framework puede nombrar una ruta, este libro nunca la escribe a mano.
La segunda declara un recurso anidado, los tokens que pertenecen a un consumidor, en /consumers/{consumer}/tokens/{token}. scoped() es la parte que importa. Le dice a Laravel que el {token} de la URL debe pertenecer al {consumer} de la URL. Sin él, /consumers/1/tokens/99 cargaría tan tranquilo el token 99 aunque perteneciera al consumidor 2, y habrías escrito un endpoint que borra credenciales ajenas. (Documentación de Laravel: Controllers › Scoping Nested Resources.)
Route model binding
// app/Http/Controllers/ConsumerController.php
public function show(Consumer $consumer): ConsumerResource
{
return ConsumerResource::make($consumer);
}
En ese método no hay ninguna consulta. El parámetro se llama $consumer, el segmento de la ruta es {consumer} y el tipo es un modelo de Eloquent, así que Laravel busca el registro por su clave antes de que el método se ejecute. Si no existe, el consumidor de tu API recibe un 404 y tu código nunca se llama.
Aquí el framework hace lo que el Capítulo 3 tuvo que construir a mano. Allí, una clave llegaba como cadena y un driver la llevaba al proveedor. Aquí el modelo es la búsqueda. (Documentación de Laravel: Routing › Route Model Binding.)
Escribe Consumer $consumer y nunca int $id seguido de Consumer::findOrFail($id). Hacen lo mismo, pero solo lo segundo depende de que alguien se acuerde de escribirlo.
Crear y actualizar
Dos requests, porque las dos operaciones tienen reglas distintas. Como en el Capítulo 2, authorize() devuelve true en ambos. Quién puede llamar a estos endpoints se decide en el controlador, más adelante en este capítulo.
// 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',
],
];
}
Tres detalles separan una actualización de una creación.
sometimes significa «valida este campo solo si se envió». Una actualización que cambia la dirección de contacto no debería tener que enviar otra vez el nombre. Eso es lo que hace de este endpoint un PATCH: el cliente envía lo que cambia.
ignore() en la regla de unicidad excluye el registro que se está actualizando. Sin él, guardar un consumidor con el nombre que ya tiene falla con «the name has already been taken».
Una restricción de la base de datos respalda la regla. La migración también declaró name como único. La regla de validación da un 422 amable. El índice es lo que hace que sea verdad cuando dos peticiones llegan en el mismo milisegundo. Ten siempre las dos cosas.
Los métodos del controlador siguen tan delgados como en el Capítulo 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() devuelve solo los campos que tienen reglas. Un cliente que añade "settings": {...} al cuerpo no consigue nada: la clave no está en las reglas, así que no está en validated(), y tampoco es asignable. Nunca le pases $request->all() a un modelo. Ese único hábito descarta la asignación en bloque (mass assignment) como vía de entrada.
Laravel 13 puede ir un paso más allá y rechazar la petición. Pon el atributo #[FailOnUnknownFields] en un FormRequest y un cuerpo con una clave sin regla es un 422, lo que le avisa al cliente de su errata donde el comportamiento por defecto la ignoraría. (Documentación de Laravel: Validation › Failing on Unknown Fields.)
store tampoco fija un código de estado. El Capítulo 2 tuvo que pedir un 201 de forma explícita, porque una licencia no es un modelo. Un Resource que envuelve un modelo recién creado responde 201 por su cuenta.
Laravel 13 también puede acortar el retorno: $consumer->toResource() encuentra ConsumerResource por su nombre. Este libro conserva la forma explícita, porque nombra la clase que el lector debería abrir a continuación.