Files
michaelschiemer/docs/features/attribute-execution/guide.md
2025-11-24 21:28:25 +01:00

8.4 KiB

Attribute Execution System Guide

Das Attribute Execution System ermöglicht die Ausführung von Attributen zur Laufzeit mit drei verschiedenen Patterns. Alle Patterns sind cachebar, testbar und unterstützen Dependency Injection.

Wichtig: Attribute werden primär auf Command/Query-Handler-Methoden verwendet, nicht auf Controllern, da die meisten Operationen über den CommandBus laufen.

Übersicht

Das System besteht aus drei Hauptkomponenten:

  1. CallbackMetadata: Speichert Metadaten über Callbacks (cachebar)
  2. CallbackExecutor: Führt Callbacks basierend auf Metadata aus
  3. AttributeRunner: Führt ausführbare Attribute aus

Die drei Patterns

Pattern A: Handler-Klassen

Handler-Klassen sind dedizierte Klassen die eine check(), handle() oder __invoke() Methode implementieren.

Vorteile:

  • 100% cachebar (nur Strings/Arrays)
  • Isoliert testbar
  • Framework-kompatibel (nutzt Container)

Beispiel:

// Handler-Klasse
final readonly class PermissionGuard
{
    public function __construct(
        private readonly array $permissions
    ) {}
    
    public function check(AttributeExecutionContext $context): bool
    {
        $user = $context->container->get(UserService::class)->getCurrentUser();
        return $user->hasPermissions(...$this->permissions);
    }
}

// Verwendung
#[Guard(PermissionGuard::class, ['edit_post', 'delete_post'])]
class PostController {}

Pattern B: First-Class Callables

First-Class Callables nutzen statische Methoden mit PHP 8.1+ Syntax.

Vorteile:

  • Modern und lesbar
  • Cachebar (class + method als Strings)
  • Flexibel mit DI

Beispiel:

// Policy-Klasse mit statischen Methoden
final readonly class UserPolicies
{
    public static function isAdmin(AttributeExecutionContext $context): bool
    {
        $user = $context->container->get(UserService::class)->getCurrentUser();
        return $user->isAdmin();
    }
    
    public static function hasRole(
        string $role,
        AttributeExecutionContext $context
    ): bool {
        $user = $context->container->get(UserService::class)->getCurrentUser();
        return $user->hasRole($role);
    }
}

// Verwendung
#[Guard(UserPolicies::isAdmin(...))]
class AdminController {}

#[Guard(UserPolicies::hasRole('editor', ...))]
class EditorController {}

Pattern C: Closure-Factories

Closure-Factories erstellen parametrisierte Closures zur Laufzeit.

Vorteile:

  • Parametrisierbar
  • Cachebar (nur Factory-Metadaten, nicht die Closure)
  • Flexibel für komplexe Logik

Beispiel:

// Factory-Klasse
final readonly class Policies
{
    public static function requirePermission(string $permission): Closure
    {
        return static function (AttributeExecutionContext $context) use ($permission): bool {
            $user = $context->container->get(UserService::class)->getCurrentUser();
            return $user->hasPermission($permission);
        };
    }
    
    public static function requireAnyPermission(string ...$permissions): Closure
    {
        return static function (AttributeExecutionContext $context) use ($permissions): bool {
            $user = $context->container->get(UserService::class)->getCurrentUser();
            foreach ($permissions as $perm) {
                if ($user->hasPermission($perm)) {
                    return true;
                }
            }
            return false;
        };
    }
}

// Verwendung
#[Guard(Policies::requirePermission('edit_post'))]
class PostController {}

#[Guard(Policies::requireAnyPermission('edit_post', 'delete_post'))]
class PostAdminController {}

Verfügbare Attribute

BeforeExecute

Führt Logik VOR Handler-Ausführung aus.

final readonly class UpdateUserHandler
{
    #[BeforeExecute(Validators::validateInput(...))]
    #[BeforeExecute(RateLimiters::perUser(10, 60))]
    #[CommandHandler]
    public function handle(UpdateUserCommand $command): void
    {
        // Handler-Logic
    }
}

AfterExecute

Führt Logik NACH erfolgreicher Handler-Ausführung aus.

final readonly class CreateOrderHandler
{
    #[AfterExecute(Notifiers::sendConfirmation(...))]
    #[AfterExecute(AuditLoggers::logOrderCreated(...))]
    #[CommandHandler]
    public function handle(CreateOrderCommand $command): Order
    {
        // Handler-Logic
        return $order;
    }
}

