Skip to content

Event System Guide

Anleitung zum Arbeiten mit dem Event-System in der Appiyon-Plattform.

Übersicht

Das Event-System ermöglicht lose Kopplung zwischen Modulen durch Event-Driven Architecture.

Event-Typen

Domain Events

Domain Events repräsentieren wichtige Business-Ereignisse:

php
final class AdminCreatedEvent implements AuditableInterface
{
    private \DateTimeImmutable $occurredAt;
    private string $eventId;

    public function __construct(private readonly Admin $admin)
    {
        $this->occurredAt = new \DateTimeImmutable();
        $this->eventId = $this->generateEventId();
    }

    public function getAdmin(): Admin
    {
        return $this->admin;
    }

    public function getOccurredAt(): \DateTimeImmutable
    {
        return $this->occurredAt;
    }

    public function getEventId(): string
    {
        return $this->eventId;
    }

    // AuditableInterface Implementation
    public function getEventName(): string
    {
        return 'admin.created';
    }

    public function getAuditableType(): string
    {
        return Admin::class;
    }

    public function getAuditableId(): string
    {
        return (string) $this->admin->getId();
    }

    public function getAuditData(): array
    {
        return [
            'name' => $this->admin->getName(),
            'email' => $this->admin->getEmail(),
        ];
    }

    public function getAdminId(): ?int
    {
        return $this->admin->getId();
    }

    private function generateEventId(): string
    {
        return sprintf(
            '%s-%s-%s',
            'admin.created',
            $this->admin->getId(),
            $this->occurredAt->format('YmdHis')
        );
    }
}

Symfony Kernel Events

Für Framework-Integration (Request/Response Lifecycle):

php
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;

class AdminDomainRestrictionSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::REQUEST => ['onKernelRequest', 31],
        ];
    }

    public function onKernelRequest(RequestEvent $event): void
    {
        // Event-Handling Logic
    }
}

AuditableInterface

Alle Domain Events sollten AuditableInterface implementieren für automatisches Audit-Logging:

php
interface AuditableInterface
{
    public function getEventName(): string;        // z.B. 'admin.created'
    public function getAuditableType(): string;    // z.B. Admin::class
    public function getAuditableId(): string;      // Entity ID
    public function getAuditData(): array;         // Relevante Daten
    public function getOccurredAt(): \DateTimeImmutable;
    public function getEventId(): string;
    public function getAdminId(): ?int;           // Wer hat Aktion ausgeführt
}

Events erstellen

1. Event-Klasse erstellen

php
<?php

declare(strict_types=1);

namespace App\Appi\Infrastructure\Admin\Event;

use App\Appi\Infrastructure\Admin\Entity\Admin;
use App\Appi\Shared\Audit\Contract\AuditableInterface;

final class AdminAuthenticatedEvent implements AuditableInterface
{
    private \DateTimeImmutable $occurredAt;
    private string $eventId;

    public function __construct(
        private readonly Admin $admin,
        private readonly string $ipAddress,
        private readonly string $userAgent
    ) {
        $this->occurredAt = new \DateTimeImmutable();
        $this->eventId = $this->generateEventId();
    }

    // Getters...

    public function getEventName(): string
    {
        return 'admin.authenticated';
    }

    public function getAuditableType(): string
    {
        return Admin::class;
    }

    public function getAuditableId(): string
    {
        return (string) $this->admin->getId();
    }

    public function getAuditData(): array
    {
        return [
            'email' => $this->admin->getEmail(),
            'ip_address' => $this->ipAddress,
            'user_agent' => $this->userAgent,
        ];
    }

    public function getOccurredAt(): \DateTimeImmutable
    {
        return $this->occurredAt;
    }

    public function getEventId(): string
    {
        return $this->eventId;
    }

    public function getAdminId(): ?int
    {
        return $this->admin->getId();
    }

    private function generateEventId(): string
    {
        return sprintf(
            '%s-%s-%s',
            'admin.authenticated',
            $this->admin->getId(),
            $this->occurredAt->format('YmdHis')
        );
    }
}

2. Event dispatchen

Im Use Case Handler:

php
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

