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
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:
202
docs/features/database/migrations.md
Normal file
202
docs/features/database/migrations.md
Normal 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)
|
||||
1526
docs/features/database/patterns.md
Normal file
1526
docs/features/database/patterns.md
Normal file
File diff suppressed because it is too large
Load Diff
754
docs/features/error-handling/advanced.md
Normal file
754
docs/features/error-handling/advanced.md
Normal 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)
|
||||
|
||||
645
docs/features/error-handling/guide.md
Normal file
645
docs/features/error-handling/guide.md
Normal 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);
|
||||
}
|
||||
```
|
||||
963
docs/features/events/system.md
Normal file
963
docs/features/events/system.md
Normal 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
|
||||
965
docs/features/filesystem/patterns.md
Normal file
965
docs/features/filesystem/patterns.md
Normal 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
|
||||
458
docs/features/queue/scheduler.md
Normal file
458
docs/features/queue/scheduler.md
Normal 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
|
||||
1136
docs/features/queue/system.md
Normal file
1136
docs/features/queue/system.md
Normal file
File diff suppressed because it is too large
Load Diff
306
docs/features/routing/value-objects.md
Normal file
306
docs/features/routing/value-objects.md
Normal 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.
|
||||
1135
docs/features/security/patterns.md
Normal file
1135
docs/features/security/patterns.md
Normal file
File diff suppressed because it is too large
Load Diff
291
docs/features/security/route-authorization.md
Normal file
291
docs/features/security/route-authorization.md
Normal 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)
|
||||
Reference in New Issue
Block a user