0Pricing
PHP Academy · Lezione

Creare flussi di lavoro basati sugli eventi

Coordini i servizi tramite eventi e idempotenza

Creare flussi di lavoro basati sugli eventi è una lezione PHP Academy gratuita su CoddyKit. Questa è la lezione 4 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento PHP Academy, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso PHP Academy include 4 lezioni in totale.

Flussi di lavoro basati sugli eventi

Un singolo messaggio è semplice. Un flusso di lavoro — "ordine effettuato → riserva scorte → addebita carta → spedisci → invia notifica", distribuito su diversi servizi — è il contesto in cui la progettazione basata sugli eventi dà il meglio di sé, ma può diventare insidiosa se applicata ingenuamente.

Questa lezione tratta la coreografia e l'orchestrazione, il pattern outbox per la pubblicazione atomica, le saghe per il rollback distribuito e l'idempotenza che tiene insieme tutto il sistema.

Coreografia vs orchestrazione

Due modi per coordinare flussi composti da più passaggi:

  • Coreografia — ogni servizio reagisce agli eventi ed emette i propri; non esiste un coordinatore centrale. L'accoppiamento è ridotto, ma il flusso complessivo è implicito e difficile da tracciare.
  • Orchestrazione — un coordinatore centrale indica a ogni servizio quale azione eseguire. È esplicita e osservabile, ma l'orchestratore costituisce un punto di accoppiamento.

Regola pratica: utilizzi la coreografia per semplici fan-out e l'orchestrazione quando un flusso contiene molti passaggi ordinati e richiede uno stato visibile.

Eventi vs comandi

Assegni ai messaggi nomi scelti con criterio:

  • Un evento esprime un fatto avvenuto nel passato: OrderPlaced. Al publisher non interessa chi lo ascolta.
  • Un comando richiede un'azione futura a uno specifico handler: ChargeCard.

Gli eventi guidano la coreografia; i comandi guidano l'orchestrazione. Mescolare la terminologia (un "evento" che in realtà si aspetta un solo handler) è una causa comune di accoppiamento nascosto.

<?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";

Il problema della doppia scrittura

Il bug classico: un gestore aggiorna il database e pubblica un messaggio come due operazioni separate. Se il processo termina tra le due operazioni, si verifica un'incoerenza: la riga è cambiata ma non è stato inviato alcun evento, o viceversa.

<?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));
}

L'outbox transazionale

La soluzione è il pattern outbox: all'interno della stessa transazione DB che modifica i dati, inserisca l'evento in una tabella outbox. Un processo relay separato legge le righe non pubblicate e le invia al broker. Un solo commit atomico, nessuna doppia scrittura.

<?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();
}

Il relay (publisher)

Un worker interroga l'outbox (oppure segue il log delle modifiche del DB tramite CDC), pubblica ogni riga e poi la contrassegna come inviata. Poiché il relay può terminare dopo la pubblicazione ma prima della marcatura, anche il relay opera con semantica at-least-once: va bene, perché i consumer sono idempotenti.

<?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']]);
    }
}

Consumer idempotenti (di nuovo)

Poiché sia il relay sia il broker operano con semantica at-least-once, i gestori downstream vedranno duplicati. Ogni consumer registra l'ID del messaggio elaborato e ignora immediatamente le ripetizioni: lo stesso meccanismo di deduplicazione appreso in precedenza, ora applicato a ogni servizio.

<?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 {}

Saga: rollback distribuito

Non è possibile aprire un'unica transazione ACID tra i servizi. Una saga modella un flusso di lunga durata come una sequenza di transazioni locali, ognuna con una azione compensativa che la annulla. Se il passaggio 3 fallisce, esegua le compensazioni dei passaggi 2 e 1 in ordine inverso.

Ad esempio, se il pagamento fallisce dopo la prenotazione delle scorte, emetta ReleaseStock per compensare. Non esiste alcun rollback automatico: deve progettare l'annullamento per ogni passaggio.

Una saga orchestrata

Un orchestratore guida la saga: avanza in caso di successo e invia le compensazioni in caso di errore. Renda persistente lo stato della saga, così che possa riprendere dopo un arresto anomalo.

<?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;
}

Timeout nei flussi lunghi

Un passaggio di una saga potrebbe semplicemente non inviare mai una risposta: il servizio di pagamento è inattivo oppure un'approvazione umana non arriva. Senza un timeout, la saga resta bloccata per sempre mantenendo le prenotazioni. Salvi una scadenza per ogni passaggio; uno scheduler cerca le saghe oltre la scadenza e attiva il percorso di errore/compensazione.

<?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
    }
}

Versionamento e osservabilità

I flussi di lavoro restano in funzione per anni; gli eventi devono evolversi in sicurezza:

  • Aggiunga un version (o uno schema) a ogni evento; i consumer tollerano i nuovi campi sconosciuti e non devono mai presumere che un campo sia presente.
  • Preferisca modifiche additive; non riutilizzi mai un campo esistente per un significato diverso.
  • Propaghi un ID di correlazione in ogni messaggio, così può tracciare una singola transazione aziendale attraverso tutti i servizi nei log e nel tracing.

Senza ID di correlazione, il debug di un flusso coordinato tramite eventi che attraversa cinque servizi è quasi impossibile.

<?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";

Verifica rapida

Come evitare il problema della doppia scrittura.

Riepilogo

Ora è in grado di progettare flussi di lavoro affidabili basati sugli eventi:

  • Scelga la coreografia (eventi) o l'orchestrazione (comandi) in base alla complessità del flusso.
  • Risolva il problema della doppia scrittura con il transactional outbox e un relay.
  • Renda ogni consumer idempotente tramite una chiave inbox/deduplicazione.
  • Utilizzi le saghe con azioni compensative per il rollback distribuito.
  • Faccia evolvere gli eventi in modo additivo e propaghi un ID di correlazione per la tracciabilità.

Questi pattern trasformano messaggi non strutturati in processi aziendali affidabili e osservabili.

Domande Frequenti

La lezione «Creare flussi di lavoro basati sugli eventi» è gratuita?

Sì — il testo completo di «Creare flussi di lavoro basati sugli eventi» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso PHP Academy, passa a CoddyKit PRO. Il corso PHP Academy include 4 lezioni in totale.

Cosa imparerò in «Creare flussi di lavoro basati sugli eventi»?

Coordini i servizi tramite eventi e idempotenza Eserciti PHP Academy con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.

Ho bisogno di esperienza per iniziare PHP Academy?

Non è richiesta alcuna esperienza precedente. PHP Academy su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 4 di 4.

Quanto tempo richiede la lezione «Creare flussi di lavoro basati sugli eventi»?

La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.

Posso scrivere ed eseguire codice in questa lezione PHP Academy?

Sì. Ogni lezione PHP Academy include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.

Tutte le lezioni di questo corso

  1. Perché usare la messaggistica asincrona
  2. Utilizzare RabbitMQ in PHP
  3. Apache Kafka con PHP
  4. Creare flussi di lavoro basati sugli eventi
← Torna a PHP Academy