0Pricing
PHP Academy · Lección

Creación de flujos de trabajo basados en eventos

Coordine servicios mediante eventos e idempotencia

Creación de flujos de trabajo basados en eventos es una lección gratuita de PHP Academy en CoddyKit. Esta es la lección 4 de 4. Puedes leer la lección completa abajo gratuitamente — luego la practicas en el navegador con un editor de código integrado y un tutor de IA 24/7. Forma parte de la ruta de aprendizaje de PHP Academy, y tu progreso se sincroniza en la web y la app de CoddyKit. El curso de PHP Academy incluye 4 lecciones en total.

Flujos de trabajo orientados a eventos

Un solo mensaje es sencillo. Un flujo de trabajo —«pedido realizado → reservar existencias → cobrar la tarjeta → enviar → notificar», que abarca varios servicios— es donde el diseño orientado a eventos demuestra su utilidad y donde puede causar problemas si se implementa de forma ingenua.

En esta lección se explican la coreografía frente a la orquestación, el patrón outbox para la publicación atómica, las sagas para la reversión distribuida y la idempotencia que mantiene unido todo el sistema.

Coreografía frente a orquestación

Hay dos formas de coordinar flujos de varios pasos:

  • Coreografía: cada servicio reacciona a los eventos y emite los suyos; no existe un cerebro central. Es débilmente acoplada, pero el flujo general es implícito y difícil de rastrear.
  • Orquestación: un coordinador central indica a cada servicio qué debe hacer a continuación. Es explícita y observable, pero el orquestador se convierte en un punto de acoplamiento.

Como regla general: use coreografía para una distribución sencilla a varios destinos y orquestación cuando el flujo tenga muchos pasos ordenados y necesite un estado visible.

Eventos frente a comandos

Asigne nombres deliberadamente a sus mensajes:

  • Un evento expresa un hecho del pasado: OrderPlaced. Al publicador no le importa quién lo escucha.
  • Un comando solicita una acción futura a un handler específico: ChargeCard.

Los eventos impulsan la coreografía; los comandos impulsan la orquestación. Mezclar la terminología (un «evento» que en realidad espera un único handler) es una fuente común de acoplamiento oculto.

<?php
final class OrderPlaced {
    public function __construct(
        public readonly string $orderId,
        public readonly string $customerId,
        public readonly int $amountCents,
        public readonly string $occurredAt,
    ) {}
}

$e = new OrderPlaced('o-42', 'c-7', 1990, gmdate('c'));
echo json_encode($e), "\n";

El problema de la doble escritura

El error clásico: un handler actualiza la base de datos y publica un mensaje como dos operaciones independientes. Si el proceso muere entre ambas, se produce una inconsistencia: la fila cambió, pero no se envió ningún evento, o sucedió lo contrario.

<?php
// BROKEN: not atomic. A crash between the two lines corrupts state.
function placeOrder(PDO $db, $broker, array $o): void {
    $db->prepare('INSERT INTO orders ...')->execute($o);
    // <-- crash here = row exists but no event ever published
    $broker->publish('OrderPlaced', json_encode($o));
}

El patrón transaccional outbox

La solución es el patrón outbox: dentro de la misma transacción de la base de datos que modifica sus datos, inserte el evento en una tabla outbox. Un proceso de relay independiente lee las filas no publicadas y las envía al broker. Un único commit atómico, sin doble escritura.

<?php
function placeOrder(PDO $db, array $o): void {
    $db->beginTransaction();
    $db->prepare('INSERT INTO orders (id, total) VALUES (?, ?)')
       ->execute([$o['id'], $o['total']]);
    // Same transaction -> atomic with the business write
    $db->prepare('INSERT INTO outbox (id, type, payload) VALUES (?, ?, ?)')
       ->execute([bin2hex(random_bytes(8)), 'OrderPlaced', json_encode($o)]);
    $db->commit();
}

El relay (publicador)

Un worker consulta periódicamente el outbox (o sigue el registro de cambios de la base de datos mediante CDC), publica cada fila y después la marca como enviada. Como el relay puede fallar después de publicar, pero antes de marcar la fila, también ofrece una entrega al menos una vez; esto no supone un problema porque los consumidores son idempotentes.

<?php
function relayOutbox(PDO $db, $broker): void {
    $rows = $db->query(
        'SELECT id, type, payload FROM outbox
         WHERE published_at IS NULL ORDER BY created_at LIMIT 100'
    )->fetchAll(PDO::FETCH_ASSOC);

    foreach ($rows as $r) {
        $broker->publish($r['type'], $r['payload'], messageId: $r['id']);
        $db->prepare('UPDATE outbox SET published_at = now() WHERE id = ?')
           ->execute([$r['id']]);
    }
}

Consumidores idempotentes (de nuevo)

Como tanto el relay como el broker ofrecen una entrega al menos una vez, los handlers posteriores verán duplicados. Cada consumidor registra el ID de los mensajes que ha procesado y descarta inmediatamente las repeticiones: la misma barrera de deduplicación que aprendió antes, aplicada ahora por servicio.

<?php
function onOrderPlaced(PDO $db, string $messageId, array $data): void {
    $db->beginTransaction();
    try {
        $db->prepare('INSERT INTO inbox (message_id) VALUES (?)')
           ->execute([$messageId]); // unique index = dedup
    } catch (PDOException $e) {
        $db->rollBack();
        return; // already handled this message
    }
    reserveStock($data['orderId']);
    $db->commit();
}
function reserveStock(string $id): void {}

Sagas: reversión distribuida

