Tworzenie przepływów pracy sterowanych zdarzeniami
Koordynowanie usług za pomocą zdarzeń i idempotentności
Tworzenie przepływów pracy sterowanych zdarzeniami to bezpłatna lekcja PHP Academy na CoddyKit. To lekcja 4 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej PHP Academy, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs PHP Academy zawiera 4 lekcji w sumie.
Przepływy pracy sterowane zdarzeniami
Pojedyncza wiadomość jest prosta. Przepływ pracy — „złożenie zamówienia → zarezerwowanie towaru → obciążenie karty → wysyłka → powiadomienie”, obejmujący kilka usług — to obszar, w którym projektowanie sterowane zdarzeniami pokazuje swoją wartość, ale też przysparza problemów, jeśli zostanie zastosowane naiwnie.
W tej lekcji omówiono choreografię i orkiestrację, wzorzec outbox do atomowego publikowania, sagi do wycofywania zmian rozproszonych oraz idempotencję, która spaja te mechanizmy.
Choreografia a orkiestracja
Istnieją dwa sposoby koordynowania przepływów wieloetapowych:
- Choreografia — każda usługa reaguje na zdarzenia i emituje własne; nie ma centralnego koordynatora. Usługi są luźno powiązane, ale ogólny przebieg jest niejawny i trudny do śledzenia.
- Orkiestracja — centralny koordynator mówi każdej usłudze, co ma zrobić dalej. Przebieg jest jawny i możliwy do monitorowania, ale orkiestrator stanowi punkt sprzężenia.
Praktyczna zasada: choreografię należy stosować przy prostym fan-out, a orkiestrację wtedy, gdy przepływ ma wiele uporządkowanych kroków i wymaga widocznego stanu.
Zdarzenia a polecenia
Nazwy wiadomości należy wybierać świadomie:
- Zdarzenie przedstawia fakt z przeszłości:
OrderPlaced. Wydawca nie musi wiedzieć, kto go nasłuchuje. - Polecenie żąda wykonania przyszłej operacji przez konkretny handler:
ChargeCard.
Zdarzenia napędzają choreografię, a polecenia — orkiestrację. Mieszanie terminologii (zdarzenie, które w rzeczywistości oczekuje jednego handlera) jest częstym źródłem ukrytego sprzężenia.
<?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";Problem podwójnego zapisu
Klasyczny błąd: handler aktualizuje bazę danych i publikuje wiadomość jako dwie osobne operacje. Jeśli proces zakończy się między nimi, powstaje niespójność — wiersz został zmieniony, ale nie wysłano zdarzenia, albo stało się odwrotnie.
<?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));
}Transakcyjny outbox
Rozwiązaniem jest wzorzec outbox: w ramach tej samej transakcji DB, która zmienia dane, należy wstawić zdarzenie do tabeli outbox. Osobny proces przekaźnikowy odczytuje nieopublikowane wiersze i wysyła je do brokera. Jeden atomowy commit, bez podwójnego zapisu.
<?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();
}Przekaźnik (publisher)
Worker odpytuje outbox (albo śledzi dziennik zmian w DB za pośrednictwem CDC), publikuje każdy wiersz, a następnie oznacza go jako wysłany. Ponieważ przekaźnik może zakończyć działanie po publikacji, ale przed oznaczeniem wiersza, sam działa w modelu co najmniej raz — i jest to w porządku, ponieważ konsumenci są idempotentni.
<?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']]);
}
}Idempotentni konsumenci (ponownie)
Ponieważ zarówno przekaźnik, jak i broker działają w modelu co najmniej raz, handlery dalszych etapów zobaczą duplikaty. Każdy konsument zapisuje identyfikator przetworzonej wiadomości i pomija powtórzenia — jest to ta sama bramka deduplikacji, której użyto wcześniej, teraz stosowana osobno w każdej usłudze.
<?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 {}Sagi: rozproszony rollback
Nie można otworzyć jednej transakcji ACID obejmującej wiele usług. Saga modeluje długotrwały przepływ jako sekwencję lokalnych transakcji, z których każda ma akcję kompensującą, która ją cofa. Jeśli krok 3 się nie powiedzie, należy uruchomić akcje kompensujące dla kroków 2 i 1 w odwrotnej kolejności.
Przykład: płatność kończy się niepowodzeniem po zarezerwowaniu towaru → należy wysłać ReleaseStock, aby wykonać kompensację. Nie ma automatycznego wycofania zmian — dla każdego kroku trzeba zaprojektować operację cofającą.
Saga orkiestracyjna
Orkiestrator prowadzi sagę: po sukcesie przechodzi do kolejnego kroku, a po błędzie uruchamia akcje kompensujące. Stan sagi należy zapisywać, aby po awarii można było wznowić jej działanie.
<?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;
}Limity czasu w długich przepływach
Krok sagi może po prostu nigdy nie odesłać odpowiedzi — usługa płatności może być niedostępna albo człowiek może nigdy nie wydać zgody. Bez limitu czasu saga zawiesi się na zawsze, blokując rezerwacje. Należy zapisać termin dla każdego kroku; harmonogram skanuje przeterminowane sagi i uruchamia ścieżkę obsługi błędu lub kompensacji.
<?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
}
}Wersjonowanie i obserwowalność
Przepływy pracy działają przez lata; zdarzenia muszą ewoluować bezpiecznie:
- Dodawać do każdego zdarzenia
version(lub schemat); odbiorcy muszą tolerować nieznane nowe pola i nigdy nie zakładać obecności konkretnego pola. - Preferować zmiany addytywne; nigdy nie zmieniać znaczenia istniejącego pola.
- Przekazywać identyfikator korelacji w każdej wiadomości, aby można było śledzić jedną transakcję biznesową we wszystkich usługach za pomocą logów i śledzenia.
Bez identyfikatorów korelacji debugowanie przepływu sterowanego choreografią, który przebiega przez pięć usług, jest niemal niemożliwe.
<?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";Szybki test
Unikanie problemu podwójnego zapisu.
Podsumowanie
Mogą Państwo teraz projektować niezawodne przepływy pracy sterowane zdarzeniami:
- Dla każdego przepływu wybierać choreografię (zdarzenia) lub orkiestrację (polecenia), zależnie od jego złożoności.
- Rozwiązywać problem podwójnego zapisu za pomocą transakcyjnego outboxa i przekaźnika.
- Zapewniać idempotentność każdego odbiorcy za pomocą skrzynki inbox lub klucza deduplikacji.
- Używać sag z działaniami kompensującymi do wycofywania zmian w systemie rozproszonym.
- Wersjonować zdarzenia przez dodawanie pól i przekazywać identyfikator korelacji, aby zapewnić możliwość śledzenia.
Wzorce te przekształcają luźne komunikaty w niezawodne i obserwowalne procesy biznesowe.
Często zadawane pytania
Czy lekcja „Tworzenie przepływów pracy sterowanych zdarzeniami” jest bezpłatna?
Tak — pełny tekst „Tworzenie przepływów pracy sterowanych zdarzeniami” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu PHP Academy, przejdź na CoddyKit PRO. Kurs PHP Academy zawiera 4 lekcji w sumie.
Co nauczysz się w „Tworzenie przepływów pracy sterowanych zdarzeniami”?
Koordynowanie usług za pomocą zdarzeń i idempotentności Ćwiczysz PHP Academy z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.
Czy potrzebuję doświadczenia, aby zacząć PHP Academy?
Nie wymagamy żadnego doświadczenia. PHP Academy w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 4 z 4.
Ile czasu zajmuje lekcja „Tworzenie przepływów pracy sterowanych zdarzeniami”?
Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.
Czy mogę pisać i uruchamiać kod w tej lekcji PHP Academy?
Tak. Każda lekcja PHP Academy zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.
Wszystkie lekcje w tym kursie
- Dlaczego warto używać komunikacji asynchronicznej
- Praca z RabbitMQ w PHP
- Apache Kafka z PHP
- Tworzenie przepływów pracy sterowanych zdarzeniami