OnError

Führt Logik bei Fehlern aus.

final readonly class CallExternalApiHandler
{
    #[OnError(ErrorHandlers::logAndNotify(...))]
    #[OnError(RetryHandlers::scheduleRetry(...))]
    #[CommandHandler]
    public function handle(CallApiCommand $command): Response
    {
        // Handler-Logic
    }
}

Guard

Schützt Methoden/Klassen mit Guards.

#[Guard(PermissionGuard::class, ['edit_post'])]
#[Guard(UserPolicies::isAdmin(...))]
#[Guard(Policies::requirePermission('edit_post'))]

Validate

Validiert Property- oder Parameter-Werte.

final readonly class User
{
    #[Validate(EmailValidator::class)]
    public string $email;
    
    #[Validate(Validators::minLength(5))]
    public string $username;
}

OnBoot

Führt Boot-Logik beim Laden einer Klasse aus.

#[OnBoot(ServiceRegistrar::registerServices(...))]
final readonly class MyService {}

Verwendung in Command/Query Handlers

Command Handler mit Attributen

final readonly class UpdateUserHandler
{
    #[BeforeExecute(Validators::validateInput(...))]
    #[BeforeExecute(RateLimiters::perUser(10, 60))]
    #[AfterExecute(Notifiers::sendEmail(...))]
    #[OnError(ErrorHandlers::logAndNotify(...))]
    #[CommandHandler]
    public function handle(UpdateUserCommand $command): void
    {
        // Business-Logic hier
        // Attribute werden automatisch ausgeführt:
        // 1. BeforeExecute Attribute (vor dieser Methode)
        // 2. Handler-Ausführung
        // 3. AfterExecute Attribute (nach erfolgreicher Ausführung)
        // 4. OnError Attribute (bei Exceptions)
    }
}

Query Handler mit Attributen

final readonly class GetUserQueryHandler
{
    #[BeforeExecute(CacheValidators::checkCache(...))]
    #[AfterExecute(CacheStrategies::storeResult(...))]
    #[QueryHandler]
    public function handle(GetUserQuery $query): User
    {
        // Query-Logic
        return $user;
    }
}

Verwendung des AttributeRunners (für manuelle Ausführung)

Alle Attribute eines Typs ausführen

$runner = $container->get(AttributeRunner::class);
$results = $runner->executeAttributes(Guard::class);

Attribute für eine Klasse ausführen

$className = ClassName::create('MyController');
$results = $runner->executeForClass($className, Guard::class);

Attribute für eine Methode ausführen

$className = ClassName::create('MyController');
$methodName = MethodName::create('edit');
$results = $runner->executeForMethod($className, $methodName, Guard::class);

Cache-Kompatibilität

Alle drei Patterns sind vollständig cachebar:

  • Handler: Nur Klassenname und Argumente werden gecacht
  • First-Class Callables: Nur Klassenname und Methodenname werden gecacht
  • Closure-Factories: Nur Factory-Klasse, Methode und Argumente werden gecacht

Die Closures selbst werden nicht gecacht, sondern zur Laufzeit aus den Factories erstellt.

Best Practices

  1. Verwende Attribute auf Command/Query-Handlern, nicht auf Controllern
  2. Präferiere Pattern A (Handler) für komplexe Logik die isoliert getestet werden soll
  3. Präferiere Pattern B (First-Class Callables) für einfache Policy-Checks
  4. Präferiere Pattern C (Closure-Factories) für parametrisierte Policies
  5. Vermeide direkte Closures in Attributen - nutze stattdessen Factories
  6. Nutze Dependency Injection über AttributeExecutionContext statt globale Services
  7. BeforeExecute für Validierung, Rate Limiting, etc.
  8. AfterExecute für Notifications, Audit Logs, Cache Updates
  9. OnError für Error Handling, Retry Logic, Logging

Performance

  • Callback-Metadata wird gecacht (nicht Closures)
  • Closures nur zur Laufzeit erstellt
  • Keine Reflection zur Laufzeit für bekannte Patterns
  • Handler werden über Container erstellt (unterstützt Lazy Loading)

Testing

Alle Patterns sind testbar:

// Handler testen
$handler = new PermissionGuard(['edit_post']);
$result = $handler->check($context);

// Static Method testen
$result = UserPolicies::isAdmin($context);

// Factory testen
$closure = Policies::requirePermission('edit_post');
$result = $closure($context);