Ir al contenido principal
Laravel, shipping fast.

Capitulo 13

Funcionalidades avanzadas

Julian Beaujardin

Once capítulos después, tienes una API que valida en la frontera, responde de forma consistente, autentica como es debido y falla en voz alta en lugar de en silencio. Esos son los cimientos. Ahora la gente empieza a pedir más: «¿podéis avisarnos cuando cambie una licencia?», «¿podemos subir quinientos dominios de golpe?», «¿podemos buscar en todo?». Cada pregunta suena simple. Cada una, construida sin cuidado, se convierte en lo que te despierta a las dos de la mañana.

Este capítulo cubre cuatro funcionalidades a las que los equipos recurren cuando lo básico ya aburre: webhooks, operaciones por lotes, búsqueda y exportaciones largas. No se limita a esbozarlas: muestra qué cuesta cada una, dónde se rompe bajo carga real y el mecanismo concreto de Laravel que evita que se rompa.

Las funcionalidades avanzadas no son extras atornillados a una API que funciona: son la misma disciplina que ya aplicaste al CRUD, aplicada a problemas más difíciles. Recorta esquinas aquí y las estarás recortando justo donde quien consume no puede ver qué salió mal.

Anadelas cuando lo basico aburra

La regla: no construyas nada de esto hasta que la versión simple haya funcionado, en producción, durante un tiempo. Un sistema de webhooks para una API con tres consumidores es una solución buscando un problema. Un índice de búsqueda para una tabla de doscientas filas es el mismo error. Estas funcionalidades se ganan su presupuesto de complejidad sólo cuando lo simple —consultar periódicamente, filtrar en el cliente, sacar un informe a mano— ha empezado visiblemente a costar más de lo que ahorra.

Una vez llegas ahí, las medias tintas son peores que no construir la funcionalidad. Un webhook que se pierde en silencio al fallar es peor que ningún webhook, porque quien consume construye asumiendo que es fiable. Un endpoint por lotes que agota su tiempo a los doscientos registros le enseña a tu mayor cliente a dejar de confiar en él. Constrúyelas bien, o no las construyas.

Webhooks: entrega que puedes demostrar

Un webhook es una promesa: cuando algo pasa de tu lado, se lo dirás al otro sin que te lo pida. La promesa es fácil de hacer y fácil de romper. El valor de un sistema de webhooks no es el POST: es lo que pasa cuando ese POST falla.

El listener que entrega el webhook es la parte que hay que acertar. Tiene que estar en cola, porque que el endpoint de un suscriptor vaya lento no puede ralentizar la creación de licencias. Tiene que reintentar, porque que su endpoint esté brevemente caído no es un problema tuyo que propagar. Y tiene que distinguir un 4xx de un 5xx, porque significan cosas opuestas.

// app/Listeners/DeliverLicenseWebhookListener.php
final class DeliverLicenseWebhookListener implements ShouldBeEncrypted, ShouldQueue
{
    use InteractsWithQueue;

    public int $tries = 4;

    /** @return array<int, int> */
    public function backoff(): array
    {
        return [5, 15, 30];
    }

    public function handle(LicenseIssuedEvent $event): void
    {
        if (! $event->webhook) {
            return;
        }

        $response = Http::connectTimeout(5)
            ->timeout(15)
            ->retry(2, 250)
            ->acceptJson()
            ->asJson()
            ->post($event->webhook, [
                'event' => 'license.issued',
                'data' => [
                    'key' => $event->license->key,
                    'name' => $event->license->name,
                    'domains' => $event->license->domains,
                ],
            ]);

        // A 4xx means the URL or its auth is wrong, and it will still be
        // wrong on attempt two. A 5xx or a dropped connection is transient
        // and worth the retry.
        if ($response->clientError()) {
            return;
        }

        $response->throw();
    }
}

ShouldBeEncrypted protege la carga y la URL de destino mientras esperan en la cola, ya que una clave de licencia y el endpoint de un suscriptor no son cosas que quieras en texto plano en una tabla. tries y backoff() dan a un suscriptor que falla cuatro intentos repartidos en aproximadamente un minuto antes de que Laravel llame a failed(). clientError() frente a throw() es todo el asunto: un 404 o un 401 es terminal y reintentarlo sólo desperdicia un worker, mientras que un 500 o una conexión caída lanza, y el mecanismo de reintentos de la cola lo recoge automáticamente.

«Entrega que puedes demostrar» significa demostrarla, no afirmarla. Una línea de registro dentro de handle() te dice que se disparó, no si el suscriptor lo recibió, cuántos intentos costó ni qué webhook lleva tres días fallando en silencio. Una tabla webhook_deliveries con columnas de evento, destino, estado de respuesta, intento y momento de entrega lo arregla: escribe una fila antes de la petición y actualízala después. Ahora «¿se disparó el webhook?» es una consulta, y «¿qué suscriptores están fallando ahora mismo?» es un panel en lugar de un ticket de soporte que empieza con «creo que hemos dejado de recibir avisos».