External Events
Opis ogólny
External Events to mechanizm umożliwiający integrację pomiędzy modułami, w szczególności pluginami wdrożeniowymi u klienta. Pozwala na wyzwalanie akcji (np. synchronizacja, powiadomienia, webhooki) w odpowiedzi na określone zdarzenia domenowe lub systemowe.
- Cel: Umożliwienie komunikacji i automatyzacji procesów pomiędzy WiseB2B a innymi systemami (ERP, CRM, WMS, platformy e-commerce, narzędzia BI), poprzez dodakowy kod (pluginy) realizujacy dodatkowe funkcje biznesowe.
- Problemy rozwiązywane:
- Separacja kodu pomiędzy modułami systemu
- możłiwość separacji przepływu procesów w sytuacjach diagnostycznych
- Automatyzacja przepływu danych i procesów biznesowych.
- Główne funkcje:
- Emisja zdarzeń domenowych .
- Obsługa listenerów i integratorów zewnętrznych.
- Konfigurowalność i rozszerzalność katalogu eventów.
Przypadki użycia
- Synchronizacja zamówień z systemem ERP
- Po zmianie statusu zamówienia generowany jest event. W ramach pluginu można doknać subskrypcji w lisnerze, który zrealizuje integrację z ERP
- Przykład:
EventManager::instance()->post(new OrderStatusHasChangedEvent($orderId, $oldStatus, $newStatus));
- Powiadomienia o zmianach w danych klienta
- Zmiana danych klienta (np. NIP) generuje event, który jest przekazywany do CRM.
- Przykład:
EventManager::instance()->post(new ClientTaxNumberHasChangedEvent($client));
- Automatyzacja procesów logistycznych
- Zmiana stanu magazynowego produktu wyzwala event, który synchronizuje dane z WMS.
- Przykład:
EventManager::instance()->post(new ProductStockHasChanged($productId, ...));
Struktura techniczna
Mechanizm External Events opiera się na:
- Bazowych klasach eventów (np.
ExternalDomainEvent
,AbstractEntityHasChangedEvent
) - Listenerach (nasłuchujących) – klasy reagujące na eventy
- EventManagerze – centralnym dispatcherze zdarzeń
Przykład eventu:
use Wise\Core\Domain\Event\AbstractEntityHasChangedEvent;
class ProductStatusHasChangedEvent extends AbstractEntityHasChangedEvent
{
public const NAME = 'product.status.changed';
public function __construct(
private int $productId,
private string $oldStatus,
private string $newStatus,
?int $id = null,
?array $data = null,
?string $entityClass = null,
?string $idExternal = null,
) {
parent::__construct($id, $data, $entityClass, $idExternal);
}
public function getProductId(): int { return $this->productId; }
public function getOldStatus(): string { return $this->oldStatus; }
public function getNewStatus(): string { return $this->newStatus; }
}
Przykład listenera:
public function onProductStatusChanged(ProductStatusHasChangedEvent $event): void
{
$payload = [
'productId' => $event->getProductId(),
'oldStatus' => $event->getOldStatus(),
'newStatus' => $event->getNewStatus(),
];
$this->externalApiClient->sendProductStatusUpdate($payload);
}
Zasada działania
- Wejście: Zdarzenie domenowe (obiekt eventu, np.
OrderStatusHasChangedEvent
). - Wyjście: Akcja integracyjna (np. webhook, synchronizacja, powiadomienie).
- Mechanizm:
- W kodzie domenowym wywoływany jest event przez
EventManager::instance()->post($event)
. - EventManager przekazuje event do zarejestrowanych listenerów.
- Listener realizuje integrację (np. wywołuje API, wysyła webhook, loguje zdarzenie).
- W kodzie domenowym wywoływany jest event przez
- Obsługa asynchroniczna:
- Możliwe jest kolejkowanie i asynchroniczna obsługa eventów (np. przez workerów, message queue).
Fragment kodu wywołania eventu:
public function setTaxNumber(?string $taxNumber): self
{
if($this->taxNumber !== $taxNumber){
$this->taxNumber = $taxNumber;
EventManager::instance()->post(new ClientTaxNumberHasChangedEvent($this));
}
return $this;
}
Integracje i zależności
- Systemy zewnętrzne: ERP, CRM, WMS, platformy e-commerce, narzędzia BI.
- Listenery – klasy integrujące eventy z zewnętrznymi API.
- EventManager – centralny dispatcher eventów.
- Konfiguracja eventów – możliwość definiowania, które eventy są emitowane i obsługiwane.
Przykład integracji:
public function onOrderStatusChanged(OrderStatusHasChangedEvent $event): void
{
$this->erpApi->updateOrderStatus([
'orderId' => $event->getOrderId(),
'oldStatus' => $event->getOldStatus(),
'newStatus' => $event->getNewStatus(),
]);
}
Standardowe eventy domenowe
W każdej domenie systemu WiseB2B standardowo tworzone są eventy, które dziedziczą po klasach bazowych: AbstractEntityHasCreatedEvent
, AbstractEntityHasChangedEvent
, AbstractEntityHasDeletedEvent
. Te eventy są kluczowe dla obsługi zdarzeń związanych z cyklem życia encji.
AbstractEntityHasCreatedEvent
Eventy dziedziczące po AbstractEntityHasCreatedEvent
są emitowane po utworzeniu nowej encji. Klasa bazowa zapewnia podstawowe pola i metody do obsługi tego typu zdarzeń.
Przykład deklaracji eventu dla encji Client:
class ClientHasCreatedEvent extends AbstractEntityHasCreatedEvent
{
public const NAME = 'client.created';
public function __construct(
private Client $client,
?int $id = null,
?array $data = null,
?string $entityClass = null,
?string $idExternal = null,
) {
parent::__construct($id, $data, $entityClass, $idExternal);
}
public function getClient(): Client { return $this->client; }
}
AbstractEntityHasChangedEvent
Eventy dziedziczące po AbstractEntityHasChangedEvent
są emitowane po modyfikacji istniejącej encji. Klasa bazowa zapewnia mechanizmy do przechowywania informacji o zmienionych polach i wartościach.
Przykład deklaracji eventu dla encji Product:
class ProductHasChangedEvent extends AbstractEntityHasChangedEvent
{
public const NAME = 'product.changed';
public function __construct(
private Product $product,
?int $id = null,
?array $data = null,
?string $entityClass = null,
?string $idExternal = null,
) {
parent::__construct($id, $data, $entityClass, $idExternal);
}
public function getProduct(): Product { return $this->product; }
}
Przykład użycia:
public function update(array $data): self
{
$oldName = $this->name;
$this->name = $data['name'] ?? $this->name;
if ($oldName !== $this->name) {
EventManager::instance()->post(new ProductHasChangedEvent($this));
}
return $this;
}
AbstractEntityHasDeletedEvent
Eventy dziedziczące po AbstractEntityHasDeletedEvent
są emitowane po usunięciu encji. Te eventy mogą być wykorzystywane do synchronizacji danych w systemach zewnętrznych lub wykonywania operacji czyszczących.
Przykład deklaracji eventu dla encji Order:
class OrderHasDeletedEvent extends AbstractEntityHasDeletedEvent
{
public const NAME = 'order.deleted';
public function __construct(
private int $orderId,
private array $orderData,
?int $id = null,
?array $data = null,
?string $entityClass = null,
?string $idExternal = null,
) {
parent::__construct($id, $data, $entityClass, $idExternal);
}
public function getOrderId(): int { return $this->orderId; }
public function getOrderData(): array { return $this->orderData; }
}
Przykład emisji eventu:
public function delete(Order $order): void
{
$orderData = [
'id' => $order->getId(),
'number' => $order->getNumber(),
'clientId' => $order->getClientId(),
// inne istotne dane zamówienia
];
$this->orderRepository->delete($order);
EventManager::instance()->post(new OrderHasDeletedEvent($order->getId(), $orderData));
}
Rozszerzanie i dostosowanie
- Dodanie nowego eventu:
- Utwórz klasę eventu dziedziczącą po
ExternalDomainEvent
. - Zaimplementuj wymagane pola i metody.
- Zarejestruj listener obsługujący event.
- Utwórz klasę eventu dziedziczącą po
- Hooki/eventy:
- Możliwość subskrypcji na dowolne eventy domenowe.
- Pluginy:
- Możliwość podpięcia własnych integratorów (np. webhooki, API, logi).
Przykład rozszerzenia:
class ClientCreditLimitExceededEvent extends ExternalDomainEvent
{
public const NAME = 'client.credit_limit.exceeded';
public function __construct(
private int $clientId,
private float $currentCredit,
private float $creditLimit
) {
parent::__construct($clientId);
}
// ...gettery
}
Testowanie
- Testy jednostkowe – testowanie emisji eventów i reakcji listenerów (mockowanie zależności).
- Testy integracyjne – testowanie pełnego przepływu event → listener → integracja.
Przykład testu jednostkowego listenera:
public function testOnProductStatusChangedSendsPayload()
{
$apiClient = $this->createMock(ExternalApiClient::class);
$listener = new ProductStatusChangedListener($apiClient);
$event = new ProductStatusHasChangedEvent(1, 'unavailable', 'available');
$apiClient->expects($this->once())
->method('sendProductStatusUpdate')
->with([
'productId' => 1,
'oldStatus' => 'unavailable',
'newStatus' => 'available',
]);
$listener->onProductStatusChanged($event);
}
Typowe problemy i debugowanie
- Brak obsługi eventu – event nie jest przechwytywany przez listenera (sprawdź rejestrację listenera).
- Błędy integracji z API – nieprawidłowe payloady, błędy autoryzacji, timeouty.
- Problemy z asynchronicznością – eventy nie są obsługiwane w odpowiedniej kolejności lub z opóźnieniem.
- Debugowanie:
- Loguj wywołania eventów i payloady.
- Używaj narzędzi do monitorowania kolejek i webhooków.
- Testuj obsługę błędów i retry mechanizmy.
Przykłady kodu
Emitowanie eventu po zmianie statusu zamówienia:
EventManager::instance()->post(new OrderStatusHasChangedEvent($orderId, $oldStatus, $newStatus));
Listener obsługujący event i wywołujący API:
public function onOrderStatusChanged(OrderStatusHasChangedEvent $event): void
{
$this->erpApi->updateOrderStatus([
'orderId' => $event->getOrderId(),
'oldStatus' => $event->getOldStatus(),
'newStatus' => $event->getNewStatus(),
]);
}
Definicja własnego eventu:
class ProductPriceHasChangedEvent extends AbstractEntityHasChangedEvent
{
public const NAME = 'product.price.changed';
public function __construct(
private int $productId,
private float $oldPrice,
private float $newPrice
) {
parent::__construct($productId);
}
// ...gettery
}
Przykład payloadu eventu:
{
"productId": 555,
"oldPrice": 5999.99,
"newPrice": 5799.99
}