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:
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):
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:
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
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:
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
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
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):
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:
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
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
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
# config/packages/messenger.yaml
framework:
messenger:
transports:
async: '%env(MESSENGER_TRANSPORT_DSN)%'
routing:
'App\Appi\Shared\Audit\Message\AuditMessage': asyncEvent Naming Convention
Format: {entity}.{action}
Beispiele:
admin.createdadmin.authenticatedadmin.action.loggedapp.approvedapp.rejecteduser.registeredmaster_data.country.created
Stoppable Events
Events können weitere Subscriber stoppen:
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
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
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
- Immutable Events: Events sollten readonly sein
- Single Purpose: Ein Event = Ein Ereignis
- Rich Information: Alle relevanten Daten mitgeben
- Naming: Vergangenheitsform (created, nicht create)
- AuditableInterface: Für alle wichtigen Domain Events
- Async für Heavy Work: Message Queue für aufwändige Verarbeitung
- Testing: Events und Subscriber immer testen
- Documentation: Event Purpose und Payload dokumentieren
Event Catalog
Admin Events
admin.created- Neuer Admin erstelltadmin.authenticated- Admin hat sich eingeloggtadmin.action.logged- Admin-Aktion wurde geloggtadmin.deleted- Admin wurde gelöschtadmin.restored- Admin wurde wiederhergestellt
Geplante Events
app.createdapp.approvedapp.rejecteddeveloper.registereddeveloper.verifieduser.registeredtenant.created
Debugging Events
# 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_subscriberBeispiele
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