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:
- CallbackMetadata: Speichert Metadaten über Callbacks (cachebar)
- CallbackExecutor: Führt Callbacks basierend auf Metadata aus
- 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
- Verwende Attribute auf Command/Query-Handlern, nicht auf Controllern
- Präferiere Pattern A (Handler) für komplexe Logik die isoliert getestet werden soll
- Präferiere Pattern B (First-Class Callables) für einfache Policy-Checks
- Präferiere Pattern C (Closure-Factories) für parametrisierte Policies
- Vermeide direkte Closures in Attributen - nutze stattdessen Factories
- Nutze Dependency Injection über
AttributeExecutionContextstatt globale Services - BeforeExecute für Validierung, Rate Limiting, etc.
- AfterExecute für Notifications, Audit Logs, Cache Updates
- 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);