final readonly class CreateAdminHandler
{
    public function __construct(
        private AdminRepositoryInterface $adminRepository,
        private EventDispatcherInterface $eventDispatcher
    ) {
    }

    public function handle(CreateAdminCommand $command): Admin
    {
        // Entity erstellen und speichern
        $admin = new Admin(/* ... */);
        $this->adminRepository->save($admin);

        // Event dispatchen
        $event = new AdminCreatedEvent($admin);
        $this->eventDispatcher->dispatch($event);

        return $admin;
    }
}

Event Subscribers

Event Subscribers reagieren auf Events:

Domain Event Subscriber

php
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class AdminEventSubscriber implements EventSubscriberInterface
{
    public function __construct(
        private readonly LoggerInterface $logger,
        private readonly NotificationService $notificationService
    ) {
    }

    public static function getSubscribedEvents(): array
    {
        return [
            AdminCreatedEvent::class => 'onAdminCreated',
            AdminAuthenticatedEvent::class => 'onAdminAuthenticated',
        ];
    }

    public function onAdminCreated(AdminCreatedEvent $event): void
    {
        $admin = $event->getAdmin();

        // Logging
        $this->logger->info('New admin created', [
            'admin_id' => $admin->getId(),
            'email' => $admin->getEmail(),
        ]);

        // Notification senden
        $this->notificationService->sendWelcomeEmail($admin);
    }

    public function onAdminAuthenticated(AdminAuthenticatedEvent $event): void
    {
        $this->logger->info('Admin authenticated', [
            'admin_id' => $event->getAdmin()->getId(),
            'ip_address' => $event->getIpAddress(),
        ]);
    }
}

Kernel Event Subscriber

php
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;

class RequestLoggingSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::REQUEST => ['onKernelRequest', 10],
            KernelEvents::RESPONSE => ['onKernelResponse', -10],
        ];
    }

    public function onKernelRequest(RequestEvent $event): void
    {
        // Before controller execution
    }

    public function onKernelResponse(ResponseEvent $event): void
    {
        // After controller execution
    }
}

Event Priority

Events haben Priorität (höher = früher):

php
public static function getSubscribedEvents(): array
{
    return [
        KernelEvents::REQUEST => [
            ['onKernelRequestEarly', 100],   // Wird zuerst ausgeführt
            ['onKernelRequestLate', -100],   // Wird zuletzt ausgeführt
        ],
    ];
}

Beispiel: AdminDomainRestriction läuft mit Priority 31 (vor Security mit Priority 8).

Audit Event Subscriber

Automatisches Audit-Logging für alle AuditableInterface Events:

php
namespace App\Appi\Shared\Event\Subscriber;

use App\Appi\Shared\Audit\Contract\AuditableInterface;
use App\Appi\Shared\Audit\Message\AuditMessage;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\MessageBusInterface;

class AuditEventSubscriber implements EventSubscriberInterface
{
    public function __construct(
        private readonly MessageBusInterface $messageBus
    ) {
    }

    public static function getSubscribedEvents(): array
    {
        return [
            // Lauscht auf ALLE Events
            '*' => ['onAnyEvent', -100], // Niedrige Priority = nach allen anderen
        ];
    }

    public function onAnyEvent(object $event): void
    {
        // Nur AuditableInterface Events verarbeiten
        if (!$event instanceof AuditableInterface) {
            return;
        }

        // AuditMessage erstellen und async verarbeiten
        $message = new AuditMessage(
            eventName: $event->getEventName(),
            auditableType: $event->getAuditableType(),
            auditableId: $event->getAuditableId(),
            auditData: $event->getAuditData(),
            occurredAt: $event->getOccurredAt(),
            adminId: $event->getAdminId()
        );

        $this->messageBus->dispatch($message);
    }
}

Async Event Processing

Mit Symfony Messenger für async Verarbeitung:

Message definieren

php
final readonly class AuditMessage
{
    public function __construct(
        public string $eventName,
        public string $auditableType,
        public string $auditableId,
        public array $auditData,
        public \DateTimeImmutable $occurredAt,
        public ?int $adminId = null
    ) {
    }
}

Message Handler

php
use Symfony\Component\Messenger\Attribute\AsMessageHandler;

