fix: Gitea Traefik routing and connection pool optimization
Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled

- Remove middleware reference from Gitea Traefik labels (caused routing issues)
- Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s)
- Add explicit service reference in Traefik labels
- Fix intermittent 504 timeouts by improving PostgreSQL connection handling

Fixes Gitea unreachability via git.michaelschiemer.de
This commit is contained in:
2025-11-09 14:46:15 +01:00
parent 85c369e846
commit 36ef2a1e2c
1366 changed files with 104925 additions and 28719 deletions

View File

@@ -0,0 +1,202 @@
# Migration System Quick Reference
## TL;DR
**Default**: Migrations sind forward-only (nur `up()` Methode).
**Optional**: Implementiere `SafelyReversible` nur wenn Rollback OHNE Datenverlust möglich ist.
## Quick Decision Tree
```
Is rollback safe (no data loss)?
├─ YES (e.g., create table, add nullable column, create index)
│ └─ Implement: Migration, SafelyReversible
└─ NO (e.g., drop column, transform data, delete data)
└─ Implement: Migration only
```
## Code Templates
### Forward-Only Migration (Default)
```php
use App\Framework\Database\Migration\Migration;
use App\Framework\Database\Migration\MigrationVersion;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\Schema\Schema;
final readonly class YourMigration implements Migration
{
public function up(ConnectionInterface $connection): void
{
$schema = new Schema($connection);
// Your migration logic
$schema->execute();
}
public function getVersion(): MigrationVersion
{
return MigrationVersion::fromTimestamp("2024_12_20_100000");
}
public function getDescription(): string
{
return 'Description of what this migration does';
}
public function getDomain(): string
{
return "YourDomain";
}
}
```
### Safe Rollback Migration
```php
use App\Framework\Database\Migration\{Migration, SafelyReversible};
use App\Framework\Database\Migration\MigrationVersion;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\Schema\Schema;
/**
* This migration is safely reversible because:
* - [Explain why rollback is safe, e.g., "Creates new empty table"]
*/
final readonly class YourSafeMigration implements Migration, SafelyReversible
{
public function up(ConnectionInterface $connection): void
{
$schema = new Schema($connection);
// Your migration logic
$schema->execute();
}
public function down(ConnectionInterface $connection): void
{
$schema = new Schema($connection);
// Reverse the changes safely
$schema->execute();
}
public function getVersion(): MigrationVersion
{
return MigrationVersion::fromTimestamp("2024_12_20_100000");
}
public function getDescription(): string
{
return 'Description of what this migration does';
}
public function getDomain(): string
{
return "YourDomain";
}
}
```
## Common Operations
### Create Migration
```bash
php console.php make:migration CreateYourTable
```
### Run Migrations
```bash
php console.php db:migrate
```
### Rollback (Only SafelyReversible)
```bash
# Rollback last migration
php console.php db:rollback
# Rollback last 3 migrations
php console.php db:rollback 3
```
### Check Status
```bash
php console.php db:status
```
## Safe vs Unsafe Cheatsheet
| Operation | Safe? | Implements |
|-----------|-------|------------|
| Create table | ✅ | Migration, SafelyReversible |
| Drop table (empty) | ✅ | Migration, SafelyReversible |
| Drop table (with data) | ❌ | Migration only |
| Add nullable column | ✅ | Migration, SafelyReversible |
| Add NOT NULL column (with default) | ⚠️ | Case by case |
| Drop column (empty) | ✅ | Migration, SafelyReversible |
| Drop column (with data) | ❌ | Migration only |
| Rename column | ✅ | Migration, SafelyReversible |
| Change column type | ❌ | Migration only |
| Create index | ✅ | Migration, SafelyReversible |
| Drop index | ✅ | Migration, SafelyReversible |
| Add foreign key | ✅ | Migration, SafelyReversible |
| Drop foreign key | ✅ | Migration, SafelyReversible |
| Transform data | ❌ | Migration only |
| Delete data | ❌ | Migration only |
| Merge tables | ❌ | Migration only |
## Error Handling
### Rollback Failed (Not SafelyReversible)
```bash
❌ Rollback failed: Migration 2024_12_20_100000 does not support safe rollback
💡 Recommendation:
Create a new forward migration to undo the changes instead:
php console.php make:migration FixYourChanges
```
### What to do:
1. Create new forward migration
2. Implement reverse logic in `up()` method
3. Run `php console.php db:migrate`
## Testing Rollback
```bash
# Test cycle in development
php console.php db:migrate
php console.php db:rollback 1
php console.php db:migrate # Should succeed again
```
## Best Practices
1.**Document why safe**: Always comment why a migration is SafelyReversible
2.**Test rollback cycle**: Test `up() → down() → up()` in development
3.**Default to forward-only**: Only add SafelyReversible if certain
4.**Never rollback in production**: Even "safe" rollbacks should be avoided
5.**Use fix-forward**: Prefer new migrations over rollback
6.**Backup before rollback**: Always backup database before rolling back
7.**Check for data**: Verify column is empty before making it SafelyReversible
## Links
- **Full Examples**: `docs/claude/examples/migrations/SafeVsUnsafeMigrations.md`
- **Database Patterns**: `docs/claude/database-patterns.md`
- **Migration Interface**: `src/Framework/Database/Migration/Migration.php`
- **SafelyReversible Interface**: `src/Framework/Database/Migration/SafelyReversible.php`
## Framework Compliance
✅ Readonly classes: `final readonly class`
✅ Composition over inheritance: Interface-based
✅ Explicit contracts: `SafelyReversible` is opt-in
✅ Type safety: Strong typing throughout
✅ No primitive obsession: Uses Value Objects (MigrationVersion)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,754 @@
# Exception System Advanced Features
## Übersicht
Das Exception-System wurde um 10 erweiterte Features erweitert, die in drei Phasen implementiert wurden:
- **Phase 1 (Foundation)**: Rate Limiting, Context Caching, Performance Tracking
- **Phase 2 (User Experience)**: User-Friendly Messages, Localization
- **Phase 3 (Advanced)**: Recovery/Retry, Pattern Detection, Correlation, Metrics, Health Checks
Alle Features sind optional und können einzeln aktiviert werden. Sie folgen den Framework-Prinzipien (final, readonly, Value Objects, Composition over Inheritance).
---
## Phase 1: Foundation Features
### 1. Exception Rate Limiting & Throttling
**Ziel**: Verhindert Log-Spam durch wiederholte gleiche Exceptions
**Komponenten**:
- `ExceptionFingerprint` - Generiert eindeutige Fingerprints für Exception-Gruppierung
- `ExceptionRateLimitConfig` - Konfiguration für Thresholds und Time Windows
- `ExceptionRateLimiter` - Rate Limiter mit Cache-basiertem Tracking
**Verwendung**:
```php
use App\Framework\ExceptionHandling\RateLimit\ExceptionRateLimiter;
use App\Framework\ExceptionHandling\RateLimit\ExceptionRateLimitConfig;
use App\Framework\Core\ValueObjects\Duration;
// Konfiguration erstellen
$config = ExceptionRateLimitConfig::withLimits(
maxExceptions: 10,
timeWindow: Duration::fromSeconds(60)
);
// Rate Limiter erstellen
$rateLimiter = new ExceptionRateLimiter($cache, $config);
// In ErrorKernel integriert (automatisch)
// Exceptions werden automatisch rate-limited, wenn Threshold erreicht wird
```
**Konfiguration**:
- `maxExceptions`: Maximale Anzahl gleicher Exceptions pro Time Window (Default: 10)
- `timeWindow`: Time Window für Rate Limiting (Default: 60 Sekunden)
- `skipLoggingOnLimit`: Skip Logging wenn Rate Limit erreicht (Default: true)
- `skipAuditOnLimit`: Skip Audit Logging wenn Rate Limit erreicht (Default: true)
- `trackMetricsOnLimit`: Track Metriken auch bei Rate Limit (Default: true)
**Fingerprint-Generierung**:
- Basierend auf Exception-Klasse, normalisierter Message, File/Line
- Optional: Component und Operation aus Context für präzisere Gruppierung
- Normalisierung entfernt variable Teile (UUIDs, Timestamps, Zahlen, etc.)
---
### 2. Exception Context Caching
**Ziel**: Performance-Optimierung durch Caching von häufig verwendeten Context-Daten
**Komponenten**:
- `ExceptionContextCache` - Cache-Wrapper für Context-Daten
- Integration in `ExceptionContextBuilder` - Automatischer Cache-Lookup
**Verwendung**:
```php
use App\Framework\ExceptionHandling\Context\ExceptionContextCache;
use App\Framework\ExceptionHandling\Context\ExceptionContextBuilder;
// Context Cache erstellen
$contextCache = new ExceptionContextCache($cache);
// Context Builder mit Cache
$builder = new ExceptionContextBuilder(
errorScope: $errorScope,
contextCache: $contextCache
);
// Automatischer Cache-Lookup beim buildFromRequest()
$context = $builder->buildFromRequest($request);
```
**Cache-Levels**:
- **Request-Level**: TTL 10 Minuten (spezifischste)
- **Session-Level**: TTL 10 Minuten
- **User-Level**: TTL 30 Minuten (am wenigsten spezifisch)
**Cache-Invalidation**:
```php
// Manuelle Invalidation bei Context-Änderungen
$contextCache->invalidateRequest($requestId);
$contextCache->invalidateSession($sessionId);
$contextCache->invalidateUser($userId);
```
---
### 3. Exception Performance Tracking
**Ziel**: Tracking von Performance-Impact pro Exception-Typ
**Komponenten**:
- `ExceptionPerformanceMetrics` - Value Object für Metriken
- `ExceptionPerformanceTracker` - Performance-Tracking
**Verwendung**:
```php
use App\Framework\ExceptionHandling\Performance\ExceptionPerformanceTracker;
$tracker = new ExceptionPerformanceTracker();
// Start tracking
$startData = $tracker->start();
// ... exception occurs ...
// End tracking
$metrics = $tracker->end($startData, $exception, $context);
// Metriken enthalten:
// - executionTimeMs: Ausführungszeit in Millisekunden
// - memoryDeltaBytes: Memory-Delta in Bytes
// - cpuUsagePercent: CPU-Usage in Prozent (wenn verfügbar)
```
**Integration**:
- Metriken werden automatisch in `ExceptionContextData::metadata['performance']` gespeichert
- Kann mit `MetricsCollector` integriert werden für Monitoring
---
## Phase 2: User Experience Features
### 4. User-Friendly Exception Messages
**Ziel**: Übersetzung von technischen Exception-Messages zu benutzerfreundlichen Texten
**Komponenten**:
- `UserFriendlyMessage` - Value Object für User-Messages
- `ExceptionMessageTranslator` - Message-Übersetzung mit Template-System
**Verwendung**:
```php
use App\Framework\ExceptionHandling\Translation\ExceptionMessageTranslator;
use App\Framework\ExceptionHandling\Translation\UserFriendlyMessage;
// Templates definieren
$templates = [
\App\Framework\Exception\DatabaseException::class => [
'message' => 'Database connection failed. Please try again later.',
'title' => 'Database Error',
'help' => 'If this problem persists, please contact support.'
],
\App\Framework\Exception\ValidationException::class => [
'message' => 'Please check your input and try again.',
'title' => 'Validation Error'
]
];
// Translator erstellen
$translator = new ExceptionMessageTranslator(
templates: $templates,
isDebugMode: false
);
// Message übersetzen
$userMessage = $translator->translate($exception, $context);
// $userMessage->message: User-friendly message
// $userMessage->title: Optional title
// $userMessage->helpText: Optional help text
// $userMessage->technicalMessage: Original technical message (für Debugging)
```
**Template-Variablen**:
- `{exception_message}` - Original Exception-Message
- `{exception_class}` - Exception-Klasse
- `{operation}` - Operation aus Context
- `{component}` - Component aus Context
**Integration**:
- Automatisch in `ResponseErrorRenderer` integriert
- In Debug-Mode werden technische Messages angezeigt
- In Production werden User-Friendly Messages verwendet
---
### 5. Exception Localization
**Ziel**: i18n-Support für Exception-Messages basierend auf User-Locale
**Komponenten**:
- `ExceptionLocalizer` - Lokalisierung mit Fallback-Chain
**Verwendung**:
```php
use App\Framework\ExceptionHandling\Localization\ExceptionLocalizer;
// Übersetzungen definieren
$translations = [
'de' => [
\App\Framework\Exception\DatabaseException::class => [
'message' => 'Datenbankverbindung fehlgeschlagen. Bitte versuchen Sie es später erneut.',
'title' => 'Datenbankfehler'
]
],
'en' => [
\App\Framework\Exception\DatabaseException::class => [
'message' => 'Database connection failed. Please try again later.',
'title' => 'Database Error'
]
]
];
// Localizer erstellen
$localizer = new ExceptionLocalizer(
translations: $translations,
defaultLocale: 'en'
);
// Locale aus Context extrahieren
$locale = $localizer->getLocale($context);
// Übersetzungen für Locale abrufen
$localeTranslations = $localizer->getTranslations($locale);
// Fallback-Chain: [user_locale, default_locale, 'en']
$fallbackChain = $localizer->getFallbackChain($locale);
```
**Locale-Extraktion**:
- Aus `ExceptionContextData::metadata['locale']`
- Fallback zu Default-Locale
- Fallback zu 'en' als letzte Option
**Integration**:
- Wird von `ExceptionMessageTranslator` verwendet
- Locale sollte in `ExceptionContextData::metadata['locale']` gespeichert werden
---
## Phase 3: Advanced Features
### 6. Exception Recovery & Retry Logic
**Ziel**: Automatische Retry-Logik für temporäre Exceptions
**Komponenten**:
- `RetryStrategy` - Retry-Strategien (Exponential Backoff, Linear, Fixed)
- `ExceptionRecoveryManager` - Recovery-Manager
- `RetryableException` - Marker-Interface
**Verwendung**:
```php
use App\Framework\ExceptionHandling\Recovery\ExceptionRecoveryManager;
use App\Framework\ExceptionHandling\Recovery\RetryableException;
use App\Framework\Exception\ExceptionMetadata;
// Retryable Exception implementieren
final class NetworkException extends \Exception implements RetryableException
{
}
// Exception mit Retry-Config erstellen
$metadata = ExceptionMetadata::withRetry(
retryAfter: 1000 // Base delay in milliseconds
)->withMaxRetries(3)
->withRetryStrategy('exponential_backoff');
// Recovery Manager
$recoveryManager = new ExceptionRecoveryManager();
// Prüfen ob Retry nötig
if ($recoveryManager->shouldRetry($exception, $metadata)) {
$delay = $recoveryManager->getRetryDelay($exception, $metadata, $attemptNumber);
// Retry nach $delay Millisekunden
}
```
**Retry-Strategien**:
- **Exponential Backoff** (Default): `baseDelay * 2^(attempt-1)`
- **Linear**: `baseDelay * attempt`
- **Fixed**: `baseDelay` (konstant)
**ExceptionMetadata Erweiterungen**:
- `maxRetries`: Maximale Anzahl Retries (Default: 3)
- `retryStrategy`: Retry-Strategie (Default: 'exponential_backoff')
- `retryAfter`: Base Delay in Millisekunden
**Retryable Exceptions**:
- Exceptions die `RetryableException` implementieren
- Exceptions mit bestimmten Namen-Patterns (NetworkException, TimeoutException, etc.)
---
### 7. Exception Pattern Detection & Auto-Fix Suggestions
**Ziel**: ML-basierte Pattern-Erkennung mit Fix-Vorschlägen
**Komponenten**:
- `ExceptionPattern` - Value Object für Patterns
- `FixSuggestion` - Value Object für Fix-Vorschläge
- `ExceptionPatternDetector` - Pattern-Detection
**Verwendung**:
```php
use App\Framework\ExceptionHandling\PatternDetection\ExceptionPatternDetector;
use App\Framework\ExceptionHandling\PatternDetection\FixSuggestion;
// Knowledge Base definieren
$knowledgeBase = [
\App\Framework\Exception\DatabaseException::class => [
'description' => 'Database connection timeout',
'fixes' => [
[
'title' => 'Check connection pool size',
'description' => 'Increase database connection pool size',
'code' => '$config->setMaxConnections(50);',
'confidence' => 'high'
],
[
'title' => 'Check database server',
'description' => 'Verify database server is running and accessible',
'confidence' => 'medium'
]
]
]
];
// Pattern Detector erstellen
$detector = new ExceptionPatternDetector($knowledgeBase);
// Patterns erkennen
$patterns = $detector->detect($exception, $context);
foreach ($patterns as $pattern) {
echo $pattern->description . "\n";
foreach ($pattern->fixSuggestions as $fix) {
echo " - " . $fix->title . ": " . $fix->description . "\n";
}
}
```
**Integration**:
- Kann in `ErrorAggregator` integriert werden
- Patterns werden in `ExceptionContextData::metadata['patterns']` gespeichert
---
### 8. Exception Correlation & Root Cause Analysis
**Ziel**: Verknüpfung verwandter Exceptions für Root-Cause-Analyse
**Komponenten**:
- `ExceptionCorrelation` - Value Object für Korrelationen
- `ExceptionCorrelationEngine` - Correlation-Engine
**Verwendung**:
```php
use App\Framework\ExceptionHandling\Correlation\ExceptionCorrelationEngine;
$correlationEngine = new ExceptionCorrelationEngine($cache);
// Exception korrelieren
$correlation = $correlationEngine->correlate($exception, $context);
// Correlation-Daten:
// - correlationKey: Request-ID, Session-ID oder User-ID
// - exceptionIds: Array von verwandten Exception-IDs
// - rootCauseId: ID der ersten Exception (Root Cause)
```
**Correlation-Keys**:
- **Request-ID** (höchste Priorität): Alle Exceptions in derselben Request
- **Session-ID**: Alle Exceptions in derselben Session
- **User-ID**: Alle Exceptions für denselben User
**Root Cause Analysis**:
- Erste Exception in Correlation-Chain ist Root Cause
- Alle weiteren Exceptions sind Folge-Exceptions
**Integration**:
- Correlation-Daten werden in `ExceptionContextData::metadata['correlations']` gespeichert
- Kann für Exception-Graph-Visualisierung verwendet werden
---
### 9. Exception Metrics & Monitoring Integration
**Ziel**: Integration mit Monitoring-Systemen (Prometheus, StatsD)
**Komponenten**:
- `ExceptionMetrics` - Value Object für Metriken
- `ExceptionMetricsCollector` - Metrics-Collector
- `PrometheusExporter` - Prometheus-Export
**Verwendung**:
```php
use App\Framework\ExceptionHandling\Metrics\ExceptionMetricsCollector;
use App\Framework\ExceptionHandling\Metrics\PrometheusExporter;
$collector = new ExceptionMetricsCollector($cache);
// Exception-Metrik aufzeichnen
$collector->record($exception, $context, $executionTimeMs);
// Metriken abrufen
$metrics = $collector->getMetrics();
// Prometheus-Format exportieren
$exporter = new PrometheusExporter();
$prometheusMetrics = $exporter->export($metrics);
// Output:
// exception_total 42
// exception_by_class{exception_class="DatabaseException"} 10
// exception_by_component{component="UserService"} 5
// exception_average_execution_time_ms 125.50
```
**Metriken**:
- `totalCount`: Gesamtanzahl Exceptions
- `byClass`: Anzahl pro Exception-Klasse
- `byComponent`: Anzahl pro Component
- `averageExecutionTimeMs`: Durchschnittliche Ausführungszeit
**Integration**:
- Kann in `ProductionMetricsController` integriert werden
- Metriken werden im Prometheus-Format exportiert
- Real-time Aggregation über Time-Windows
---
### 10. Exception Health Checks & Circuit Breakers
**Ziel**: Circuit Breaker Pattern für Exception-basierte Health Checks
**Komponenten**:
- `ExceptionHealthChecker` - Health Checker
**Verwendung**:
```php
use App\Framework\ExceptionHandling\Health\ExceptionHealthChecker;
use App\Framework\ExceptionHandling\Metrics\ExceptionMetricsCollector;
$metricsCollector = new ExceptionMetricsCollector($cache);
$healthChecker = new ExceptionHealthChecker(
metricsCollector: $metricsCollector,
errorRateThreshold: 0.1, // 10% Error Rate
timeWindowSeconds: 60
);
// Health Check durchführen
$result = $healthChecker->check();
// Status:
// - Healthy: Error Rate < 5%
// - Warning: Error Rate 5-10%
// - Unhealthy: Error Rate > 10%
```
**Health Check Status**:
- **Healthy**: Error Rate unterhalb des Thresholds
- **Warning**: Error Rate bei 50% des Thresholds
- **Unhealthy**: Error Rate oberhalb des Thresholds
**Integration**:
- Implementiert `HealthCheckInterface`
- Kann in `HealthCheckManager` registriert werden
- Nutzt bestehende `CircuitBreaker` Infrastruktur
---
## DI Container Integration
### Beispiel-Konfiguration
```php
// Exception Rate Limiter
$container->bind(ExceptionRateLimitConfig::class, function() {
return ExceptionRateLimitConfig::withLimits(
maxExceptions: 10,
timeWindow: Duration::fromSeconds(60)
);
});
$container->singleton(ExceptionRateLimiter::class, function($container) {
return new ExceptionRateLimiter(
cache: $container->get(Cache::class),
config: $container->get(ExceptionRateLimitConfig::class)
);
});
// Exception Context Cache
$container->singleton(ExceptionContextCache::class, function($container) {
return new ExceptionContextCache(
cache: $container->get(Cache::class)
);
});
// Exception Context Builder
$container->singleton(ExceptionContextBuilder::class, function($container) {
return new ExceptionContextBuilder(
errorScope: $container->get(ErrorScope::class),
contextCache: $container->get(ExceptionContextCache::class)
);
});
// Exception Performance Tracker
$container->singleton(ExceptionPerformanceTracker::class, function() {
return new ExceptionPerformanceTracker();
});
// Exception Message Translator
$container->singleton(ExceptionMessageTranslator::class, function($container) {
$templates = require __DIR__ . '/config/exception_messages.php';
return new ExceptionMessageTranslator(
templates: $templates,
isDebugMode: $container->get('config')->get('app.debug', false)
);
});
// Exception Localizer
$container->singleton(ExceptionLocalizer::class, function() {
$translations = require __DIR__ . '/config/exception_translations.php';
return new ExceptionLocalizer(
translations: $translations,
defaultLocale: 'en'
);
});
// Exception Recovery Manager
$container->singleton(ExceptionRecoveryManager::class, function() {
return new ExceptionRecoveryManager();
});
// Exception Pattern Detector
$container->singleton(ExceptionPatternDetector::class, function() {
$knowledgeBase = require __DIR__ . '/config/exception_patterns.php';
return new ExceptionPatternDetector($knowledgeBase);
});
// Exception Correlation Engine
$container->singleton(ExceptionCorrelationEngine::class, function($container) {
return new ExceptionCorrelationEngine(
cache: $container->get(Cache::class)
);
});
// Exception Metrics Collector
$container->singleton(ExceptionMetricsCollector::class, function($container) {
return new ExceptionMetricsCollector(
cache: $container->get(Cache::class)
);
});
// Exception Health Checker
$container->singleton(ExceptionHealthChecker::class, function($container) {
return new ExceptionHealthChecker(
metricsCollector: $container->get(ExceptionMetricsCollector::class),
errorRateThreshold: 0.1,
timeWindowSeconds: 60
);
});
// ErrorKernel mit allen Features
$container->singleton(ErrorKernel::class, function($container) {
return new ErrorKernel(
rendererFactory: $container->get(ErrorRendererFactory::class),
reporter: $container->get(Reporter::class),
errorAggregator: $container->get(ErrorAggregatorInterface::class),
contextProvider: $container->get(ExceptionContextProvider::class),
auditLogger: $container->get(ExceptionAuditLogger::class),
rateLimiter: $container->get(ExceptionRateLimiter::class), // NEU
executionContext: $container->get(ExecutionContext::class),
consoleOutput: $container->get(ConsoleOutput::class),
isDebugMode: $container->get('config')->get('app.debug', false)
);
});
```
---
## Best Practices
### 1. Rate Limiting konfigurieren
```php
// Für Production: Strikte Limits
$config = ExceptionRateLimitConfig::withLimits(
maxExceptions: 5,
timeWindow: Duration::fromSeconds(60)
);
// Für Development: Lockerere Limits
$config = ExceptionRateLimitConfig::withLimits(
maxExceptions: 50,
timeWindow: Duration::fromSeconds(60)
);
```
### 2. Context Caching nutzen
```php
// Context Cache nur aktivieren wenn Context-Erstellung teuer ist
// (z.B. DB-Queries, externe API-Calls)
if ($needsCaching) {
$builder = new ExceptionContextBuilder(
errorScope: $errorScope,
contextCache: $contextCache
);
} else {
$builder = new ExceptionContextBuilder(errorScope: $errorScope);
}
```
### 3. User-Friendly Messages definieren
```php
// Alle wichtigen Exceptions sollten User-Friendly Messages haben
$templates = [
\App\Framework\Exception\DatabaseException::class => [
'message' => 'Database connection failed. Please try again later.',
'title' => 'Database Error',
'help' => 'If this problem persists, please contact support.'
],
\App\Framework\Exception\ValidationException::class => [
'message' => 'Please check your input and try again.',
'title' => 'Validation Error'
]
];
```
### 4. Retry-Strategien wählen
```php
// Für Network-Exceptions: Exponential Backoff
$metadata = ExceptionMetadata::withRetry(1000)
->withRetryStrategy('exponential_backoff');
// Für Rate-Limited APIs: Linear
$metadata = ExceptionMetadata::withRetry(2000)
->withRetryStrategy('linear');
// Für Polling: Fixed Delay
$metadata = ExceptionMetadata::withRetry(5000)
->withRetryStrategy('fixed');
```
### 5. Health Checks konfigurieren
```php
// Für kritische Services: Strikte Thresholds
$healthChecker = new ExceptionHealthChecker(
metricsCollector: $metricsCollector,
errorRateThreshold: 0.05, // 5%
timeWindowSeconds: 60
);
// Für weniger kritische Services: Lockerere Thresholds
$healthChecker = new ExceptionHealthChecker(
metricsCollector: $metricsCollector,
errorRateThreshold: 0.2, // 20%
timeWindowSeconds: 300
);
```
---
## Migration Guide
### Von altem System migrieren
1. **Rate Limiting aktivieren**:
```php
// In DI Container
$container->singleton(ExceptionRateLimiter::class, ...);
```
2. **Context Caching aktivieren** (optional):
```php
// Nur wenn Performance-Probleme auftreten
$container->singleton(ExceptionContextCache::class, ...);
```
3. **User-Friendly Messages hinzufügen**:
```php
// Templates in config/exception_messages.php definieren
```
4. **Health Checks registrieren**:
```php
// In HealthCheckManager
$healthCheckManager->register($exceptionHealthChecker);
```
---
## Troubleshooting
### Rate Limiting blockiert zu viele Exceptions
**Problem**: Rate Limiting blockiert auch wichtige Exceptions
**Lösung**: Threshold erhöhen oder Time Window anpassen
```php
$config = ExceptionRateLimitConfig::withLimits(
maxExceptions: 20, // Erhöht
timeWindow: Duration::fromSeconds(60)
);
```
### Context Cache liefert veraltete Daten
**Problem**: Context Cache enthält veraltete User/Session-Daten
**Lösung**: Cache bei User/Session-Änderungen invalidieren
```php
$contextCache->invalidateUser($userId);
$contextCache->invalidateSession($sessionId);
```
### Performance Tracking zeigt keine Daten
**Problem**: Performance-Metriken werden nicht aufgezeichnet
**Lösung**: Sicherstellen dass `ExceptionPerformanceTracker` in `ErrorKernel` integriert ist
```php
// Performance-Tracking manuell starten
$startData = $performanceTracker->start();
// ... exception occurs ...
$metrics = $performanceTracker->end($startData, $exception, $context);
```
---
## Weitere Ressourcen
- [Error Handling Guidelines](./error-handling.md)
- [Exception Architecture](../ERROR-HANDLING-UNIFIED-ARCHITECTURE.md)
- [Audit Logging](./audit-logging.md)

