Ir al contenido principal
Laravel, shipping fast.
// app/Http/Resources/ConsumerResource.php
public function toArray(Request $request): array
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'contact_email' => $this->contact_email,
        'rate_limit' => $this->rate_limit,
        'license_limit' => $this->license_limit,
        'tokens_count' => $this->whenCounted('tokens'),
        'tokens' => TokenResource::collection(
            $this->whenLoaded('tokens'),
        ),
        'created_at' => $this->created_at,
    ];
}

Con un modelo detrás, un Resource puede acceder a las propiedades directamente: $this->name se reenvía al modelo.

Las dos líneas condicionales son la forma en que un Resource y una consulta cooperan. whenCounted('tokens') incluye el recuento solo si la consulta lo pidió con withCount('tokens'). whenLoaded('tokens') incluye los tokens solo si la consulta los cargó de forma anticipada. Si no ocurrió ninguna de las dos cosas, las claves simplemente no aparecen. (Documentación de Laravel: Eloquent: API Resources › Conditional Relationships.)

Esa regla es lo que te protege del problema N+1 del Capítulo 9. Un Resource que lee $this->tokens sin condiciones ejecuta una consulta por consumidor en cuanto se usa en una lista. Un Resource que usa whenLoaded no puede disparar ninguna consulta. El controlador decide qué se carga. El Resource solo lo comunica.

Y settings no está. No está en el Resource, así que nunca sale.

Listar: paginación, filtros, orden

Un endpoint de listado es donde una API respaldada por tablas gana o pierde su rendimiento, y donde llega la mayor parte de la entrada en la que no pensaste. Los parámetros de consulta son entrada. Valídalos como un cuerpo:

// app/Http/Requests/IndexConsumersRequest.php
public function rules(): array
{
    return [
        'search' => ['string', 'max:100'],
        'sort' => [Rule::in(['name', 'created_at'])],
        'direction' => [Rule::in(['asc', 'desc'])],
        'per_page' => ['integer', 'between:1,100'],
    ];
}
// app/Http/Controllers/ConsumerController.php
public function index(
    IndexConsumersRequest $request,
): AnonymousResourceCollection {
    $query = Consumer::query()->withCount('tokens');

    if ($request->filled('search')) {
        $term = $request->validated('search');
        $term = addcslashes($term, '\\%_');
        $query->whereLike('name', "%{$term}%");
    }

    $consumers = $query
        ->orderBy(
            $request->validated('sort', 'name'),
            $request->validated('direction', 'asc'),
        )
        ->paginate($request->integer('per_page', 25));

    return ConsumerResource::collection($consumers);
}

Cada línea de esa clase de request evita algo.

sort es una lista blanca. Un nombre de columna no puede enlazarse como parámetro de consulta del modo en que se enlaza un valor, así que una columna de orden tomada directamente de la petición se interpola en el SQL. Rule::in() es lo que hace seguro orderBy($request->validated('sort')). También significa que un cliente solo puede ordenar por columnas que has indexado.

per_page tiene suelo y techo. Sin el techo, un cliente puede pedir la tabla entera. Sin el suelo, también: el query builder ignora un límite negativo, y vuelven todas las filas.

search tiene una longitud. Un término de búsqueda de diez mil caracteres no es una búsqueda. El controlador además escapa %, _ y la barra invertida, para que quien llama no pueda soltar sus propios comodines contra tu índice.

La respuesta tiene la forma que prometió el Capítulo 2, con los añadidos que solo un paginador puede aportar. Cada elemento está abreviado aquí:

{
    "data": [
        { "id": 1, "name": "Partner A", "tokens_count": 2 },
        { "id": 2, "name": "Provisioning", "tokens_count": 1 }
    ],
    "links": { "next": ".../consumers?page=2", "prev": null },
    "meta": { "current_page": 1, "per_page": 25, "total": 40 }
}

No escribiste nada de links ni de meta. Un consumidor sigue links.next hasta que es null, y nunca tiene que construir una URL.

No se pudo cargar el audio. Inténtalo de nuevo en un momento.