Event-Driven Architecture di NexaUI Framework

Pengenalan

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.

Konsep Dasar

Events

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 Emitters

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 Listeners

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

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

Jenis Events di NexaUI

Domain Events

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

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

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

Mendaftarkan Event Listeners

Di NexaUI, Anda dapat mendaftarkan event listeners di beberapa tempat:

Di File Konfigurasi

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,
    ],
];

Secara Programatis

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

Async Event Processing

NexaUI mendukung pemrosesan event secara asynchronous menggunakan queue system. Ini berguna untuk operasi yang membutuhkan waktu lama atau tidak kritis terhadap waktu.

Mendaftarkan Async Listener

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'],
    ],
];

Implementasi Async Listener

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

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

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

Event Sourced Aggregate

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

Menggunakan Event Sourcing

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 (Command Query Responsibility Segregation)

CQRS adalah pola yang memisahkan operasi baca (queries) dari operasi tulis (commands). NexaUI mendukung CQRS melalui komponen CommandBus dan QueryBus.

Commands

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 Handlers

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

Queries

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 Handlers

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

Menggunakan Command Bus dan Query Bus

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

Praktik Terbaik

Immutable Events

Buat events immutable untuk menghindari side effects dan memudahkan debugging.

Event Naming

Gunakan nama past tense untuk events karena merepresentasikan sesuatu yang telah terjadi (misalnya UserRegistered, bukan RegisterUser).

Single Responsibility

Setiap event dan listener harus memiliki tanggung jawab tunggal dan melakukan satu tugas dengan baik.

Avoid Circular Dependencies

Hindari dependencies melingkar antara events dan listeners untuk mencegah infinite loops.

Event Versioning

Pertimbangkan untuk menambahkan versioning ke events untuk menangani perubahan struktur event di masa depan.

Event Documentation

Dokumentasikan events dengan baik, termasuk tujuan, struktur data, dan contoh penggunaan.

Kesimpulan

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.