View File

@@ -0,0 +1,645 @@
# Error Handling & Debugging
This guide covers error handling patterns and debugging strategies in the framework.
## Exception Handling
All custom exceptions in the framework must extend `FrameworkException` to ensure consistent error handling, logging, and recovery mechanisms.
### The FrameworkException System
The framework provides a sophisticated exception system with:
- **ExceptionContext**: Rich context information for debugging
- **ErrorCode**: Categorized error codes with recovery hints
- **RetryAfter**: Support for recoverable operations
- **Fluent Interface**: Easy context building
### Creating Custom Exceptions
```php
namespace App\Domain\User\Exceptions;
use App\Framework\Exception\FrameworkException;
use App\Framework\Exception\Core\DatabaseErrorCode;
use App\Framework\Exception\ExceptionContext;
final class UserNotFoundException extends FrameworkException
{
public static function byId(UserId $id): self
{
return self::create(
DatabaseErrorCode::ENTITY_NOT_FOUND,
"User with ID '{$id->toString()}' not found"
)->withData([
'user_id' => $id->toString(),
'search_type' => 'by_id'
]);
}
public static function byEmail(Email $email): self
{
$context = ExceptionContext::forOperation('user.lookup', 'UserRepository')
->withData(['email' => $email->getMasked()]);
return self::fromContext(
"User with email not found",
$context,
DatabaseErrorCode::ENTITY_NOT_FOUND
);
}
}
```
### Using ErrorCode Enums
The framework provides category-specific error code enums for better organization and type safety:
```php
use App\Framework\Exception\Core\DatabaseErrorCode;
use App\Framework\Exception\Core\AuthErrorCode;
use App\Framework\Exception\Core\HttpErrorCode;
use App\Framework\Exception\Core\SecurityErrorCode;
use App\Framework\Exception\Core\ValidationErrorCode;
// Database errors
DatabaseErrorCode::CONNECTION_FAILED
DatabaseErrorCode::QUERY_FAILED
DatabaseErrorCode::TRANSACTION_FAILED
DatabaseErrorCode::CONSTRAINT_VIOLATION
// Authentication errors
AuthErrorCode::CREDENTIALS_INVALID
AuthErrorCode::TOKEN_EXPIRED
AuthErrorCode::SESSION_EXPIRED
AuthErrorCode::ACCOUNT_LOCKED
// HTTP errors
HttpErrorCode::BAD_REQUEST
HttpErrorCode::NOT_FOUND
HttpErrorCode::METHOD_NOT_ALLOWED
HttpErrorCode::RATE_LIMIT_EXCEEDED
// Security errors
SecurityErrorCode::CSRF_TOKEN_INVALID
SecurityErrorCode::SQL_INJECTION_DETECTED
SecurityErrorCode::XSS_DETECTED
SecurityErrorCode::PATH_TRAVERSAL_DETECTED
// Validation errors
ValidationErrorCode::INVALID_INPUT
ValidationErrorCode::REQUIRED_FIELD_MISSING
ValidationErrorCode::BUSINESS_RULE_VIOLATION
ValidationErrorCode::INVALID_FORMAT
// Using error codes in exceptions:
throw FrameworkException::create(
DatabaseErrorCode::QUERY_FAILED,
"Failed to execute user query"
)->withContext(
ExceptionContext::forOperation('user.find', 'UserRepository')
->withData(['query' => 'SELECT * FROM users WHERE id = ?'])
->withDebug(['bind_params' => [$userId]])
);
```
### Exception Context Building
```php
// Method 1: Using factory methods
$exception = FrameworkException::forOperation(
'payment.process',
'PaymentService',
'Payment processing failed',
HttpErrorCode::BAD_GATEWAY
)->withData([
'amount' => $amount->toArray(),
'gateway' => 'stripe',
'customer_id' => $customerId
])->withMetadata([
'attempt' => 1,
'idempotency_key' => $idempotencyKey
]);
// Method 2: Building context separately
$context = ExceptionContext::empty()
->withOperation('order.validate', 'OrderService')
->withData([
'order_id' => $orderId,
'total' => $total->toDecimal()
])
->withDebug([
'validation_rules' => ['min_amount', 'max_items'],
'failed_rule' => 'min_amount'
]);
throw FrameworkException::fromContext(
'Order validation failed',
$context,
ValidationErrorCode::BUSINESS_RULE_VIOLATION
);
```
### Recoverable Exceptions
```php
// Creating recoverable exceptions with retry hints
final class RateLimitException extends FrameworkException
{
public static function exceeded(int $retryAfter): self
{
return self::create(
HttpErrorCode::RATE_LIMIT_EXCEEDED,
'API rate limit exceeded'
)->withRetryAfter($retryAfter)
->withData(['retry_after_seconds' => $retryAfter]);
}
}
// Using in code
try {
$response = $apiClient->request($endpoint);
} catch (RateLimitException $e) {
if ($e->isRecoverable()) {
$waitTime = $e->getRetryAfter();
// Schedule retry after $waitTime seconds
}
throw $e;
}
```
### Exception Categories
```php
// Check exception category for handling strategies
try {
$result = $operation->execute();
} catch (FrameworkException $e) {
if ($e->isCategory('AUTH')) {
// Handle authentication errors
return $this->redirectToLogin();
}
if ($e->isCategory('VAL')) {
// Handle validation errors
return $this->validationErrorResponse($e);
}
if ($e->isErrorCode(DatabaseErrorCode::CONNECTION_FAILED)) {
// Handle specific database connection errors
$this->notifyOps($e);
}
throw $e;
}
```
### Simple Exceptions for Quick Use
```php
// When you don't need the full context system
throw FrameworkException::simple('Quick error message');
// With previous exception
} catch (\PDOException $e) {
throw FrameworkException::simple(
'Database operation failed',
$e,
500
);
}
```
### Exception Data Sanitization
The framework automatically sanitizes sensitive data in exceptions:
```php
// Sensitive keys are automatically redacted
$exception->withData([
'username' => 'john@example.com',
'password' => 'secret123', // Will be logged as '[REDACTED]'
'api_key' => 'sk_live_...' // Will be logged as '[REDACTED]'
]);
```
### Best Practices
1. **Always extend FrameworkException** for custom exceptions
2. **Use ErrorCode enum** for categorizable errors
3. **Provide rich context** with operation, component, and data
4. **Use factory methods** for consistent exception creation
5. **Sanitize sensitive data** (automatic for common keys)
6. **Make exceptions domain-specific** (UserNotFoundException vs generic NotFoundException)
7. **Include recovery hints** for recoverable errors
## Unified Error Kernel Architecture
The framework uses a **context-aware error handling system** centered around the `ErrorKernel` class that automatically detects execution context (CLI vs HTTP) and handles errors accordingly.
### ErrorKernel Overview
**Location**: `src/Framework/ExceptionHandling/ErrorKernel.php`
**Key Features**:
- Automatic context detection (CLI vs HTTP)
- Colored console output for CLI errors
- HTTP Response objects for web errors
- Integration with OWASP Security Event System
- Unified error logging via LogReporter
```php
use App\Framework\ExceptionHandling\ErrorKernel;
final readonly class ErrorKernel
{
public function __construct(
private ErrorRendererFactory $rendererFactory = new ErrorRendererFactory,
private ?ExecutionContext $executionContext = null,
private ?ConsoleOutput $consoleOutput = null
) {}
/**
* Context-aware exception handler
* - CLI: Colored console output
* - HTTP: Logs error (middleware creates response)
*/
public function handle(Throwable $e, array $context = []): mixed
{
// Automatic logging
$log = new LogReporter();
$log->report($e);
// Context-aware handling
$executionContext = $this->executionContext ?? ExecutionContext::detect();
if ($executionContext->isCli()) {
$this->handleCliException($e);
return null;
}
// HTTP context - middleware will create response
return null;
}
/**
* Create HTTP Response from exception (for middleware recovery)
*/
public function createHttpResponse(
Throwable $exception,
?ExceptionContextProvider $contextProvider = null,
bool $isDebugMode = false
): Response {
$renderer = new ResponseErrorRenderer($isDebugMode);
return $renderer->createResponse($exception, $contextProvider);
}
}
```
### CLI Error Handling
**CliErrorHandler** registers global PHP error handlers for CLI context:
```php
use App\Framework\ExceptionHandling\CliErrorHandler;
use App\Framework\Console\ConsoleOutput;
// Registration in AppBootstrapper
$output = new ConsoleOutput();
$cliErrorHandler = new CliErrorHandler($output);
$cliErrorHandler->register();
// Automatic colored output for errors:
// - Red for uncaught exceptions
// - Yellow for warnings
// - Cyan for notices
// - Full stack traces in CLI
```
### HTTP Error Handling
**ExceptionHandlingMiddleware** catches exceptions in HTTP request pipeline:
```php
use App\Framework\Http\Middlewares\ExceptionHandlingMiddleware;
#[MiddlewarePriorityAttribute(MiddlewarePriority::ERROR_HANDLING)]
final readonly class ExceptionHandlingMiddleware implements HttpMiddleware
{
public function __invoke(
MiddlewareContext $context,
Next $next,
RequestStateManager $stateManager
): MiddlewareContext {
try {
return $next($context);
} catch (\Throwable $e) {
// Log exception
$this->logger->error('Unhandled exception in HTTP request', [
'exception' => get_class($e),
'message' => $e->getMessage(),
]);
// Create HTTP Response
$errorKernel = new ErrorKernel();
$response = $errorKernel->createHttpResponse(
$e,
null,
isDebugMode: false
);
return $context->withResponse($response);
}
}
}
```
### OWASP Security Event Integration
Exceptions can trigger OWASP security events for audit logging:
```php
use App\Application\Security\OWASPSecurityEventLogger;
use App\Application\Security\OWASPEventIdentifier;
// Automatic security logging
try {
$this->authenticateUser($credentials);
} catch (AuthenticationException $e) {
// ErrorKernel logs exception
$this->errorKernel->handle($e);
// OWASP event for security audit trail
$this->eventDispatcher->dispatch(
new AuthenticationFailedEvent(
OWASPEventIdentifier::AUTHN_LOGIN_FAILURE,
$credentials->username,
$e->getMessage()
)
);
throw $e;
}
```
### Legacy ErrorHandling Module Removed
**IMPORTANT**: The legacy `ErrorHandling` module (`src/Framework/ErrorHandling/`) has been **completely removed** as of the unified exception architecture migration.
**Migration Path**:
- All error handling now uses `ErrorKernel` and `FrameworkException`
- CLI errors: `CliErrorHandler``ErrorKernel`
- HTTP errors: `ExceptionHandlingMiddleware``ErrorKernel`
- Security events: Direct event dispatch via `EventDispatcher`
**Old Pattern** (removed):
```php
// ❌ Legacy - NO LONGER EXISTS
use App\Framework\ErrorHandling\ErrorHandler;
use App\Framework\ErrorHandling\SecurityEventLogger;
$errorHandler = new ErrorHandler();
$errorHandler->register();
```
**New Pattern** (current):
```php
// ✅ Unified - ErrorKernel
use App\Framework\ExceptionHandling\ErrorKernel;
$errorKernel = new ErrorKernel();
$errorKernel->handle($exception);
```
## Logging Best Practices
### Automatic Exception Logging
All exceptions handled by `ErrorKernel` are automatically logged via `LogReporter`:
```php
// Automatic logging happens in ErrorKernel::handle()
$log = new LogReporter();
$log->report($exception);
// Logs include:
// - Exception class and message
// - Stack trace
// - File and line number
// - Context data
```
### Manual Logging
```php
use App\Framework\Logging\Logger;
// Log exceptions with context
try {
$user = $this->userRepository->find($userId);
} catch (UserNotFoundException $e) {
Logger::error('User lookup failed', [
'user_id' => $userId,
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
throw $e;
}
// Log levels
Logger::debug('Debugging information');
Logger::info('Informational message');
Logger::warning('Warning condition');
Logger::error('Error condition');
Logger::critical('Critical failure');
```
## Debug Strategies
### Development vs Production
**Development** (`APP_DEBUG=true`):
- Full stack traces displayed
- Detailed error messages
- Debug data in responses
- SQL query logging
**Production** (`APP_DEBUG=false`):
- Generic error messages
- Stack traces hidden from users
- Errors logged server-side
- Security-safe responses
### Debugging Tools
```php
// Enable debug mode in ErrorKernel
$errorKernel = new ErrorKernel(
executionContext: ExecutionContext::cli(),
consoleOutput: new ConsoleOutput()
);
// HTTP Response with debug mode
$response = $errorKernel->createHttpResponse(
$exception,
$contextProvider,
isDebugMode: true // Shows stack trace in response
);
```
### Error Context Providers
```php
use App\Framework\ExceptionHandling\Context\ExceptionContextProvider;
// Attach custom context to exceptions
$contextProvider = new ExceptionContextProvider();
$contextProvider->attachContext($exception, [
'request_id' => $requestId,
'user_id' => $userId,
'operation' => 'payment.process'
]);
$response = $errorKernel->createHttpResponse(
$exception,
$contextProvider,
isDebugMode: false
);
```
## Error Recovery Patterns
### Graceful Degradation
```php
// Try primary service, fallback to secondary
try {
return $this->primaryCache->get($key);
} catch (CacheException $e) {
Logger::warning('Primary cache failed, using fallback', [
'error' => $e->getMessage()
]);
return $this->fallbackCache->get($key);
}
```
### Circuit Breaker Pattern
```php
use App\Framework\Resilience\CircuitBreaker;
$circuitBreaker = new CircuitBreaker(
failureThreshold: 5,
timeout: Duration::fromSeconds(60)
);
try {
return $circuitBreaker->call(function() {
return $this->externalApi->request($endpoint);
});
} catch (CircuitOpenException $e) {
// Circuit is open - use cached response
return $this->cachedResponse;
}
```
### Retry with Exponential Backoff
```php
use App\Framework\Queue\ValueObjects\RetryStrategy;
$retryStrategy = new ExponentialBackoffStrategy(
maxAttempts: 3,
baseDelaySeconds: 60
);
$attempt = 0;
while ($attempt < $retryStrategy->getMaxAttempts()) {
try {
return $this->performOperation();
} catch (TransientException $e) {
$attempt++;
if (!$retryStrategy->shouldRetry($attempt)) {
throw $e;
}
$delay = $retryStrategy->getDelay($attempt);
sleep($delay->toSeconds());
}
}
```
## Common Error Scenarios
### 1. Database Connection Failure
```php
try {
$connection = $this->connectionPool->getConnection();
} catch (ConnectionException $e) {
// Log error
$this->errorKernel->handle($e);
// Return cached data or error response
return $this->getCachedData() ?? $this->errorResponse();
}
```
### 2. Validation Errors
```php
try {
$user = User::create($email, $name);
} catch (ValidationException $e) {
// Return validation errors to user
return new JsonResult([
'errors' => $e->getErrors()
], status: Status::UNPROCESSABLE_ENTITY);
}
```
### 3. Authentication Failures
```php
try {
$user = $this->authenticator->authenticate($credentials);
} catch (AuthenticationException $e) {
// Log security event
$this->eventDispatcher->dispatch(
new AuthenticationFailedEvent($credentials->username)
);
// Return 401 Unauthorized
return new JsonResult([
'error' => 'Invalid credentials'
], status: Status::UNAUTHORIZED);
}
```
### 4. Resource Not Found
```php
try {
$order = $this->orderRepository->find($orderId);
} catch (OrderNotFoundException $e) {
// Return 404 Not Found
return new JsonResult([
'error' => 'Order not found'
], status: Status::NOT_FOUND);
}
```
### 5. Rate Limit Exceeded
```php
try {
$this->rateLimiter->checkLimit($userId);
} catch (RateLimitException $e) {
// Return 429 Too Many Requests with retry hint
return new JsonResult([
'error' => 'Rate limit exceeded',
'retry_after' => $e->getRetryAfter()
], status: Status::TOO_MANY_REQUESTS);
}
```

