Skip to main content

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

  1. 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));
  1. 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));
  1. 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:
    1. W kodzie domenowym wywoływany jest event przez EventManager::instance()->post($event).
    2. EventManager przekazuje event do zarejestrowanych listenerów.
    3. Listener realizuje integrację (np. wywołuje API, wysyła webhook, loguje zdarzenie).
  • 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:
    1. Utwórz klasę eventu dziedziczącą po ExternalDomainEvent.
    2. Zaimplementuj wymagane pola i metody.
    3. Zarejestruj listener obsługujący event.
  • 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
}