No puede abrir una única transacción ACID entre varios servicios. Una saga modela un flujo de larga duración como una secuencia de transacciones locales, cada una con una acción compensatoria que la deshace. Si falla el paso 3, ejecuta las compensaciones de los pasos 2 y 1 en orden inverso.

Ejemplo: el pago falla después de reservar las existencias → emita ReleaseStock como compensación. No existe una reversión automática: debe diseñar la acción de deshacer para cada paso.

Una saga orquestada

Un orquestador dirige la saga: avanza cuando hay éxito y ejecuta las compensaciones cuando se produce un fallo. Persista el estado de la saga para que pueda reanudarse después de un fallo.

<?php
function handleStepResult(array $saga, string $step, bool $ok, $bus): array {
    if ($ok) {
        $next = ['reserveStock' => 'chargeCard', 'chargeCard' => 'ship'][$step] ?? null;
        if ($next) { $bus->send($next, $saga['orderId']); $saga['state'] = $next; }
        else { $saga['state'] = 'completed'; }
    } else {
        // Run compensations in reverse for whatever already succeeded
        foreach (array_reverse($saga['done']) as $s) {
            $bus->send('compensate.' . $s, $saga['orderId']);
        }
        $saga['state'] = 'compensating';
    }
    return $saga;
}

Tiempos de espera en flujos largos

Un paso de una saga puede simplemente no responder nunca: el servicio de pagos está caído o una aprobación humana no llega. Sin un tiempo de espera, la saga queda bloqueada indefinidamente mientras mantiene las reservas. Persista una fecha límite para cada paso; un planificador busca las sagas vencidas y activa la ruta de fallo o compensación.

<?php
function reapTimedOutSagas(PDO $db, $bus): void {
    $rows = $db->query(
        "SELECT order_id, state FROM sagas
         WHERE state NOT IN ('completed','compensating')
           AND deadline_at < now()"
    )->fetchAll(PDO::FETCH_ASSOC);

    foreach ($rows as $r) {
        echo "Saga {$r['order_id']} timed out at step {$r['state']}\n";
        $bus->send('saga.compensate', $r['order_id']); // trigger rollback
    }
}

Versionado y observabilidad

Los flujos de trabajo duran años; los eventos deben evolucionar de forma segura:

  • Añada un version (o schema) a cada evento; los consumidores deben tolerar campos nuevos desconocidos y no asumir nunca que un campo está presente.
  • Prefiera los cambios aditivos; nunca reutilice el significado de un campo existente.
  • Propague un id de correlación en cada mensaje para poder rastrear una transacción de negocio a través de todos los servicios en sus registros y trazas.

Sin identificadores de correlación, depurar un flujo coreografiado que atraviesa cinco servicios es prácticamente imposible.

<?php
$envelope = [
    'type'          => 'OrderPlaced',
    'version'       => 2,
    'correlationId' => $incoming['correlationId'] ?? bin2hex(random_bytes(8)),
    'occurredAt'    => gmdate('c'),
    'data'          => ['orderId' => 'o-42'],
];
echo json_encode($envelope, JSON_PRETTY_PRINT), "\n";

Comprobación rápida

Evitar el problema de la doble escritura.

Resumen

Ahora puede diseñar flujos de trabajo fiables basados en eventos:

  • Elija la coreografía (eventos) o la orquestación (comandos) según la complejidad del flujo.
  • Resuelva la doble escritura con el outbox transaccional + un relé.
  • Haga que cada consumidor sea idempotente mediante una clave de inbox/deduplicación.
  • Use sagas con acciones compensatorias para realizar reversiones distribuidas.
  • Haga evolucionar los eventos de forma aditiva y propague un id de correlación para garantizar la trazabilidad.

Estos patrones convierten mensajes poco estructurados en procesos de negocio fiables y observables.

Preguntas frecuentes

¿La lección «Creación de flujos de trabajo basados en eventos» es gratis?

Sí — el texto completo de «Creación de flujos de trabajo basados en eventos» es gratis para leer aquí en la web. Para practicarla de forma interactiva (editor de código integrado y tutor de IA 24/7) y desbloquear el resto del curso de PHP Academy, actualiza a CoddyKit PRO. El curso de PHP Academy incluye 4 lecciones en total.

¿Qué aprenderé en «Creación de flujos de trabajo basados en eventos»?

Coordine servicios mediante eventos e idempotencia Practicas PHP Academy con código real que ejecutas directamente en el navegador, y un tutor de IA 24/7 responde tus preguntas mientras trabajas en la lección.

¿Necesito experiencia previa para empezar PHP Academy?

No se requiere experiencia previa. PHP Academy en CoddyKit está estructurado para principiantes hasta estudiantes avanzados, así que puedes empezar aquí o desde el inicio y avanzar a tu ritmo. Esta es la lección 4 de 4.

¿Cuánto tiempo toma la lección «Creación de flujos de trabajo basados en eventos»?

La mayoría de las lecciones de CoddyKit toman alrededor de 5–10 minutos. Cada una es compacta e interactiva, así que avanzas constantemente y retomas exactamente por donde dejaste en la web y la app.

¿Puedo escribir y ejecutar código en esta lección de PHP Academy?

Sí. Cada lección de PHP Academy incluye un editor de código integrado, así que escribes y ejecutas código real directamente en tu navegador y obtienes retroalimentación instantánea de IA — sin configuración local necesaria.

Todas las lecciones de este curso

  1. Por qué usar mensajería asíncrona
  2. Trabajo con RabbitMQ en PHP
  3. Apache Kafka con PHP
  4. Creación de flujos de trabajo basados en eventos
← Volver a PHP Academy