#[AsMessageHandler]
class AuditMessageHandler
{
    public function __construct(
        private readonly AdminAuditLogRepositoryInterface $auditLogRepository
    ) {
    }

    public function __invoke(AuditMessage $message): void
    {
        $auditLog = new AdminAuditLog(
            $message->eventName,
            $message->auditableType,
            $message->auditableId,
            $message->auditData,
            $message->adminId
        );

        $this->auditLogRepository->save($auditLog);
    }
}

Messenger Konfiguration

yaml
# config/packages/messenger.yaml
framework:
    messenger:
        transports:
            async: '%env(MESSENGER_TRANSPORT_DSN)%'

        routing:
            'App\Appi\Shared\Audit\Message\AuditMessage': async

Event Naming Convention

Format: {entity}.{action}

Beispiele:

  • admin.created
  • admin.authenticated
  • admin.action.logged
  • app.approved
  • app.rejected
  • user.registered
  • master_data.country.created

Stoppable Events

Events können weitere Subscriber stoppen:

php
use Symfony\Contracts\EventDispatcher\Event;

final class ValidatableEvent extends Event
{
    private bool $isValid = true;

    public function invalidate(): void
    {
        $this->isValid = false;
        $this->stopPropagation(); // Stoppt weitere Subscriber
    }

    public function isValid(): bool
    {
        return $this->isValid;
    }
}

Testing Events

Event Dispatching testen

php
use PHPUnit\Framework\TestCase;
use Symfony\Component\EventDispatcher\EventDispatcher;

class CreateAdminHandlerTest extends TestCase
{
    public function testDispatchesAdminCreatedEvent(): void
    {
        $eventDispatcher = $this->createMock(EventDispatcherInterface::class);

        $eventDispatcher->expects($this->once())
            ->method('dispatch')
            ->with($this->isInstanceOf(AdminCreatedEvent::class));

        $handler = new CreateAdminHandler($repository, $eventDispatcher);
        $handler->handle($command);
    }
}

Event Subscriber testen

php
class AdminEventSubscriberTest extends TestCase
{
    public function testOnAdminCreatedSendsNotification(): void
    {
        $notificationService = $this->createMock(NotificationService::class);
        $notificationService->expects($this->once())
            ->method('sendWelcomeEmail');

        $subscriber = new AdminEventSubscriber($logger, $notificationService);

        $event = new AdminCreatedEvent($admin);
        $subscriber->onAdminCreated($event);
    }
}

Best Practices

  1. Immutable Events: Events sollten readonly sein
  2. Single Purpose: Ein Event = Ein Ereignis
  3. Rich Information: Alle relevanten Daten mitgeben
  4. Naming: Vergangenheitsform (created, nicht create)
  5. AuditableInterface: Für alle wichtigen Domain Events
  6. Async für Heavy Work: Message Queue für aufwändige Verarbeitung
  7. Testing: Events und Subscriber immer testen
  8. Documentation: Event Purpose und Payload dokumentieren

Event Catalog

Admin Events

  • admin.created - Neuer Admin erstellt
  • admin.authenticated - Admin hat sich eingeloggt
  • admin.action.logged - Admin-Aktion wurde geloggt
  • admin.deleted - Admin wurde gelöscht
  • admin.restored - Admin wurde wiederhergestellt

Geplante Events

  • app.created
  • app.approved
  • app.rejected
  • developer.registered
  • developer.verified
  • user.registered
  • tenant.created

Debugging Events

bash
# Alle Event Listeners anzeigen
php bin/console debug:event-dispatcher

# Listener für spezifisches Event
php bin/console debug:event-dispatcher kernel.request

# Alle Subscriber
php bin/console debug:container --tag=kernel.event_subscriber

Beispiele

Vollständige Beispiele:

Checkliste

  • [ ] Event-Klasse als final readonly
  • [ ] AuditableInterface implementiert
  • [ ] Alle relevanten Daten im Event
  • [ ] Event-Naming-Convention eingehalten
  • [ ] Event in Use Case dispatched
  • [ ] Event Subscriber erstellt (falls nötig)
  • [ ] Async Verarbeitung konfiguriert (falls heavy work)
  • [ ] Unit Tests geschrieben
  • [ ] Event in Catalog dokumentiert

Built with VitePress