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

329 lines
8.4 KiB
Markdown

# 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:**
```php
// 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:**
```php
// 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:**
```php
// 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.
```php
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.
```php
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.
```php
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.
```php
#[Guard(PermissionGuard::class, ['edit_post'])]
#[Guard(UserPolicies::isAdmin(...))]
#[Guard(Policies::requirePermission('edit_post'))]
```
### Validate
Validiert Property- oder Parameter-Werte.
```php
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.
```php
#[OnBoot(ServiceRegistrar::registerServices(...))]
final readonly class MyService {}
```
## Verwendung in Command/Query Handlers
### Command Handler mit Attributen
```php
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
```php
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
```php
$runner = $container->get(AttributeRunner::class);
$results = $runner->executeAttributes(Guard::class);
```
### Attribute für eine Klasse ausführen
```php
$className = ClassName::create('MyController');
$results = $runner->executeForClass($className, Guard::class);
```
### Attribute für eine Methode ausführen
```php
$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:
```php
// 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);
```