View File

@@ -0,0 +1,963 @@
# Event System
This guide covers the event-driven architecture of the framework.
## Overview
The framework provides a sophisticated event system built on two core components:
- **EventBus**: Simple, lightweight interface for basic event dispatching
- **EventDispatcher**: Full-featured implementation with handler management, priorities, and propagation control
**Key Features**:
- Attribute-based event handler registration via `#[OnEvent]`
- Priority-based handler execution
- Event propagation control (stop propagation)
- Support for inheritance and interface-based event matching
- Automatic handler discovery via framework's attribute system
- Domain events for business logic decoupling
- Application lifecycle events for framework integration
## EventBus vs EventDispatcher
### EventBus Interface
**Purpose**: Simple contract for event dispatching
```php
interface EventBus
{
public function dispatch(object $event): void;
}
```
**When to use**:
- Simple event dispatching without return values
- Fire-and-forget events
- When you don't need handler results
- Dependency injection when you want interface-based type hints
**Example Usage**:
```php
final readonly class OrderService
{
public function __construct(
private EventBus $eventBus
) {}
public function createOrder(CreateOrderCommand $command): Order
{
$order = Order::create($command);
// Fire event without caring about results
$this->eventBus->dispatch(new OrderCreatedEvent($order));
return $order;
}
}
```
### EventDispatcher Implementation
**Purpose**: Full-featured event system with handler management and results
**Key Features**:
- Returns array of handler results
- Priority-based handler execution (highest priority first)
- Stop propagation support
- Manual handler registration via `addHandler()` or `listen()`
- Automatic handler discovery via `#[OnEvent]` attribute
- Inheritance and interface matching
**When to use**:
- Need handler return values for processing
- Want priority control over execution order
- Need to stop event propagation conditionally
- Require manual handler registration
- Building complex event workflows
**Example Usage**:
```php
final readonly class PaymentProcessor
{
public function __construct(
private EventDispatcher $dispatcher
) {}
public function processPayment(Payment $payment): PaymentResult
{
// Dispatch and collect results from all handlers
$results = $this->dispatcher->dispatch(
new PaymentProcessingEvent($payment)
);
// Process handler results
foreach ($results as $result) {
if ($result instanceof PaymentValidationFailure) {
throw new PaymentException($result->reason);
}
}
return new PaymentResult($payment, $results);
}
}
```
### Implementation Relationship
```php
// EventDispatcher implements EventBus interface
final class EventDispatcher implements EventBus
{
public function dispatch(object $event): array
{
// Full implementation with handler management
}
}
// In DI Container
$container->singleton(EventBus::class, EventDispatcher::class);
$container->singleton(EventDispatcher::class, EventDispatcher::class);
```
**Recommendation**: Use `EventDispatcher` for type hints when you need full features, use `EventBus` interface when you want loose coupling and don't need results.
## Domain Events
### What are Domain Events?
Domain Events represent significant state changes or occurrences in your business domain. They enable loose coupling between domain components.
**Characteristics**:
- Immutable (`readonly` classes)
- Named in past tense (OrderCreated, UserRegistered, PaymentCompleted)
- Contain only relevant domain data
- No business logic (pure data objects)
- Include timestamp for audit trail
### Creating Domain Events
```php
namespace App\Domain\Order\Events;
use App\Domain\Order\ValueObjects\OrderId;
use App\Framework\Core\ValueObjects\Timestamp;
final readonly class OrderCreatedEvent
{
public readonly Timestamp $occurredAt;
public function __construct(
public OrderId $orderId,
public UserId $userId,
public Money $total,
?Timestamp $occurredAt = null
) {
$this->occurredAt = $occurredAt ?? Timestamp::now();
}
}
```
**Best Practices**:
- Use Value Objects for event properties
- Include timestamp for audit trail
- Keep events focused (single responsibility)
- Version events for backward compatibility
- Document event payload in PHPDoc
### Framework Lifecycle Events
The framework provides built-in events for application lifecycle:
#### ApplicationBooted
**Triggered**: After application bootstrap completes
```php
final readonly class ApplicationBooted
{
public function __construct(
public \DateTimeImmutable $bootTime,
public string $environment,
public Version $version,
public \DateTimeImmutable $occurredAt = new \DateTimeImmutable()
) {}
}
```
**Use Cases**:
- Initialize services after boot
- Start background workers
- Setup scheduled tasks
- Warm caches
#### BeforeHandleRequest
**Triggered**: Before HTTP request processing
```php
final readonly class BeforeHandleRequest
{
public function __construct(
public Request $request,
public Timestamp $timestamp,
public array $context = []
) {}
}
```
**Use Cases**:
- Request logging
- Performance monitoring
- Security checks
- Request transformation
#### AfterHandleRequest
**Triggered**: After HTTP request processing
```php
final readonly class AfterHandleRequest
{
public function __construct(
public Request $request,
public Response $response,
public Duration $processingTime,
public Timestamp $timestamp,
public array $context = []
) {}
}
```
**Use Cases**:
- Response logging
- Performance metrics
- Analytics collection
- Cleanup operations
### Example: User Registration Event
```php
namespace App\Domain\User\Events;
final readonly class UserRegisteredEvent
{
public readonly Timestamp $occurredAt;
public function __construct(
public UserId $userId,
public Email $email,
public UserName $userName,
?Timestamp $occurredAt = null
) {
$this->occurredAt = $occurredAt ?? Timestamp::now();
}
}
// Dispatching the event
final readonly class UserService
{
public function __construct(
private UserRepository $repository,
private EventBus $eventBus
) {}
public function register(RegisterUserCommand $command): User
{
$user = User::create(
$command->email,
$command->userName,
$command->password
);
$this->repository->save($user);
// Dispatch domain event
$this->eventBus->dispatch(
new UserRegisteredEvent(
$user->id,
$user->email,
$user->userName
)
);
return $user;
}
}
```
## Event Handler Registration
### Attribute-Based Registration
**Primary Method**: Use `#[OnEvent]` attribute for automatic discovery
```php
use App\Framework\Core\Events\OnEvent;
final readonly class UserEventHandlers
{
public function __construct(
private EmailService $emailService,
private Logger $logger
) {}
#[OnEvent(priority: 100)]
public function sendWelcomeEmail(UserRegisteredEvent $event): void
{
$this->emailService->send(
to: $event->email,
template: 'welcome',
data: ['userName' => $event->userName->value]
);
}
#[OnEvent(priority: 50)]
public function logUserRegistration(UserRegisteredEvent $event): void
{
$this->logger->info('User registered', [
'user_id' => $event->userId->toString(),
'email' => $event->email->value,
'occurred_at' => $event->occurredAt->format('Y-m-d H:i:s')
]);
}
}
```
**Attribute Parameters**:
- `priority` (optional): Handler execution order (higher = earlier, default: 0)
- `stopPropagation` (optional): Stop execution after this handler (default: false)
### Priority-Based Execution
Handlers execute in **priority order** (highest to lowest):
```php
#[OnEvent(priority: 200)] // Executes first
public function criticalHandler(OrderCreatedEvent $event): void { }
#[OnEvent(priority: 100)] // Executes second
public function highPriorityHandler(OrderCreatedEvent $event): void { }
#[OnEvent(priority: 50)] // Executes third
public function normalHandler(OrderCreatedEvent $event): void { }
#[OnEvent] // Executes last (default priority: 0)
public function defaultHandler(OrderCreatedEvent $event): void { }
```
**Use Cases for Priorities**:
- **Critical (200+)**: Security checks, validation, fraud detection
- **High (100-199)**: Transaction logging, audit trail
- **Normal (50-99)**: Business logic, notifications
- **Low (0-49)**: Analytics, metrics, cleanup
### Stop Propagation
**Purpose**: Prevent subsequent handlers from executing
```php
#[OnEvent(priority: 100, stopPropagation: true)]
public function validatePayment(PaymentProcessingEvent $event): PaymentValidationResult
{
$result = $this->validator->validate($event->payment);
if (!$result->isValid()) {
// Stop propagation - no further handlers execute
return new PaymentValidationFailure($result->errors);
}
return new PaymentValidationSuccess();
}
#[OnEvent(priority: 50)]
public function processPayment(PaymentProcessingEvent $event): void
{
// This handler only executes if validation passes
// (previous handler didn't set stopPropagation result)
$this->gateway->charge($event->payment);
}
```
**Important**: `stopPropagation` stops execution **after** the current handler completes.
### Manual Handler Registration
**Alternative**: Register handlers programmatically
```php
final readonly class EventInitializer
{
public function __construct(
private EventDispatcher $dispatcher
) {}
#[Initializer]
public function registerHandlers(): void
{
// Using listen() method
$this->dispatcher->listen(
OrderCreatedEvent::class,
function (OrderCreatedEvent $event) {
// Handle event
},
priority: 100
);
// Using addHandler() method
$this->dispatcher->addHandler(
eventClass: UserRegisteredEvent::class,
handler: [$this->userService, 'onUserRegistered'],
priority: 50
);
}
}
```
**When to use manual registration**:
- Dynamic handler registration based on configuration
- Third-party library integration
- Closure-based handlers for simple cases
- Runtime handler modification
### Handler Discovery
The framework automatically discovers handlers marked with `#[OnEvent]`:
```php
// In Application Bootstrap
final readonly class EventSystemInitializer
{
#[Initializer]
public function initialize(EventDispatcher $dispatcher): void
{
// Automatic discovery finds all #[OnEvent] methods
$discoveredHandlers = $this->attributeScanner->findMethodsWithAttribute(
OnEvent::class
);
foreach ($discoveredHandlers as $handler) {
$dispatcher->addHandler(
eventClass: $handler->getEventClass(),
handler: [$handler->instance, $handler->method],
priority: $handler->attribute->priority ?? 0
);
}
}
}
```
**No Manual Setup Required**: Framework handles discovery automatically during initialization.
## Event Middleware
**Concept**: Middleware pattern for event processing (transform, filter, log, etc.)
### Custom Event Middleware
```php
interface EventMiddleware
{
public function process(object $event, callable $next): mixed;
}
final readonly class LoggingEventMiddleware implements EventMiddleware
{
public function __construct(
private Logger $logger
) {}
public function process(object $event, callable $next): mixed
{
$eventClass = get_class($event);
$startTime = microtime(true);
$this->logger->debug("Event dispatched: {$eventClass}");
try {
$result = $next($event);
$duration = (microtime(true) - $startTime) * 1000;
$this->logger->debug("Event processed: {$eventClass}", [
'duration_ms' => $duration
]);
return $result;
} catch (\Throwable $e) {
$this->logger->error("Event failed: {$eventClass}", [
'error' => $e->getMessage()
]);
throw $e;
}
}
}
```
### Middleware Pipeline
```php
final class EventDispatcherWithMiddleware
{
/** @var EventMiddleware[] */
private array $middleware = [];
public function addMiddleware(EventMiddleware $middleware): void
{
$this->middleware[] = $middleware;
}
public function dispatch(object $event): array
{
$pipeline = array_reduce(
array_reverse($this->middleware),
fn($next, $middleware) => fn($event) => $middleware->process($event, $next),
fn($event) => $this->eventDispatcher->dispatch($event)
);
return $pipeline($event);
}
}
```
### Validation Middleware
```php
final readonly class ValidationEventMiddleware implements EventMiddleware
{
public function __construct(
private ValidatorInterface $validator
) {}
public function process(object $event, callable $next): mixed
{
// Validate event before dispatching
$errors = $this->validator->validate($event);
if (!empty($errors)) {
throw new InvalidEventException(
"Event validation failed: " . implode(', ', $errors)
);
}
return $next($event);
}
}
```
## Async Event Processing
### Queue-Based Async Handling
```php
use App\Framework\Queue\Queue;
use App\Framework\Queue\ValueObjects\JobPayload;
final readonly class AsyncEventHandler
{
public function __construct(
private Queue $queue
) {}
#[OnEvent(priority: 50)]
public function processAsync(OrderCreatedEvent $event): void
{
// Dispatch to queue for async processing
$job = new ProcessOrderEmailJob(
orderId: $event->orderId,
userEmail: $event->userEmail
);
$this->queue->push(
JobPayload::immediate($job)
);
}
}
// Background Job
final readonly class ProcessOrderEmailJob
{
public function __construct(
public OrderId $orderId,
public Email $userEmail
) {}
public function handle(EmailService $emailService): void
{
// Process email asynchronously
$emailService->sendOrderConfirmation(
$this->orderId,
$this->userEmail
);
}
}
```
### Event Buffering Pattern
```php
final class EventBuffer
{
private array $bufferedEvents = [];
#[OnEvent]
public function bufferEvent(DomainEvent $event): void
{
$this->bufferedEvents[] = $event;
}
public function flush(EventDispatcher $dispatcher): void
{
foreach ($this->bufferedEvents as $event) {
$dispatcher->dispatch($event);
}
$this->bufferedEvents = [];
}
}
// Usage in transaction
try {
$this->entityManager->beginTransaction();
// Buffer events during transaction
$this->eventBuffer->bufferEvent(new OrderCreatedEvent(/* ... */));
$this->eventBuffer->bufferEvent(new InventoryReservedEvent(/* ... */));
$this->entityManager->commit();
// Flush events after successful commit
$this->eventBuffer->flush($this->dispatcher);
} catch (\Exception $e) {
$this->entityManager->rollback();
// Events are not flushed on rollback
throw $e;
}
```
## Event Best Practices
### 1. Event Naming
**✅ Good**: Past tense, descriptive
```php
OrderCreatedEvent
UserRegisteredEvent
PaymentCompletedEvent
InventoryReservedEvent
```
**❌ Bad**: Present/future tense, vague
```php
CreateOrderEvent
RegisterUserEvent
CompletePayment
ReserveInventory
```
### 2. Event Granularity
**✅ Focused Events**:
```php
final readonly class OrderCreatedEvent { /* ... */ }
final readonly class OrderShippedEvent { /* ... */ }
final readonly class OrderCancelledEvent { /* ... */ }
```
**❌ God Events**:
```php
final readonly class OrderEvent
{
public function __construct(
public string $action, // 'created', 'shipped', 'cancelled'
public Order $order
) {}
}
```
### 3. Immutability
**✅ Readonly Events**:
```php
final readonly class UserUpdatedEvent
{
public function __construct(
public UserId $userId,
public Email $newEmail
) {}
}
```
**❌ Mutable Events**:
```php
final class UserUpdatedEvent
{
public UserId $userId;
public Email $newEmail;
public function setEmail(Email $email): void
{
$this->newEmail = $email; // Bad: Events should be immutable
}
}
```
### 4. Handler Independence
**✅ Independent Handlers**:
```php
#[OnEvent]
public function sendEmail(OrderCreatedEvent $event): void
{
// Self-contained - doesn't depend on other handlers
$this->emailService->send(/* ... */);
}
#[OnEvent]
public function updateInventory(OrderCreatedEvent $event): void
{
// Independent - no shared state with email handler
$this->inventoryService->reserve(/* ... */);
}
```
**❌ Coupled Handlers**:
```php
private bool $emailSent = false;
#[OnEvent(priority: 100)]
public function sendEmail(OrderCreatedEvent $event): void
{
$this->emailService->send(/* ... */);
$this->emailSent = true; // Bad: Shared state
}
#[OnEvent(priority: 50)]
public function logEmailSent(OrderCreatedEvent $event): void
{
if ($this->emailSent) { // Bad: Depends on other handler
$this->logger->info('Email sent');
}
}
```
### 5. Error Handling
**✅ Graceful Degradation**:
```php
#[OnEvent]
public function sendNotification(OrderCreatedEvent $event): void
{
try {
$this->notificationService->send(/* ... */);
} catch (NotificationException $e) {
// Log error but don't fail the entire event dispatch
$this->logger->error('Notification failed', [
'order_id' => $event->orderId->toString(),
'error' => $e->getMessage()
]);
}
}
```
### 6. Avoid Circular Events
**❌ Circular Dependency**:
```php
#[OnEvent]
public function onOrderCreated(OrderCreatedEvent $event): void
{
// Bad: Dispatching event from event handler can cause loops
$this->eventBus->dispatch(new OrderProcessedEvent($event->orderId));
}
#[OnEvent]
public function onOrderProcessed(OrderProcessedEvent $event): void
{
// Bad: Creates circular dependency
$this->eventBus->dispatch(new OrderCreatedEvent(/* ... */));
}
```
**✅ Use Command/Service Layer**:
```php
#[OnEvent]
public function onOrderCreated(OrderCreatedEvent $event): void
{
// Good: Use service to perform additional work
$this->orderProcessingService->processOrder($event->orderId);
}
```
### 7. Performance Considerations
**Heavy Operations → Queue**:
```php
#[OnEvent]
public function generateInvoicePdf(OrderCreatedEvent $event): void
{
// Heavy operation - push to queue
$this->queue->push(
JobPayload::immediate(
new GenerateInvoiceJob($event->orderId)
)
);
}
```
**Light Operations → Synchronous**:
```php
#[OnEvent]
public function logOrderCreation(OrderCreatedEvent $event): void
{
// Light operation - execute immediately
$this->logger->info('Order created', [
'order_id' => $event->orderId->toString()
]);
}
```
## Common Patterns
### Event Sourcing Pattern
```php
final class OrderEventStore
{
#[OnEvent]
public function appendEvent(DomainEvent $event): void
{
$this->eventStore->append([
'event_type' => get_class($event),
'event_data' => json_encode($event),
'occurred_at' => $event->occurredAt->format('Y-m-d H:i:s'),
'aggregate_id' => $event->getAggregateId()
]);
}
}
```
### Saga Pattern
```php
final readonly class OrderSaga
{
#[OnEvent(priority: 100)]
public function onOrderCreated(OrderCreatedEvent $event): void
{
// Step 1: Reserve inventory
$this->inventoryService->reserve($event->items);
}
#[OnEvent(priority: 90)]
public function onInventoryReserved(InventoryReservedEvent $event): void
{
// Step 2: Process payment
$this->paymentService->charge($event->orderId);
}
#[OnEvent(priority: 80)]
public function onPaymentCompleted(PaymentCompletedEvent $event): void
{
// Step 3: Ship order
$this->shippingService->ship($event->orderId);
}
}
```
### CQRS Pattern
```php
// Command Side - Dispatches Events
final readonly class CreateOrderHandler
{
public function handle(CreateOrderCommand $command): Order
{
$order = Order::create($command);
$this->repository->save($order);
$this->eventBus->dispatch(
new OrderCreatedEvent($order)
);
return $order;
}
}
// Query Side - Maintains Read Model
#[OnEvent]
public function updateOrderReadModel(OrderCreatedEvent $event): void
{
$this->orderReadModel->insert([
'order_id' => $event->orderId->toString(),
'user_id' => $event->userId->toString(),
'total' => $event->total->toDecimal(),
'status' => 'created'
]);
}
```
## Framework Integration
### With Queue System
```php
#[OnEvent]
public function queueHeavyTask(OrderCreatedEvent $event): void
{
$this->queue->push(
JobPayload::immediate(
new ProcessOrderAnalyticsJob($event->orderId)
)
);
}
```
### With Scheduler
```php
#[OnEvent]
public function scheduleReminder(OrderCreatedEvent $event): void
{
$this->scheduler->schedule(
'send-order-reminder-' . $event->orderId,
OneTimeSchedule::at(Timestamp::now()->addDays(3)),
fn() => $this->emailService->sendReminder($event->orderId)
);
}
```
### With Cache System
```php
#[OnEvent]
public function invalidateCache(OrderCreatedEvent $event): void
{
$this->cache->forget(
CacheTag::fromString("user_{$event->userId}")
);
}
```
## Summary
The Event System provides:
-**Decoupled Architecture**: Loose coupling via events
-**Flexible Handling**: Priority-based, propagation control
-**Automatic Discovery**: `#[OnEvent]` attribute registration
-**Multiple Patterns**: Domain events, application events, async processing
-**Framework Integration**: Works with Queue, Scheduler, Cache
-**Testing Support**: Easy to unit test and integration test
-**Performance**: Efficient handler execution with priority optimization
**When to Use Events**:
- Decoupling domain logic from side effects (email, notifications)
- Cross-module communication without tight coupling
- Audit trail and event sourcing
- Async processing of non-critical operations
- CQRS and saga patterns
**When NOT to Use Events**:
- Synchronous business logic requiring immediate results
- Simple function calls (don't over-engineer)
- Performance-critical paths (events have overhead)
- Operations requiring transactional guarantees across handlers

View File

@@ -0,0 +1,965 @@
# Filesystem Patterns
Comprehensive guide to the Custom PHP Framework's Filesystem module.
## Overview
The Filesystem module provides a robust, type-safe, and security-focused abstraction for file operations. Built on framework principles of immutability, readonly classes, and value objects.
**Core Components**:
- `FileStorage` - Main filesystem operations interface
- `FileValidator` - Security-focused path and content validation
- `SerializerRegistry` - Automatic serializer detection and management
- `FileOperationContext` - Rich logging context for operations
- `TemporaryDirectory` - Safe temporary file management
---
## FileStorage
### Basic Usage
```php
use App\Framework\Filesystem\FileStorage;
use App\Framework\Filesystem\FileValidator;
use App\Framework\Logging\Logger;
// Simple storage without validation or logging
$storage = new FileStorage('/var/www/storage');
// Write file
$storage->put('documents/report.txt', 'File contents');
// Read file
$content = $storage->get('documents/report.txt');
// Check existence
if ($storage->exists('documents/report.txt')) {
// File exists
}
// Delete file
$storage->delete('documents/report.txt');
// Copy file
$storage->copy('source.txt', 'destination.txt');
// List directory
$files = $storage->files('documents');
```
### With Validator Integration
```php
// Create validator with security defaults
$validator = FileValidator::createDefault();
// Initialize storage with validator
$storage = new FileStorage(
baseDirectory: '/var/www/uploads',
validator: $validator
);
// All operations are now validated
try {
$storage->put('file.txt', 'content'); // Validates path, extension, size
} catch (FileValidationException $e) {
// Handle validation failure
}
```
### With Logger Integration
```php
use App\Framework\Logging\Logger;
$storage = new FileStorage(
baseDirectory: '/var/www/storage',
validator: FileValidator::createDefault(),
logger: $container->get(Logger::class)
);
// Operations are automatically logged with severity levels:
// - high severity: DELETE, MOVE → warning level
// - large operations (>10MB) → info level
// - normal operations → debug level
$storage->delete('important.txt'); // Logs as WARNING
```
---
## FileValidator
### Security-Focused Validation
The `FileValidator` provides multiple layers of security validation:
**1. Path Traversal Prevention**
- Detects `../`, `..\\`, URL-encoded variants
- Prevents null bytes in paths
- Optional base directory restriction
**2. Extension Filtering**
- Whitelist (allowedExtensions) - only specific extensions allowed
- Blacklist (blockedExtensions) - dangerous extensions blocked
- Case-insensitive matching
**3. File Size Limits**
- Maximum file size enforcement
- Uses `FileSize` value object for type safety
- Human-readable error messages
### Factory Methods
```php
// Default validator - blocks dangerous extensions, 100MB max
$validator = FileValidator::createDefault();
// Blocks: exe, bat, sh, cmd, com
// Max size: 100MB
// Strict validator - only whitelisted extensions allowed
$validator = FileValidator::createStrict(['txt', 'pdf', 'docx']);
// Only allows: txt, pdf, docx
// Max size: 50MB
// Upload validator - secure upload configuration
$validator = FileValidator::forUploads();
// Allows: jpg, jpeg, png, gif, pdf, txt, csv, json
// Blocks: exe, bat, sh, cmd, com, php, phtml
// Max size: 10MB
// Image validator - image files only
$validator = FileValidator::forImages();
// Allows: jpg, jpeg, png, gif, webp, svg
// Max size: 5MB
// Custom max size
$validator = FileValidator::forUploads(FileSize::fromMegabytes(50));
```
### Validation Methods
```php
// Individual validation methods
$validator->validatePath($path); // Path traversal, null bytes
$validator->validateExtension($path); // Extension whitelist/blacklist
$validator->validateFileSize($size); // Size limits
$validator->validateExists($path); // File existence
$validator->validateReadable($path); // Read permissions
$validator->validateWritable($path); // Write permissions
// Composite validation for common operations
$validator->validateRead($path); // Path + exists + readable
$validator->validateWrite($path, $size); // Path + extension + size + writable
$validator->validateUpload($path, $size); // Path + extension + size
// Query methods (non-throwing)
$isAllowed = $validator->isExtensionAllowed('pdf');
$allowedExts = $validator->getAllowedExtensions();
$blockedExts = $validator->getBlockedExtensions();
$maxSize = $validator->getMaxFileSize();
```
### Custom Validator Configuration
```php
use App\Framework\Core\ValueObjects\FileSize;
$validator = new FileValidator(
allowedExtensions: ['json', 'xml', 'yaml'],
blockedExtensions: null, // Don't use blacklist with whitelist
maxFileSize: FileSize::fromMegabytes(25),
baseDirectory: '/var/www/uploads' // Restrict to this directory
);
```
### Exception Handling
```php
use App\Framework\Filesystem\Exceptions\FileValidationException;
use App\Framework\Filesystem\Exceptions\FileNotFoundException;
use App\Framework\Filesystem\Exceptions\FilePermissionException;
try {
$validator->validateUpload('../../../etc/passwd', FileSize::fromKilobytes(1));
} catch (FileValidationException $e) {
// Path traversal detected
error_log($e->getMessage());
// "Path traversal attempt detected"
}
try {
$validator->validateUpload('malware.exe', FileSize::fromKilobytes(100));
} catch (FileValidationException $e) {
// Blocked extension
// "File extension '.exe' is blocked"
}
try {
$validator->validateRead('/nonexistent/file.txt');
} catch (FileNotFoundException $e) {
// File not found
}
try {
$validator->validateWrite('/readonly/path/file.txt');
} catch (FilePermissionException $e) {
// Permission denied
}
```
---
## SerializerRegistry
### Auto-Detection and Management
The `SerializerRegistry` automatically detects and manages file serializers based on extensions and MIME types.
### Default Registry
```php
use App\Framework\Filesystem\SerializerRegistry;
// Creates registry with common serializers pre-registered
$registry = SerializerRegistry::createDefault();
// Automatically includes:
// - JsonSerializer (.json, application/json)
// - XmlSerializer (.xml, application/xml, text/xml)
// - CsvSerializer (.csv, text/csv)
// - YamlSerializer (.yaml, .yml, application/yaml)
```
### Auto-Detection
```php
// Detect serializer from file path
$serializer = $registry->detectFromPath('/data/config.json');
// Returns: JsonSerializer
$serializer = $registry->detectFromPath('/exports/data.csv');
// Returns: CsvSerializer
// Get by extension
$serializer = $registry->getByExtension('xml');
// Get by MIME type
$serializer = $registry->getByMimeType('application/json');
```
### Custom Serializers
```php
// Implement Serializer interface
final readonly class TomlSerializer implements Serializer
{
public function serialize(mixed $data): string
{
return Toml::encode($data);
}
public function unserialize(string $data): mixed
{
return Toml::decode($data);
}
public function getSupportedExtensions(): array
{
return ['toml'];
}
public function getSupportedMimeTypes(): array
{
return ['application/toml'];
}
public function getName(): string
{
return 'toml';
}
}
// Register custom serializer
$registry->register(new TomlSerializer());
// Set as default serializer
$registry->setDefault(new TomlSerializer());
// Use auto-detection
$serializer = $registry->detectFromPath('config.toml');
```
### Registry Statistics
```php
$stats = $registry->getStatistics();
// Returns:
// [
// 'total_serializers' => 5,
// 'total_extensions' => 8,
// 'total_mime_types' => 6,
// 'has_default' => true,
// 'default_serializer' => 'json'
// ]
// Get all registered names
$names = $registry->getRegisteredNames();
// Returns: ['json', 'xml', 'csv', 'yaml', 'toml']
// List all serializers
$serializers = $registry->getAll();
```
---
## FileOperationContext
### Logging Context with Severity Levels
`FileOperationContext` provides rich metadata for logging filesystem operations with automatic severity classification.
### Severity Levels
**High Severity** (logged as WARNING):
- DELETE - File deletion
- DELETE_DIRECTORY - Directory deletion
- MOVE - File/directory move
**Medium Severity** (logged as INFO if large, DEBUG otherwise):
- WRITE - File write
- COPY - File copy
- CREATE_DIRECTORY - Directory creation
**Low Severity** (logged as DEBUG):
- READ - File read
- LIST_DIRECTORY - Directory listing
- GET_METADATA - Metadata retrieval
- All other read operations
### Factory Methods
```php
use App\Framework\Filesystem\ValueObjects\FileOperationContext;
use App\Framework\Filesystem\ValueObjects\FileOperation;
use App\Framework\Core\ValueObjects\FileSize;
// Simple operation
$context = FileOperationContext::forOperation(
FileOperation::DELETE,
'/path/to/file.txt'
);
// Operation with destination (copy, move)
$context = FileOperationContext::forOperationWithDestination(
FileOperation::COPY,
'/source/file.txt',
'/dest/file.txt'
);
// Write operation with size tracking
$context = FileOperationContext::forWrite(
'/path/to/file.txt',
FileSize::fromKilobytes(150),
userId: 'user123'
);
// Read operation with size tracking
$context = FileOperationContext::forRead(
'/path/to/file.txt',
FileSize::fromMegabytes(5)
);
```
### Context Enhancement
```php
// Add metadata
$context = $context->withMetadata([
'source' => 'upload',
'mime_type' => 'application/pdf',
'original_filename' => 'document.pdf'
]);
// Add user ID
$context = $context->withUserId('admin');
// Metadata merges with existing data
$context = $context->withMetadata(['key1' => 'value1'])
->withMetadata(['key2' => 'value2']);
// Results in: ['key1' => 'value1', 'key2' => 'value2']
```
### Context Queries
```php
// Check severity
if ($context->isHighSeverity()) {
// High severity operation (DELETE, MOVE, DELETE_DIRECTORY)
}
// Check operation type
if ($context->isWriteOperation()) {
// Write operation (WRITE, DELETE, MOVE, etc.)
}
// Check operation size
if ($context->isLargeOperation()) {
// Large operation (>10MB)
}
```
### Logging Integration
```php
// Convert to array for structured logging
$logData = $context->toArray();
// Returns:
// [
// 'operation' => 'write',
// 'operation_name' => 'file.write',
// 'path' => '/path/to/file.txt',
// 'timestamp' => '2025-01-15T10:30:00+00:00',
// 'severity' => 'medium',
// 'bytes_affected' => 153600,
// 'bytes_affected_human' => '150 KB',
// 'user_id' => 'user123',
// 'metadata' => ['source' => 'upload']
// ]
// Human-readable string
$message = $context->toString();
// Returns: "Write file contents, path: /path/to/file.txt, bytes: 150 KB, user: user123"
```
### Automatic Logging in FileStorage
```php
// FileStorage automatically logs operations based on severity:
private function logOperation(FileOperationContext $context): void
{
if ($this->logger === null) {
return;
}
$logContext = LogContext::fromArray($context->toArray());
if ($context->isHighSeverity()) {
// DELETE, MOVE, DELETE_DIRECTORY
$this->logger->framework->warning($context->toString(), $logContext);
} elseif ($context->isLargeOperation()) {
// Operations >10MB
$this->logger->framework->info($context->toString(), $logContext);
} else {
// Normal operations
$this->logger->framework->debug($context->toString(), $logContext);
}
}
```
---
## TemporaryDirectory
### Safe Temporary File Management
`TemporaryDirectory` provides automatic cleanup and safe handling of temporary files.
### Basic Usage
```php
use App\Framework\Filesystem\TemporaryDirectory;
// Create with auto-cleanup on destruct
$temp = TemporaryDirectory::create();
// Get path to temp directory
$path = $temp->path();
// Get FilePath for file in temp directory
$filePath = $temp->filePath('test.txt');
// Write files normally
file_put_contents($filePath->toString(), 'content');
// Auto-deletes on destruct
unset($temp); // Directory and all contents deleted
```
### Advanced Configuration
```php
// Custom name
$temp = TemporaryDirectory::create()
->name('my-temp-dir')
->force(); // Overwrite if exists
// Custom location
$temp = TemporaryDirectory::create()
->location('/custom/tmp')
->name('test-dir')
->force();
// Disable auto-delete
$temp = TemporaryDirectory::create()
->doNotDeleteAutomatically();
// Manual cleanup
$temp->empty(); // Empty directory contents
$temp->delete(); // Delete directory and contents
```
### Fluent Interface
```php
$temp = TemporaryDirectory::create()
->name('upload-processing')
->location('/var/tmp')
->force()
->create(); // Explicitly create
$processedFile = $temp->filePath('processed.txt');
```
---
## Best Practices
### 1. Always Use FileValidator for User Input
```php
// ❌ UNSAFE - no validation
$storage = new FileStorage('/uploads');
$storage->put($_FILES['file']['name'], file_get_contents($_FILES['file']['tmp_name']));
// ✅ SAFE - with validation
$validator = FileValidator::forUploads();
$storage = new FileStorage('/uploads', validator: $validator);
try {
$validator->validateUpload(
$_FILES['file']['name'],
FileSize::fromBytes($_FILES['file']['size'])
);
$storage->put($_FILES['file']['name'], file_get_contents($_FILES['file']['tmp_name']));
} catch (FileValidationException $e) {
// Handle validation error
}
```
### 2. Use Appropriate Validators
```php
// Image uploads
$validator = FileValidator::forImages(FileSize::fromMegabytes(5));
// Document uploads
$validator = FileValidator::forUploads(FileSize::fromMegabytes(20));
// Strict validation for config files
$validator = FileValidator::createStrict(['json', 'yaml', 'toml']);
```
### 3. Log High-Severity Operations
```php
// Always use logger for production systems
$storage = new FileStorage(
baseDirectory: '/var/www/storage',
validator: FileValidator::createDefault(),
logger: $logger // Critical for audit trail
);
// High-severity operations are automatically logged as WARNING
$storage->delete('/important/document.pdf');
```
### 4. Use TemporaryDirectory for Processing
```php
// Process uploads safely
$temp = TemporaryDirectory::create();
try {
// Extract archive to temp directory
$archive->extractTo($temp->path());
// Process files
foreach ($temp->files() as $file) {
$this->processFile($file);
}
// Move processed files to final location
$storage->copy($temp->filePath('result.txt'), '/final/result.txt');
} finally {
// Auto-cleanup on scope exit
unset($temp);
}
```
### 5. Combine SerializerRegistry with FileStorage
```php
$registry = SerializerRegistry::createDefault();
$storage = new FileStorage('/data', validator: FileValidator::createDefault());
// Auto-detect serializer and deserialize
$serializer = $registry->detectFromPath('config.json');
$data = $serializer->unserialize($storage->get('config.json'));
// Serialize and store
$content = $serializer->serialize(['key' => 'value']);
$storage->put('output.json', $content);
```
---
## Security Considerations
### Path Traversal Prevention
```php
// FileValidator blocks these automatically:
$validator->validatePath('../../../etc/passwd'); // ❌ BLOCKED
$validator->validatePath('..\\..\\windows\\system32'); // ❌ BLOCKED
$validator->validatePath('%2e%2e/etc/passwd'); // ❌ BLOCKED (URL-encoded)
$validator->validatePath("/path/with\0nullbyte"); // ❌ BLOCKED (null byte)
```
### Extension Filtering
```php
// Always use whitelist for user uploads
$validator = FileValidator::createStrict(['jpg', 'png', 'pdf']);
// Or use specialized validators
$validator = FileValidator::forImages(); // Images only
$validator = FileValidator::forUploads(); // Blocks dangerous extensions
// NEVER trust client-provided MIME types
// Use extension-based validation instead
```
### Base Directory Restriction
```php
// Restrict all operations to base directory
$validator = new FileValidator(
allowedExtensions: null,
blockedExtensions: ['exe', 'sh', 'bat'],
maxFileSize: FileSize::fromMegabytes(100),
baseDirectory: '/var/www/uploads' // Cannot escape this directory
);
// Attempts to escape are blocked
$validator->validatePath('/var/www/uploads/../../../etc/passwd'); // ❌ BLOCKED
```
---
## Performance Considerations
### Large File Operations
```php
// FileOperationContext detects large operations (>10MB)
if ($context->isLargeOperation()) {
// Triggers INFO-level logging instead of DEBUG
// Consider background processing for very large files
}
// Use streaming for large files
$storage->stream('large-file.mp4', function($stream) {
while (!feof($stream)) {
echo fread($stream, 8192);
}
});
```
### Caching Validator Results
```php
// Validator operations are fast, but for high-volume scenarios:
$isValid = $validator->isExtensionAllowed('pdf'); // Non-throwing check
if ($isValid) {
// Proceed with operation
} else {
// Reject early without exception overhead
}
```
---
## Phase 2 Performance Optimizations
The Filesystem module includes advanced performance optimizations achieved through caching strategies and syscall reduction.
**Framework Integration**: All performance optimizations are **automatically enabled by default** via `FilesystemInitializer` with sensible production settings. Disable only for debugging:
```env
# Filesystem Performance (caching enabled by default)
# Set to true only for debugging performance issues
# FILESYSTEM_DISABLE_CACHE=false
```
**Default Settings**:
- FileValidator caching: ENABLED (TTL: 60s, Max: 100 entries)
- FileStorage directory caching: ENABLED (session-based cache)
- clearstatcache() optimization: ENABLED (minimal syscalls)
### CachedFileValidator - Result Cache
**Performance Gain**: 99% faster for cached validations
**Description**: LRU cache decorator for FileValidator that caches validation results (both successes and failures) to avoid repeated expensive validation operations.
**Automatic Integration**: Enabled by default via `FilesystemInitializer` when resolving `FileValidator::class` from DI container. Disable only for debugging via `FILESYSTEM_DISABLE_CACHE=true` in `.env`.
**Features**:
- LRU eviction when cache size exceeds limit (default: 100 entries)
- Configurable TTL (default: 60 seconds)
- Caches path and extension validation (not file size/existence checks)
- Automatic cache invalidation on TTL expiry
- Cache statistics for monitoring
**Usage**:
```php
use App\Framework\Filesystem\CachedFileValidator;
use App\Framework\Filesystem\FileValidator;
$validator = FileValidator::createDefault();
$cachedValidator = new CachedFileValidator(
validator: $validator,
cacheTtl: 60, // 60 seconds TTL
maxCacheSize: 100 // Max 100 cached results
);
// First call - cache miss (validates fully)
$cachedValidator->validatePath('/path/to/file.txt');
// Second call - cache hit (99% faster)
$cachedValidator->validatePath('/path/to/file.txt');
// Get cache statistics
$stats = $cachedValidator->getCacheStats();
// ['path_cache_size' => 1, 'extension_cache_size' => 0, ...]
```
**What's Cached**:
- ✅ Path validation (traversal checks, null bytes)
- ✅ Extension validation (allowed/blocked lists)
- ✅ Composite validations (validateRead, validateWrite, validateUpload)
**What's NOT Cached**:
- ❌ File size validation (size can change)
- ❌ File existence checks (files can be created/deleted)
- ❌ Permission checks (permissions can change)
**Performance Characteristics**:
- Cache hit latency: <0.1ms
- Cache miss latency: ~1ms (original validation)
- Memory usage: ~100KB for 100 entries
### CachedFileStorage - Directory Cache
**Performance Gain**: 25% fewer syscalls for write operations
**Description**: Decorator for FileStorage that caches directory existence checks to reduce redundant `is_dir()` syscalls during write operations.
**Automatic Integration**: Enabled by default via `FilesystemInitializer` when resolving `Storage::class`, `FileStorage::class`, or any named storage (`filesystem.storage.*`) from DI container. Disable only for debugging via `FILESYSTEM_DISABLE_CACHE=true` in `.env`.
**Features**:
- Session-based cache (cleared on object destruction)
- Write-through caching (directories cached when created or verified)
- Conservative strategy (only caches successful operations)
- Parent directory recursive caching
- O(1) cache lookup performance
**Usage**:
```php
use App\Framework\Filesystem\CachedFileStorage;
use App\Framework\Filesystem\FileStorage;
$storage = new FileStorage('/var/www/storage');
$cachedStorage = new CachedFileStorage(
storage: $storage,
basePath: '/var/www/storage'
);
// First write - cache miss (checks directory exists)
$cachedStorage->put('nested/deep/file1.txt', 'content1');
// Second write to same directory - cache hit (skips is_dir check)
$cachedStorage->put('nested/deep/file2.txt', 'content2'); // 25% faster
// Get cache statistics
$stats = $cachedStorage->getCacheStats();
// ['cached_directories' => 2, 'cache_entries' => [...]]
```
**Cache Strategy**:
- Directories cached on first verification or creation
- Parent directories automatically cached (if `/a/b/c` exists, `/a/b` and `/a` are cached)
- Normalized paths (resolved symlinks, no trailing slashes)
- Hash-based cache keys for O(1) lookup
**Performance Characteristics**:
- Cache hit latency: <0.01ms (array lookup)
- Cache miss latency: ~1ms (is_dir syscall)
- Memory usage: ~50 bytes per cached directory
- Syscall reduction: 25% for repeated writes to same directories
### clearstatcache() Optimization
**Performance Gain**: ~1-2ms faster read operations
**Description**: Strategic placement of `clearstatcache()` calls - only before write operations where fresh stat info is critical, removed from read operations where stat cache is valid.
**Optimization Details**:
**Before Optimization**:
```php
// FileStorage::get() - UNNECESSARY
public function get(string $path): string
{
clearstatcache(true, $resolvedPath); // ❌ Unnecessary for reads
if (!is_file($resolvedPath)) {
throw new FileNotFoundException($path);
}
// Also in error handling
clearstatcache(true, $resolvedPath); // ❌ Unnecessary
if (!is_file($resolvedPath)) {
throw new FileNotFoundException($path);
}
return file_get_contents($resolvedPath);
}
```
**After Optimization**:
```php
// FileStorage::get() - Removed unnecessary clearstatcache
public function get(string $path): string
{
// ✅ No clearstatcache - stat cache is valid for reads
if (!is_file($resolvedPath)) {
throw new FileNotFoundException($path);
}
return file_get_contents($resolvedPath);
}
// FileStorage::put() - Added necessary clearstatcache
public function put(string $path, string $content): void
{
$dir = dirname($resolvedPath);
// ✅ Clear stat cache before directory check
clearstatcache(true, $dir);
if (!is_dir($dir)) {
mkdir($dir, 0777, true);
}
file_put_contents($resolvedPath, $content);
}
```
**Performance Impact**:
- Read operations: ~1-2ms faster (removed 2x clearstatcache calls)
- Write operations: No performance impact (necessary clearstatcache added)
- Stat cache correctness: Maintained for write operations, valid for read operations
---
## Testing
### Unit Testing with Validators
```php
it('validates file uploads correctly', function () {
$validator = FileValidator::forUploads();
// Valid upload
$validator->validateUpload(
'/uploads/document.pdf',
FileSize::fromMegabytes(2)
);
expect(true)->toBeTrue(); // No exception
// Invalid - path traversal
try {
$validator->validateUpload(
'../../../etc/passwd',
FileSize::fromKilobytes(1)
);
expect(true)->toBeFalse('Should throw');
} catch (FileValidationException $e) {
expect($e->getMessage())->toContain('Path traversal');
}
});
```
### Integration Testing with FileStorage
```php
it('integrates validator with storage', function () {
$testDir = sys_get_temp_dir() . '/test_' . uniqid();
mkdir($testDir);
$validator = FileValidator::createStrict(['txt']);
$storage = new FileStorage($testDir, validator: $validator);
// Valid operation
$storage->put('allowed.txt', 'content');
expect($storage->exists('allowed.txt'))->toBeTrue();
// Invalid operation
try {
$storage->put('blocked.exe', 'malicious');
expect(true)->toBeFalse('Should throw');
} catch (FileValidationException $e) {
expect($e->getMessage())->toContain('not allowed');
}
// Cleanup
array_map('unlink', glob($testDir . '/*'));
rmdir($testDir);
});
```
---
## Summary
The Filesystem module provides:
**Type-Safe Operations** - Value objects throughout (FilePath, FileSize, FileOperation)
**Security-First** - Path traversal prevention, extension filtering, size limits
**Rich Logging** - Automatic severity classification, detailed context
**Auto-Detection** - Serializer registry with extension/MIME type mapping
**Immutable Design** - Readonly classes, transformation methods
**Framework Compliance** - Follows all framework architectural principles
**Key Integration Points**:
- Works with framework's Logger for audit trails
- Uses Value Objects (FileSize, Timestamp, FilePath)
- Event system integration available
- Queue system integration for async operations
- Cache integration for serializer registry
**Performance Optimizations**:
- **CachedFileValidator**: 99% faster validation for repeated paths (LRU cache)
- **CachedFileStorage**: 25% fewer syscalls for write operations (directory cache)
- **clearstatcache() Optimization**: 1-2ms faster read operations (strategic placement)
- Memory-efficient caching strategies
- O(1) cache lookup performance
**Production Ready**:
- Comprehensive test coverage (145 tests, 319 assertions)
- Security-focused validation
- Performance-optimized design with caching
- Detailed error messages
- Production logging with severity levels

View File

@@ -0,0 +1,458 @@
# Scheduler-Queue Pipeline
Komplette Dokumentation der Scheduler-Queue Integration Pipeline im Custom PHP Framework.
## Übersicht
Die Scheduler-Queue Pipeline ist eine vollständig integrierte Lösung für zeitbasierte Aufgabenplanung und asynchrone Job-Ausführung. Sie kombiniert das Framework's Scheduler System mit dem erweiterten Queue System für robuste, skalierbare Background-Processing.
## Architektur
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Scheduler │───▶│ Job Dispatch │───▶│ Queue System │───▶│ Background Exec │
│ System │ │ & Validation │ │ (13 Tables) │ │ & Logging │
└─────────────────┘ └─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │ │
Cron/Interval JobPayload FileQueue/Redis Named Job Classes
OneTime/Manual Value Objects w/ Metrics w/ handle() Method
Schedule Types Type Safety & Monitoring Result Logging
```
## Pipeline Komponenten
### 1. Scheduler System
**Location**: `src/Framework/Scheduler/`
**Schedule Types**:
- **CronSchedule**: Traditionelle Cron-basierte Zeitplanung
- **IntervalSchedule**: Wiederholende Ausführung in festen Intervallen
- **OneTimeSchedule**: Einmalige Ausführung zu bestimmter Zeit
- **ManualSchedule**: Manuelle Trigger-basierte Ausführung
**Core Services**:
- `SchedulerService`: Hauptservice für Task-Management
- `TaskExecutionResult`: Value Object für Execution-Ergebnisse
- `Timestamp`/`Duration`: Zeit-Value Objects mit Framework-Compliance
### 2. Job Dispatch & Validation
**Integration Layer zwischen Scheduler und Queue**
**Key Features**:
- **Type Safety**: Verwendung von JobPayload Value Objects
- **Named Job Classes**: Vermeidung von Anonymous Classes (ClassName Restrictions)
- **Validation**: Input-Validierung vor Queue-Dispatch
- **Error Handling**: Graceful Fallbacks bei Dispatch-Fehlern
### 3. Queue System
**Location**: `src/Framework/Queue/`
**Database Schema** (13 Specialized Tables):
```sql
jobs -- Haupt-Job-Queue
job_batches -- Batch-Job-Verwaltung
job_history -- Execution-Historie
job_metrics -- Performance-Metriken
dead_letter_jobs -- Fehlgeschlagene Jobs
job_priorities -- Prioritäts-basierte Scheduling
job_dependencies -- Inter-Job Dependencies
delayed_jobs -- Zeit-verzögerte Ausführung
worker_processes -- Worker-Management
job_locks -- Distributed Locking
job_progress -- Progress Tracking
recurring_jobs -- Wiederkehrende Patterns
job_tags -- Kategorisierung & Filtering
```
**Core Features**:
- **Multi-Driver Support**: FileQueue, Redis, Database
- **Priority Scheduling**: High/Medium/Low Priority Queues
- **Metrics & Monitoring**: Real-time Performance Tracking
- **Dead Letter Queue**: Automatic Failed Job Management
- **Distributed Locking**: Multi-Worker Coordination
### 4. Background Execution & Logging
**Job Processing mit umfassender Protokollierung**
**Execution Pattern**:
```php
final class ScheduledBackgroundJob
{
public function handle(): array
{
// Business Logic Execution
$result = $this->performWork();
// Automatic Logging
$this->logExecution($result);
return $result;
}
}
```
## Verwendung
### Basic Scheduler-Queue Integration
```php
use App\Framework\Scheduler\Services\SchedulerService;
use App\Framework\Scheduler\Schedules\IntervalSchedule;
use App\Framework\Queue\Queue;
use App\Framework\Queue\ValueObjects\JobPayload;
use App\Framework\Core\ValueObjects\Duration;
// 1. Schedule Setup
$scheduler = $container->get(SchedulerService::class);
$queue = $container->get(Queue::class);
// 2. Create Interval Schedule (every 10 minutes)
$schedule = IntervalSchedule::every(Duration::fromMinutes(10));
// 3. Register Task with Queue Dispatch
$scheduler->schedule('email-cleanup', $schedule, function() use ($queue) {
$job = new EmailCleanupJob(
olderThan: Duration::fromDays(30),
batchSize: 100
);
$payload = JobPayload::immediate($job);
$queue->push($payload);
return ['status' => 'queued', 'timestamp' => time()];
});
```
### Advanced Pipeline Configuration
```php
// Complex Multi-Stage Pipeline
final class ReportGenerationPipeline
{
public function __construct(
private readonly SchedulerService $scheduler,
private readonly Queue $queue
) {}
public function setupDailyReports(): void
{
// Stage 1: Data Collection (Daily at 2 AM)
$this->scheduler->schedule(
'daily-data-collection',
CronSchedule::fromExpression('0 2 * * *'),
fn() => $this->dispatchDataCollection()
);
// Stage 2: Report Generation (After data collection)
$this->scheduler->schedule(
'daily-report-generation',
CronSchedule::fromExpression('30 2 * * *'),
fn() => $this->dispatchReportGeneration()
);
// Stage 3: Distribution (After generation)
$this->scheduler->schedule(
'daily-report-distribution',
CronSchedule::fromExpression('0 3 * * *'),
fn() => $this->dispatchReportDistribution()
);
}
private function dispatchDataCollection(): array
{
$job = new DataCollectionJob(
sources: ['database', 'analytics', 'external_apis'],
target_date: Timestamp::yesterday()
);
$payload = JobPayload::withPriority($job, Priority::HIGH);
$this->queue->push($payload);
return ['stage' => 'data_collection', 'queued_at' => time()];
}
}
```
### Named Job Classes (Best Practice)
```php
// ✅ Framework-Compliant Job Class
final class EmailCleanupJob
{
public function __construct(
private readonly Duration $olderThan,
private readonly int $batchSize = 100
) {}
public function handle(): array
{
$deleted = $this->emailService->deleteOldEmails(
$this->olderThan,
$this->batchSize
);
$this->logCleanupResults($deleted);
return [
'deleted_count' => count($deleted),
'batch_size' => $this->batchSize,
'cleanup_threshold' => $this->olderThan->toDays() . ' days'
];
}
public function getType(): string
{
return 'email-cleanup';
}
}
```
## Pipeline Monitoring
### Health Checks
```php
// Pipeline Health Verification
final class PipelineHealthChecker
{
public function checkPipelineHealth(): PipelineHealthReport
{
return new PipelineHealthReport([
'scheduler_status' => $this->checkSchedulerHealth(),
'queue_status' => $this->checkQueueHealth(),
'integration_status' => $this->checkIntegrationHealth(),
'performance_metrics' => $this->gatherPerformanceMetrics()
]);
}
private function checkSchedulerHealth(): array
{
$dueTasks = $this->scheduler->getDueTasks();
$nextExecution = $this->scheduler->getNextExecutionTime();
return [
'due_tasks_count' => count($dueTasks),
'next_execution' => $nextExecution?->format('Y-m-d H:i:s'),
'status' => count($dueTasks) < 100 ? 'healthy' : 'overloaded'
];
}
private function checkQueueHealth(): array
{
$stats = $this->queue->getStats();
return [
'queue_size' => $stats['total_size'],
'priority_distribution' => $stats['priority_breakdown'],
'processing_rate' => $this->calculateProcessingRate(),
'status' => $stats['total_size'] < 1000 ? 'healthy' : 'backlog'
];
}
}
```
### Performance Metrics
```php
// Real-time Pipeline Performance
final class PipelineMetricsCollector
{
public function collectMetrics(string $timeframe = '1hour'): array
{
return [
'scheduler_metrics' => [
'tasks_executed' => $this->getExecutedTasksCount($timeframe),
'average_execution_time' => $this->getAverageExecutionTime($timeframe),
'success_rate' => $this->getSchedulerSuccessRate($timeframe)
],
'queue_metrics' => [
'jobs_processed' => $this->getProcessedJobsCount($timeframe),
'average_wait_time' => $this->getAverageWaitTime($timeframe),
'throughput' => $this->calculateThroughput($timeframe)
],
'integration_metrics' => [
'dispatch_success_rate' => $this->getDispatchSuccessRate($timeframe),
'end_to_end_latency' => $this->getEndToEndLatency($timeframe),
'error_rate' => $this->getIntegrationErrorRate($timeframe)
]
];
}
}
```
## Troubleshooting
### Häufige Probleme
**1. Jobs werden nicht ausgeführt**
```bash
# Diagnose Queue Status
docker exec php php console.php queue:status
# Check Scheduler Tasks
docker exec php php console.php scheduler:status
# Verify Integration
docker exec php php tests/debug/test-scheduler-queue-integration-fixed.php
```
**2. Memory Leaks bei großen Jobs**
```php
// Memory-effiziente Job Implementation
final class LargeDataProcessingJob
{
public function handle(): array
{
// Batch Processing to prevent memory exhaustion
$batch = $this->dataSource->getBatch($this->batchSize);
while (!empty($batch)) {
$this->processBatch($batch);
// Force garbage collection
gc_collect_cycles();
$batch = $this->dataSource->getNextBatch($this->batchSize);
}
return ['processed' => $this->totalProcessed];
}
}
```
**3. Queue Backlog Management**
```php
// Automatic Backlog Resolution
final class BacklogManager
{
public function resolveBacklog(): void
{
$stats = $this->queue->getStats();
if ($stats['total_size'] > $this->backlogThreshold) {
// Scale up workers
$this->workerManager->scaleUp($this->calculateRequiredWorkers($stats));
// Prioritize critical jobs
$this->queue->reprioritize(['high_priority_types']);
// Alert operations team
$this->alertManager->sendBacklogAlert($stats);
}
}
}
```
## Testing
### Integration Tests
```php
// Complete Pipeline Testing
describe('Scheduler Queue Pipeline', function () {
it('handles full pipeline flow', function () {
// 1. Setup Scheduler Task
$schedule = IntervalSchedule::every(Duration::fromSeconds(1));
$this->scheduler->schedule('test-task', $schedule, function() {
$job = new TestPipelineJob('pipeline-test');
$payload = JobPayload::immediate($job);
$this->queue->push($payload);
return ['dispatched' => true];
});
// 2. Execute Scheduler
$results = $this->scheduler->executeDueTasks();
expect($results)->toHaveCount(1);
expect($results[0]->success)->toBeTrue();
// 3. Process Queue
$jobPayload = $this->queue->pop();
expect($jobPayload)->not->toBeNull();
$result = $jobPayload->job->handle();
expect($result['status'])->toBe('completed');
// 4. Verify End-to-End
expect($this->queue->size())->toBe(0);
});
});
```
### Performance Tests
```php
// Pipeline Performance Benchmarks
describe('Pipeline Performance', function () {
it('processes 1000 jobs within 30 seconds', function () {
$startTime = microtime(true);
// Dispatch 1000 jobs via scheduler
for ($i = 0; $i < 1000; $i++) {
$job = new BenchmarkJob("job-{$i}");
$payload = JobPayload::immediate($job);
$this->queue->push($payload);
}
// Process all jobs
while ($this->queue->size() > 0) {
$jobPayload = $this->queue->pop();
$jobPayload->job->handle();
}
$executionTime = microtime(true) - $startTime;
expect($executionTime)->toBeLessThan(30.0);
expect($this->queue->size())->toBe(0);
});
});
```
## Best Practices
### 1. Job Design
- **Named Classes**: Immer Named Classes statt Anonymous Functions verwenden
- **Type Safety**: JobPayload Value Objects für alle Queue-Operationen
- **Idempotency**: Jobs sollten mehrfach ausführbar sein ohne Seiteneffekte
- **Error Handling**: Graceful Degradation bei Fehlern implementieren
### 2. Scheduler Configuration
- **Reasonable Intervals**: Nicht zu aggressive Scheduling-Intervalle
- **Resource Awareness**: CPU/Memory-Limits bei Task-Design beachten
- **Monitoring**: Kontinuierliche Überwachung der Execution-Times
- **Failover**: Backup-Strategien für kritische Tasks
### 3. Queue Management
- **Priority Classes**: Sinnvolle Priorisierung für verschiedene Job-Types
- **Batch Processing**: Große Datenmengen in Batches aufteilen
- **Dead Letter Handling**: Automatische Failed-Job-Recovery
- **Metrics Collection**: Performance-Daten für Optimierung sammeln
### 4. Production Deployment
- **Health Checks**: Regelmäßige Pipeline-Health-Verification
- **Alerting**: Automatische Benachrichtigungen bei Problemen
- **Scaling**: Auto-Scaling für Worker-Processes
- **Backup**: Disaster-Recovery-Strategien für Queue-Daten
## Framework Integration
Die Pipeline nutzt konsequent Framework-Patterns:
- **Value Objects**: Timestamp, Duration, JobPayload, Priority
- **Readonly Classes**: Unveränderliche Job-Definitionen
- **Event System**: Integration mit Framework's Event Dispatcher
- **DI Container**: Automatic Service Resolution
- **Attribute Discovery**: Convention-over-Configuration
- **MCP Integration**: AI-gestützte Pipeline-Analyse
## Performance Charakteristiken
**Typische Performance-Werte**:
- **Job Dispatch Latency**: < 50ms
- **Queue Throughput**: 1000+ Jobs/Minute (FileQueue)
- **Memory Usage**: < 50MB für Standard-Jobs
- **Scheduler Precision**: ±1 Second für Cron-basierte Tasks
- **End-to-End Latency**: < 500ms für einfache Jobs
**Skalierungscharakteristiken**:
- **Horizontal Scaling**: Multi-Worker Support
- **Queue Capacity**: 100,000+ Jobs (Database-backed)
- **Scheduler Load**: 10,000+ concurrent scheduled tasks
- **Memory Efficiency**: Linear scaling mit Job-Complexity

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,306 @@
# Routing Value Objects
Das Framework unterstützt **parallele Routing-Ansätze** für maximale Flexibilität und Typsicherheit.
## Überblick
```php
// ✅ Traditioneller String-Ansatz (schnell & gewohnt)
#[Route(path: '/api/users/{id}')]
// ✅ Value Object-Ansatz (typsicher & framework-konform)
#[Route(path: RoutePath::fromElements('api', 'users', Placeholder::fromString('id')))]
```
Beide Ansätze sind **vollständig kompatibel** und können parallel verwendet werden.
## String-Basierte Routen
**Einfach und gewohnt** für schnelle Entwicklung:
```php
final readonly class UserController
{
#[Route(path: '/api/users')]
public function index(): JsonResult { /* ... */ }
#[Route(path: '/api/users/{id}')]
public function show(int $id): JsonResult { /* ... */ }
#[Route(path: '/api/users/{id}/posts/{postId}')]
public function showPost(int $id, int $postId): JsonResult { /* ... */ }
#[Route(path: '/files/{path*}', method: Method::GET)]
public function downloadFile(string $path): StreamResult { /* ... */ }
}
```
## Value Object-Basierte Routen
**Typsicher und framework-konform** für Production-Code:
### Basis-Syntax
```php
use App\Framework\Router\ValueObjects\RoutePath;
use App\Framework\Router\ValueObjects\Placeholder;
final readonly class ImageController
{
#[Route(path: RoutePath::fromElements('images', Placeholder::fromString('filename')))]
public function show(string $filename): ImageResult
{
return new ImageResult($this->imageService->getImage($filename));
}
}
```
### Typisierte Parameter
```php
#[Route(path: RoutePath::fromElements(
'api',
'users',
Placeholder::typed('userId', 'uuid'),
'posts',
Placeholder::typed('postId', 'int')
))]
public function getUserPost(string $userId, int $postId): JsonResult
{
// userId wird automatisch als UUID validiert
// postId wird automatisch als Integer validiert
}
```
### Verfügbare Parameter-Typen
| Typ | Regex Pattern | Beispiel |
|-----|---------------|----------|
| `int` | `(\d+)` | `/users/{id}` → 123 |
| `uuid` | `([0-9a-f]{8}-...)` | `/users/{id}``550e8400-e29b-...` |
| `slug` | `([a-z0-9\-]+)` | `/posts/{slug}``my-blog-post` |
| `alpha` | `([a-zA-Z]+)` | `/category/{name}``Technology` |
| `alphanumeric` | `([a-zA-Z0-9]+)` | `/code/{id}``ABC123` |
| `filename` | `([a-zA-Z0-9._\-]+)` | `/files/{name}``image.jpg` |
### Wildcard-Parameter
```php
#[Route(path: RoutePath::fromElements('files', Placeholder::wildcard('path')))]
public function serveFile(string $path): StreamResult
{
// Matched: /files/uploads/2024/image.jpg
// $path = "uploads/2024/image.jpg"
}
```
## Fluent Builder API
**Expressiver Builder** für komplexe Routen:
```php
final readonly class ApiController
{
#[Route(path: RoutePath::create()
->segment('api')
->segment('v1')
->segment('users')
->typedParameter('userId', 'uuid')
->segment('posts')
->typedParameter('postId', 'int')
->build()
)]
public function getUserPost(string $userId, int $postId): JsonResult { /* ... */ }
// Quick Helper für häufige Patterns
#[Route(path: RoutePath::create()->segments('api', 'users')->uuid())]
public function showUser(string $id): JsonResult { /* ... */ }
}
```
## Praktische Beispiele
### RESTful API Routes
```php
final readonly class ProductController
{
// String-Ansatz für einfache Routen
#[Route(path: '/api/products', method: Method::GET)]
public function index(): JsonResult { }
#[Route(path: '/api/products', method: Method::POST)]
public function create(CreateProductRequest $request): JsonResult { }
// Value Object-Ansatz für komplexe Routen
#[Route(path: RoutePath::fromElements(
'api',
'products',
Placeholder::typed('productId', 'uuid'),
'reviews',
Placeholder::typed('reviewId', 'int')
), method: Method::GET)]
public function getProductReview(string $productId, int $reviewId): JsonResult { }
}
```
### File Serving
```php
final readonly class FileController
{
// Static files (string)
#[Route(path: '/assets/{type}/{filename}')]
public function staticAsset(string $type, string $filename): StreamResult { }
// Dynamic file paths (Value Object mit Wildcard)
#[Route(path: RoutePath::fromElements(
'uploads',
Placeholder::wildcard('path')
))]
public function uploadedFile(string $path): StreamResult { }
}
```
### Admin Routes
```php
final readonly class AdminController
{
// String für bekannte Admin-Pfade
#[Route(path: '/admin/dashboard')]
#[Auth(strategy: 'ip', allowedIps: ['127.0.0.1'])]
public function dashboard(): ViewResult { }
// Value Objects für dynamische Admin-Actions
#[Route(path: RoutePath::fromElements(
'admin',
'users',
Placeholder::typed('userId', 'uuid'),
'actions',
Placeholder::fromString('action')
))]
#[Auth(strategy: 'session', roles: ['admin'])]
public function userAction(string $userId, string $action): JsonResult { }
}
```
## Migration & Kompatibilität
### Bestehende Routen bleiben unverändert
```php
// ✅ Weiterhin gültig und funktional
#[Route(path: '/api/users/{id}')]
public function show(int $id): JsonResult { }
```
### Schrittweise Migration
```php
final readonly class UserController
{
// Phase 1: Strings für einfache Routen
#[Route(path: '/users')]
public function index(): ViewResult { }
// Phase 2: Value Objects für neue komplexe Routen
#[Route(path: RoutePath::fromElements(
'users',
Placeholder::typed('userId', 'uuid'),
'preferences',
Placeholder::fromString('section')
))]
public function userPreferences(string $userId, string $section): JsonResult { }
}
```
### Konsistenz-Check
```php
// Beide Ansätze sind äquivalent
$stringRoute = new Route(path: '/api/users/{id}');
$objectRoute = new Route(path: RoutePath::fromElements('api', 'users', Placeholder::fromString('id')));
$stringRoute->getPathAsString() === $objectRoute->getPathAsString(); // true
```
## Best Practices
### Wann String verwenden
- **Prototyping**: Schnelle Route-Erstellung
- **Einfache Routen**: Statische oder 1-Parameter-Routen
- **Legacy-Kompatibilität**: Bestehende Routen beibehalten
```php
// ✅ Gut für einfache Fälle
#[Route(path: '/health')]
#[Route(path: '/api/status')]
#[Route(path: '/users/{id}')]
```
### Wann Value Objects verwenden
- **Production-Code**: Maximale Typsicherheit
- **Komplexe Routen**: Mehrere Parameter mit Validierung
- **API-Endpoints**: Starke Typisierung für externe Schnittstellen
- **Framework-Konsistenz**: Vollständige Value Object-Nutzung
```php
// ✅ Gut für komplexe Fälle
#[Route(path: RoutePath::fromElements(
'api',
'v2',
'organizations',
Placeholder::typed('orgId', 'uuid'),
'projects',
Placeholder::typed('projectId', 'slug'),
'files',
Placeholder::wildcard('filePath')
))]
```
### Hybrid-Ansatz (Empfohlen)
```php
final readonly class ProjectController
{
// String für einfache Routen
#[Route(path: '/projects')]
public function index(): ViewResult { }
// Value Objects für komplexe/kritische Routen
#[Route(path: RoutePath::fromElements(
'api',
'projects',
Placeholder::typed('projectId', 'uuid'),
'members',
Placeholder::typed('memberId', 'uuid')
))]
public function getProjectMember(string $projectId, string $memberId): JsonResult { }
}
```
## Router-Integration
Das Framework konvertiert automatisch zwischen beiden Ansätzen:
```php
// Route-Attribute bietet einheitliche Interface
public function getPathAsString(): string // Immer String für Router
public function getRoutePath(): RoutePath // Immer RoutePath-Objekt
// RouteCompiler verwendet automatisch getPathAsString()
$path = $routeAttribute->getPathAsString(); // Funktioniert für beide
```
## Fazit
- **String-Routen**: Schnell, gewohnt, ideal für einfache Fälle
- **Value Object-Routen**: Typsicher, framework-konform, ideal für komplexe Fälle
- **Vollständige Kompatibilität**: Beide Ansätze parallel nutzbar
- **Keine Breaking Changes**: Bestehender Code funktioniert weiterhin
- **Schrittweise Adoption**: Migration nach Bedarf möglich
**Empfehlung**: Hybrid-Ansatz mit Strings für einfache und Value Objects für komplexe Routen.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,291 @@
# Route Authorization System
Dokumentation des namespace-basierten Route Authorization Systems.
## Übersicht
Das Route Authorization System ermöglicht die Zugangskontrolle für Routes auf Basis von:
1. **Legacy `#[Auth]` Attribute** - Backward compatibility
2. **Namespace-basierte Blockierung** - Blockiere ganze Controller-Namespaces (z.B. `App\Application\Admin\*`)
3. **Namespace-basierte IP-Restrictions** - IP-basierte Zugriffskontrolle per Namespace
4. **Route-spezifische `#[IpAuth]` Attribute** - Feinkörnige IP-Kontrolle per Route
## Architektur
```
RouteAuthorizationService
├── checkLegacyAuthAttribute()
├── checkNamespaceAccessPolicy() [NEU]
├── checkNamespaceIpRestrictions()
└── checkRouteIpAuthAttribute()
```
Der Service wird in der `RoutingMiddleware` **nach dem Routing** aufgerufen, sodass die gematchte Route bekannt ist.
## Konfiguration
### Initializer-basierte Konfiguration
**Location**: `src/Framework/Auth/RouteAuthorizationServiceInitializer.php`
```php
#[Initializer]
public function __invoke(Container $container): RouteAuthorizationService
{
$namespaceConfig = [
// Namespace Pattern => Configuration
'App\Application\Admin\*' => [
'visibility' => 'admin', // IP-based restriction
'access_policy' => NamespaceAccessPolicy::blocked()
],
];
return new RouteAuthorizationService(
config: $this->config,
namespaceConfig: $namespaceConfig
);
}
```
### Namespace Patterns
**Wildcard-basierte Patterns**:
- `App\Application\Admin\*` - Matched alle Admin-Controller
- `App\Application\Api\*` - Matched alle API-Controller
- `App\Application\*` - Matched alle Application-Controller
**Exact Match**:
- `App\Application\Admin\Dashboard` - Nur exakt dieser Namespace
## Use Cases
### 1. Admin-Bereich komplett blockieren
```php
$namespaceConfig = [
'App\Application\Admin\*' => [
'access_policy' => NamespaceAccessPolicy::blocked()
],
];
```
**Ergebnis**: Alle Admin-Controller werfen `RouteNotFound` (404)
### 2. Admin-Bereich mit Allowlist
```php
use App\Application\Admin\LoginController;
use App\Application\Admin\HealthCheckController;
$namespaceConfig = [
'App\Application\Admin\*' => [
'access_policy' => NamespaceAccessPolicy::blockedExcept(
LoginController::class,
HealthCheckController::class
)
],
];
```
**Ergebnis**:
-`LoginController` und `HealthCheckController` öffentlich erreichbar
- ❌ Alle anderen Admin-Controller blockiert
### 3. Kombination: IP-Restriction + Namespace-Blocking
```php
$namespaceConfig = [
'App\Application\Admin\*' => [
'visibility' => 'admin', // Nur Admin-IPs erlaubt
'access_policy' => NamespaceAccessPolicy::blockedExcept(
LoginController::class // Aber Login ist public
)
],
];
```
**Ergebnis**:
-`LoginController` - öffentlich erreichbar (trotz admin visibility)
- 🔒 Alle anderen Admin-Controller - nur von Admin-IPs
### 4. API-Bereich mit IP-Restriction (ohne Blocking)
```php
$namespaceConfig = [
'App\Application\Api\*' => [
'visibility' => 'local', // Nur localhost/private IPs
// Kein access_policy - keine Namespace-Blockierung
],
];
```
**Ergebnis**: API nur von localhost/private IPs erreichbar
### 5. Mehrere Namespace-Policies
```php
$namespaceConfig = [
// Admin komplett gesperrt
'App\Application\Admin\*' => [
'access_policy' => NamespaceAccessPolicy::blocked()
],
// API nur von localhost
'App\Application\Api\*' => [
'visibility' => 'local'
],
// Internal Tools nur admin IPs
'App\Application\Internal\*' => [
'visibility' => 'admin',
'access_policy' => NamespaceAccessPolicy::blocked()
],
];
```
## Value Objects
### NamespaceAccessPolicy
```php
// Alle Controller im Namespace blockieren
NamespaceAccessPolicy::blocked()
// Alle blockieren außer spezifische Controller
NamespaceAccessPolicy::blockedExcept(
LoginController::class,
HealthCheckController::class
)
// Alle erlauben (default)
NamespaceAccessPolicy::allowed()
// Prüfen ob Controller blockiert ist
$policy->isControllerBlocked(Dashboard::class) // true/false
```
## Visibility Modes (IP-Restrictions)
**Predefined Modes**:
- `public` - Keine IP-Restrictions
- `admin` - Nur Admin-IPs (WireGuard, etc.)
- `local` - Nur localhost/127.0.0.1
- `development` - Development-IPs
- `private` - Alias für `local`
- `custom` - Custom IP-Liste via Config
## Execution Order
1. **Legacy Auth Attribute** - Backward compatibility check
2. **Namespace Access Policy** - Block/Allow basierend auf Controller-Klasse
3. **Namespace IP Restrictions** - IP-basierte Zugriffskontrolle
4. **Route IP Auth Attribute** - Feinkörnige Route-Level IP-Kontrolle
Alle Checks werfen `RouteNotFound` bei Failure (versteckt Route-Existenz).
## Testing
### Unit Test Beispiel
```php
use App\Framework\Auth\RouteAuthorizationService;
use App\Framework\Auth\ValueObjects\NamespaceAccessPolicy;
it('blocks admin controllers except allowlist', function () {
$config = ['App\Application\Admin\*' => [
'access_policy' => NamespaceAccessPolicy::blockedExcept(
LoginController::class
)
]];
$service = new RouteAuthorizationService(
config: $this->config,
namespaceConfig: $config
);
// Should throw RouteNotFound for Dashboard
expect(fn() => $service->authorize($request, $dashboardRoute))
->toThrow(RouteNotFound::class);
// Should allow LoginController
expect(fn() => $service->authorize($request, $loginRoute))
->not->toThrow(RouteNotFound::class);
});
```
## Best Practices
### 1. Namespace-Blocking für Production
- Blockiere Admin/Internal-Bereiche in Production
- Nutze Allowlist nur für wirklich öffentliche Endpoints (Login, Health)
### 2. IP-Restrictions für Sensitive Bereiche
- Kombiniere Namespace-Blocking mit IP-Restrictions
- Nutze `visibility: 'admin'` für maximale Sicherheit
### 3. Graceful Error Handling
- Alle Checks werfen `RouteNotFound` (404)
- Versteckt Route-Existenz vor Angreifern
- Keine Information Leakage
### 4. Konfiguration via Initializer
- Zentrale Konfiguration in `RouteAuthorizationServiceInitializer`
- Environment-spezifische Configs möglich (dev vs. production)
- Type-safe via Value Objects
## Migration Guide
### Von altem System (RoutingMiddleware::withNamespaceConfig)
**Alt**:
```php
RoutingMiddleware::withNamespaceConfig(
$router, $dispatcher, $config, $performance, $container,
namespaceConfig: [
'App\Application\Admin\*' => ['visibility' => 'admin']
]
);
```
**Neu**:
```php
// In RouteAuthorizationServiceInitializer
$namespaceConfig = [
'App\Application\Admin\*' => [
'visibility' => 'admin',
'access_policy' => NamespaceAccessPolicy::blocked() // NEU
]
];
```
## Troubleshooting
### Problem: Route wirft 404 obwohl sie erreichbar sein sollte
**Debugging**:
1. Prüfe `RouteAuthorizationServiceInitializer` Config
2. Check ob Controller-Namespace in `namespaceConfig` matched
3. Prüfe `access_policy` - ist Controller in Allowlist?
4. Check IP-Restrictions (`visibility`)
### Problem: Allowlist funktioniert nicht
**Ursache**: Controller-Klasse exakt mit `::class` angeben
```php
// ❌ Falsch
NamespaceAccessPolicy::blockedExcept('LoginController')
// ✅ Korrekt
NamespaceAccessPolicy::blockedExcept(
\App\Application\Admin\LoginController::class
)
```
## Framework Integration
- **Automatic Discovery**: Service wird via `#[Initializer]` automatisch registriert
- **DI Container**: Alle Dependencies werden automatisch injected
- **Type Safety**: Value Objects für alle Policies
- **Readonly Classes**: Unveränderliche Policies für Thread-Safety
- **Framework-konform**: Nutzt bestehende Patterns (IpAuthPolicy, RouteNotFound)