Some checks failed
Deploy Application / deploy (push) Has been cancelled
329 lines
8.4 KiB
Markdown
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);
|
|
```
|
|
|