Event-Driven Architecture (EDA) adalah pola arsitektur perangkat lunak yang berfokus pada produksi, deteksi, konsumsi, dan reaksi terhadap events. NexaUI menyediakan sistem event yang powerful untuk membangun aplikasi yang loosely coupled dan scalable.
Dalam EDA, komponen berkomunikasi melalui events, bukan melalui panggilan langsung. Ini memungkinkan komponen untuk berevolusi secara independen dan memudahkan pengembangan sistem yang kompleks.
Event adalah notifikasi bahwa sesuatu yang penting telah terjadi dalam sistem. Events biasanya merepresentasikan perubahan state atau tindakan yang telah dilakukan.
Di NexaUI, events direpresentasikan sebagai objek yang meng-extend class NexaEvent
:
declare(strict_types=1);
namespace App\Events;
use App\System\Events\NexaEvent;
use App\Models\Domain\User;
class UserRegistered extends NexaEvent
{
private User $user;
public function __construct(User $user)
{
parent::__construct();
$this->user = $user;
}
public function getUser(): User
{
return $this->user;
}
}
Event emitter adalah komponen yang menghasilkan events. Di NexaUI, hampir semua komponen dapat menjadi event emitter.
declare(strict_types=1);
namespace App\Services;
use App\System\Events\EventDispatcherInterface;
use App\Events\UserRegistered;
use App\Models\Domain\User;
use App\Models\Repositories\UserRepository;
class UserService
{
private UserRepository $userRepository;
private EventDispatcherInterface $eventDispatcher;
public function __construct(
UserRepository $userRepository,
EventDispatcherInterface $eventDispatcher
) {
$this->userRepository = $userRepository;
$this->eventDispatcher = $eventDispatcher;
}
public function registerUser(string $name, string $email, string $password): User
{
$user = new User(
$this->userRepository->nextIdentity(),
$name,
$email,
password_hash($password, PASSWORD_DEFAULT)
);
$this->userRepository->save($user);
// Dispatch event
$this->eventDispatcher->dispatch(new UserRegistered($user));
return $user;
}
}
Event listener adalah komponen yang mendengarkan events tertentu dan bereaksi terhadapnya.
Di NexaUI, listeners didefinisikan sebagai class yang mengimplementasikan interface
EventListenerInterface
.
declare(strict_types=1);
namespace App\Listeners;
use App\System\Events\EventListenerInterface;
use App\Events\UserRegistered;
use App\Services\EmailService;
class SendWelcomeEmail implements EventListenerInterface
{
private EmailService $emailService;
public function __construct(EmailService $emailService)
{
$this->emailService = $emailService;
}
public function handle($event): void
{
if (!$event instanceof UserRegistered) {
return;
}
$user = $event->getUser();
$this->emailService->sendEmail(
$user->getEmail(),
'Welcome to NexaUI',
'welcome-email',
['user' => $user]
);
}
}
Event dispatcher bertanggung jawab untuk mengirimkan events ke listeners yang terdaftar. NexaUI menyediakan implementasi default dari event dispatcher.
declare(strict_types=1);
namespace App\System\Events;
class NexaEventDispatcher implements EventDispatcherInterface
{
private array $listeners = [];
public function addListener(string $eventName, $listener): void
{
if (!isset($this->listeners[$eventName])) {
$this->listeners[$eventName] = [];
}
$this->listeners[$eventName][] = $listener;
}
public function dispatch(object $event): void
{
$eventName = get_class($event);
if (!isset($this->listeners[$eventName])) {
return;
}
foreach ($this->listeners[$eventName] as $listener) {
if (is_callable($listener)) {
$listener($event);
} elseif (is_object($listener) && method_exists($listener, 'handle')) {
$listener->handle($event);
}
}
}
}
Domain events merepresentasikan kejadian penting dalam domain bisnis. Mereka biasanya dipicu oleh perubahan state pada entity atau aggregate.
declare(strict_types=1);
namespace App\Domain\Events;
use App\System\Events\NexaDomainEvent;
use App\Domain\Order;
class OrderPlaced extends NexaDomainEvent
{
private Order $order;
public function __construct(Order $order)
{
parent::__construct();
$this->order = $order;
}
public function getOrder(): Order
{
return $this->order;
}
}
Application events merepresentasikan kejadian yang terkait dengan aplikasi, seperti login, logout, atau perubahan konfigurasi.
declare(strict_types=1);
namespace App\Events;
use App\System\Events\NexaApplicationEvent;
use App\Models\Domain\User;
class UserLoggedIn extends NexaApplicationEvent
{
private User $user;
private string $ipAddress;
public function __construct(User $user, string $ipAddress)
{
parent::__construct();
$this->user = $user;
$this->ipAddress = $ipAddress;
}
public function getUser(): User
{
return $this->user;
}
public function getIpAddress(): string
{
return $this->ipAddress;
}
}
System events merepresentasikan kejadian yang terkait dengan infrastruktur sistem, seperti startup, shutdown, atau error.
declare(strict_types=1);
namespace App\System\Events;
class ApplicationStarted extends NexaSystemEvent
{
private string $environment;
private float $startTime;
public function __construct(string $environment, float $startTime)
{
parent::__construct();
$this->environment = $environment;
$this->startTime = $startTime;
}
public function getEnvironment(): string
{
return $this->environment;
}
public function getStartTime(): float
{
return $this->startTime;
}
}
Di NexaUI, Anda dapat mendaftarkan event listeners di beberapa tempat:
Anda dapat mendaftarkan listeners di file konfigurasi config/events.php
:
return [
// Event => [Listeners]
App\Events\UserRegistered::class => [
App\Listeners\SendWelcomeEmail::class,
App\Listeners\CreateUserProfile::class,
],
App\Events\UserLoggedIn::class => [
App\Listeners\LogUserActivity::class,
App\Listeners\UpdateLastLoginTime::class,
],
];
Anda juga dapat mendaftarkan listeners secara programatis:
// Di bootstrap aplikasi
$eventDispatcher = $container->get(EventDispatcherInterface::class);
// Mendaftarkan class listener
$eventDispatcher->addListener(
UserRegistered::class,
$container->get(SendWelcomeEmail::class)
);
// Mendaftarkan closure
$eventDispatcher->addListener(
UserRegistered::class,
function (UserRegistered $event) {
// Handle event
$user = $event->getUser();
// ...
}
);
NexaUI mendukung pemrosesan event secara asynchronous menggunakan queue system. Ini berguna untuk operasi yang membutuhkan waktu lama atau tidak kritis terhadap waktu.
return [
App\Events\UserRegistered::class => [
// Sync listener
App\Listeners\CreateUserProfile::class,
// Async listeners
[App\Listeners\SendWelcomeEmail::class, 'async' => true],
[App\Listeners\NotifyAdminAboutNewUser::class, 'async' => true, 'queue' => 'notifications'],
],
];
Async listeners harus mengimplementasikan interface ShouldQueue
:
declare(strict_types=1);
namespace App\Listeners;
use App\System\Events\EventListenerInterface;
use App\System\Events\ShouldQueue;
use App\Events\UserRegistered;
use App\Services\EmailService;
class SendWelcomeEmail implements EventListenerInterface, ShouldQueue
{
private EmailService $emailService;
public function __construct(EmailService $emailService)
{
$this->emailService = $emailService;
}
public function handle($event): void
{
if (!$event instanceof UserRegistered) {
return;
}
$user = $event->getUser();
$this->emailService->sendEmail(
$user->getEmail(),
'Welcome to NexaUI',
'welcome-email',
['user' => $user]
);
}
}
Event Sourcing adalah pola di mana perubahan state aplikasi disimpan sebagai sequence
dari events. NexaUI menyediakan dukungan untuk Event Sourcing melalui komponen
NexaEventStore
.
Event Store adalah repository khusus untuk menyimpan dan mengambil events:
declare(strict_types=1);
namespace App\System\Events;
interface EventStoreInterface
{
public function append(string $aggregateId, string $aggregateType, array $events): void;
public function getEvents(string $aggregateId, string $aggregateType): array;
public function getAllEvents(): array;
}
Aggregate yang menggunakan Event Sourcing harus mengimplementasikan interface
EventSourcedInterface
:
declare(strict_types=1);
namespace App\Domain;
use App\System\Domain\NexaAggregate;
use App\System\Events\EventSourcedInterface;
use App\Domain\Events\OrderPlaced;
use App\Domain\Events\OrderItemAdded;
use App\Domain\Events\OrderPaid;
class Order extends NexaAggregate implements EventSourcedInterface
{
private string $id;
private string $customerId;
private array $items = [];
private bool $isPaid = false;
private array $uncommittedEvents = [];
public static function create(string $id, string $customerId): self
{
$order = new self();
$order->applyEvent(new OrderPlaced($id, $customerId));
return $order;
}
public function addItem(string $productId, int $quantity, float $price): void
{
$this->applyEvent(new OrderItemAdded($this->id, $productId, $quantity, $price));
}
public function pay(): void
{
if ($this->isPaid) {
throw new \DomainException('Order is already paid');
}
$this->applyEvent(new OrderPaid($this->id));
}
public function applyEvent(object $event): void
{
$this->apply($event);
$this->uncommittedEvents[] = $event;
}
public function getUncommittedEvents(): array
{
return $this->uncommittedEvents;
}
public function clearUncommittedEvents(): void
{
$this->uncommittedEvents = [];
}
private function apply(object $event): void
{
if ($event instanceof OrderPlaced) {
$this->id = $event->getOrderId();
$this->customerId = $event->getCustomerId();
} elseif ($event instanceof OrderItemAdded) {
$this->items[] = [
'product_id' => $event->getProductId(),
'quantity' => $event->getQuantity(),
'price' => $event->getPrice(),
];
} elseif ($event instanceof OrderPaid) {
$this->isPaid = true;
}
}
public static function reconstituteFromEvents(array $events): self
{
$order = new self();
foreach ($events as $event) {
$order->apply($event);
}
return $order;
}
}
Contoh penggunaan Event Sourcing di service:
declare(strict_types=1);
namespace App\Services;
use App\Domain\Order;
use App\System\Events\EventStoreInterface;
use App\System\Events\EventDispatcherInterface;
class OrderService
{
private EventStoreInterface $eventStore;
private EventDispatcherInterface $eventDispatcher;
public function __construct(
EventStoreInterface $eventStore,
EventDispatcherInterface $eventDispatcher
) {
$this->eventStore = $eventStore;
$this->eventDispatcher = $eventDispatcher;
}
public function createOrder(string $orderId, string $customerId): Order
{
$order = Order::create($orderId, $customerId);
// Simpan events ke event store
$this->eventStore->append($orderId, Order::class, $order->getUncommittedEvents());
// Dispatch events
foreach ($order->getUncommittedEvents() as $event) {
$this->eventDispatcher->dispatch($event);
}
$order->clearUncommittedEvents();
return $order;
}
public function getOrder(string $orderId): Order
{
// Ambil events dari event store
$events = $this->eventStore->getEvents($orderId, Order::class);
// Rekonstruksi order dari events
return Order::reconstituteFromEvents($events);
}
public function addItemToOrder(string $orderId, string $productId, int $quantity, float $price): Order
{
$order = $this->getOrder($orderId);
$order->addItem($productId, $quantity, $price);
// Simpan events baru ke event store
$this->eventStore->append($orderId, Order::class, $order->getUncommittedEvents());
// Dispatch events
foreach ($order->getUncommittedEvents() as $event) {
$this->eventDispatcher->dispatch($event);
}
$order->clearUncommittedEvents();
return $order;
}
}
CQRS adalah pola yang memisahkan operasi baca (queries) dari operasi tulis (commands).
NexaUI mendukung CQRS melalui komponen CommandBus
dan QueryBus
.
Command merepresentasikan intensi untuk mengubah state sistem:
declare(strict_types=1);
namespace App\Commands;
use App\System\Commands\Command;
class CreateOrder implements Command
{
private string $customerId;
private array $items;
public function __construct(string $customerId, array $items)
{
$this->customerId = $customerId;
$this->items = $items;
}
public function getCustomerId(): string
{
return $this->customerId;
}
public function getItems(): array
{
return $this->items;
}
}
Command handler bertanggung jawab untuk mengeksekusi command:
declare(strict_types=1);
namespace App\CommandHandlers;
use App\System\Commands\CommandHandler;
use App\Commands\CreateOrder;
use App\Services\OrderService;
class CreateOrderHandler implements CommandHandler
{
private OrderService $orderService;
public function __construct(OrderService $orderService)
{
$this->orderService = $orderService;
}
public function handle(CreateOrder $command): void
{
$orderId = uniqid('order_');
$customerId = $command->getCustomerId();
$order = $this->orderService->createOrder($orderId, $customerId);
foreach ($command->getItems() as $item) {
$this->orderService->addItemToOrder(
$orderId,
$item['product_id'],
$item['quantity'],
$item['price']
);
}
}
}
Query merepresentasikan permintaan untuk mendapatkan data:
declare(strict_types=1);
namespace App\Queries;
use App\System\Queries\Query;
class GetOrderDetails implements Query
{
private string $orderId;
public function __construct(string $orderId)
{
$this->orderId = $orderId;
}
public function getOrderId(): string
{
return $this->orderId;
}
}
Query handler bertanggung jawab untuk mengeksekusi query dan mengembalikan hasil:
declare(strict_types=1);
namespace App\QueryHandlers;
use App\System\Queries\QueryHandler;
use App\Queries\GetOrderDetails;
use App\ReadModels\OrderDetailsRepository;
class GetOrderDetailsHandler implements QueryHandler
{
private OrderDetailsRepository $repository;
public function __construct(OrderDetailsRepository $repository)
{
$this->repository = $repository;
}
public function handle(GetOrderDetails $query): array
{
return $this->repository->getOrderDetails($query->getOrderId());
}
}
Di controller:
declare(strict_types=1);
namespace App\Controllers;
use App\System\NexaController;
use App\System\Commands\CommandBus;
use App\System\Queries\QueryBus;
use App\Commands\CreateOrder;
use App\Queries\GetOrderDetails;
class OrderController extends NexaController
{
private CommandBus $commandBus;
private QueryBus $queryBus;
public function __construct(CommandBus $commandBus, QueryBus $queryBus)
{
parent::__construct();
$this->commandBus = $commandBus;
$this->queryBus = $queryBus;
}
public function create(): void
{
$data = $this->getPost();
$command = new CreateOrder(
$data['customer_id'],
$data['items']
);
$this->commandBus->dispatch($command);
$this->redirect('/orders');
}
public function show(string $orderId): void
{
$query = new GetOrderDetails($orderId);
$orderDetails = $this->queryBus->dispatch($query);
$this->render('orders/show', ['order' => $orderDetails]);
}
}
Buat events immutable untuk menghindari side effects dan memudahkan debugging.
Gunakan nama past tense untuk events karena merepresentasikan sesuatu yang telah terjadi
(misalnya UserRegistered
, bukan RegisterUser
).
Setiap event dan listener harus memiliki tanggung jawab tunggal dan melakukan satu tugas dengan baik.
Hindari dependencies melingkar antara events dan listeners untuk mencegah infinite loops.
Pertimbangkan untuk menambahkan versioning ke events untuk menangani perubahan struktur event di masa depan.
Dokumentasikan events dengan baik, termasuk tujuan, struktur data, dan contoh penggunaan.
Event-Driven Architecture adalah pendekatan yang powerful untuk membangun aplikasi yang loosely coupled, scalable, dan maintainable. NexaUI menyediakan infrastruktur dan konvensi yang mendukung implementasi EDA, membantu Anda membangun aplikasi yang lebih fleksibel dan responsif.
Dengan mengadopsi EDA di NexaUI, Anda dapat membangun aplikasi yang dapat beradaptasi dengan perubahan kebutuhan bisnis dan tumbuh seiring waktu tanpa meningkatkan kompleksitas secara signifikan.