feat: CI/CD pipeline setup complete - Ansible playbooks updated, secrets configured, workflow ready
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -464,23 +464,937 @@ $schema->table('users', function (Blueprint $table) {
|
||||
|
||||
## EntityManager Usage
|
||||
|
||||
TODO: Document EntityManager and UnitOfWork pattern
|
||||
### Overview
|
||||
|
||||
Der EntityManager ist das zentrale Interface für alle Datenbank-Operationen. Er kombiniert mehrere Patterns:
|
||||
- **Unit of Work**: Change Tracking und Transaction Management
|
||||
- **Identity Map**: Vermeidung von Duplikaten
|
||||
- **Lazy Loading**: Performance-Optimierung durch verzögertes Laden
|
||||
- **Batch Loading**: N+1 Query Prevention
|
||||
|
||||
### Core Features
|
||||
|
||||
**Service Classes** (interne Organisation):
|
||||
- `EntityFinder`: Entity-Suche und Lazy Loading
|
||||
- `EntityPersister`: Insert, Update, Delete Operationen
|
||||
- `EntityQueryManager`: QueryBuilder Integration
|
||||
- `EntityUtilities`: Hilfsmethoden und Profiling
|
||||
|
||||
### Basic Operations
|
||||
|
||||
#### Finding Entities
|
||||
|
||||
```php
|
||||
use App\Framework\Database\EntityManager;
|
||||
|
||||
final readonly class UserService
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManager $entityManager
|
||||
) {}
|
||||
|
||||
public function getUserById(UserId $id): ?User
|
||||
{
|
||||
// Lazy Loading (Standard)
|
||||
return $this->entityManager->find(User::class, $id->value);
|
||||
}
|
||||
|
||||
public function getUserByIdEager(UserId $id): ?User
|
||||
{
|
||||
// Eager Loading - lädt Relations sofort
|
||||
return $this->entityManager->findEager(User::class, $id->value);
|
||||
}
|
||||
|
||||
public function getAllUsers(): array
|
||||
{
|
||||
return $this->entityManager->findAll(User::class);
|
||||
}
|
||||
|
||||
public function findUserByEmail(Email $email): ?User
|
||||
{
|
||||
return $this->entityManager->findOneBy(
|
||||
User::class,
|
||||
['email' => $email->value]
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Saving Entities
|
||||
|
||||
```php
|
||||
public function createUser(CreateUserCommand $command): User
|
||||
{
|
||||
$user = User::create(
|
||||
id: $this->entityManager->generateId(),
|
||||
email: $command->email,
|
||||
name: $command->name
|
||||
);
|
||||
|
||||
// save() erkennt automatisch INSERT vs UPDATE
|
||||
return $this->entityManager->save($user);
|
||||
}
|
||||
|
||||
public function updateUser(User $user, UpdateUserCommand $command): User
|
||||
{
|
||||
// Explizites UPDATE
|
||||
$user->updateProfile($command->name);
|
||||
return $this->entityManager->update($user);
|
||||
}
|
||||
```
|
||||
|
||||
#### Batch Operations
|
||||
|
||||
```php
|
||||
public function createMultipleUsers(array $commands): array
|
||||
{
|
||||
$users = [];
|
||||
foreach ($commands as $command) {
|
||||
$users[] = User::create(
|
||||
id: $this->entityManager->generateId(),
|
||||
email: $command->email,
|
||||
name: $command->name
|
||||
);
|
||||
}
|
||||
|
||||
// Batch-Insert für bessere Performance
|
||||
return $this->entityManager->saveAll(...$users);
|
||||
}
|
||||
```
|
||||
|
||||
#### Deleting Entities
|
||||
|
||||
```php
|
||||
public function deleteUser(User $user): void
|
||||
{
|
||||
$this->entityManager->delete($user);
|
||||
}
|
||||
```
|
||||
|
||||
### N+1 Query Prevention
|
||||
|
||||
**Problem**: Lazy Loading führt zu N+1 Queries
|
||||
|
||||
```php
|
||||
// ❌ N+1 Problem
|
||||
$users = $this->entityManager->findAll(User::class);
|
||||
|
||||
foreach ($users as $user) {
|
||||
// Jede Iteration führt eine separate Query aus (N+1)
|
||||
echo $user->getProfile()->bio;
|
||||
}
|
||||
```
|
||||
|
||||
**Solution**: Batch Loading mit Relations
|
||||
|
||||
```php
|
||||
// ✅ Single Query mit Batch Loading
|
||||
$users = $this->entityManager->findWithRelations(
|
||||
User::class,
|
||||
criteria: [],
|
||||
relations: ['profile', 'posts'] // Lädt Relations in Batches
|
||||
);
|
||||
|
||||
foreach ($users as $user) {
|
||||
// Kein zusätzlicher Query - Profile bereits geladen
|
||||
echo $user->getProfile()->bio;
|
||||
}
|
||||
```
|
||||
|
||||
### Lazy Loading vs Eager Loading
|
||||
|
||||
**Lazy Loading** (Standard):
|
||||
- Relations werden bei Zugriff geladen
|
||||
- Performance-Vorteil bei ungenutzten Relations
|
||||
- Potentielles N+1 Problem
|
||||
|
||||
```php
|
||||
$user = $this->entityManager->find(User::class, $id);
|
||||
// Profile wird erst bei Zugriff geladen:
|
||||
$bio = $user->getProfile()->bio; // Separate Query
|
||||
```
|
||||
|
||||
**Eager Loading**:
|
||||
- Relations werden sofort geladen
|
||||
- Kein N+1 Problem
|
||||
- Overhead bei ungenutzten Relations
|
||||
|
||||
```php
|
||||
$user = $this->entityManager->findEager(User::class, $id);
|
||||
// Profile ist bereits geladen - kein zusätzlicher Query
|
||||
$bio = $user->getProfile()->bio;
|
||||
```
|
||||
|
||||
### Identity Map
|
||||
|
||||
Der EntityManager nutzt eine Identity Map zur Vermeidung von Duplikaten:
|
||||
|
||||
```php
|
||||
// Beide Aufrufe liefern die GLEICHE Instanz
|
||||
$user1 = $this->entityManager->find(User::class, '123');
|
||||
$user2 = $this->entityManager->find(User::class, '123');
|
||||
|
||||
var_dump($user1 === $user2); // true
|
||||
```
|
||||
|
||||
**Vorteile**:
|
||||
- Keine Duplikate im Speicher
|
||||
- Konsistente Object Identity
|
||||
- Automatisches Change Tracking
|
||||
|
||||
**Utility Methods**:
|
||||
|
||||
```php
|
||||
// Detach Entity (aus Identity Map entfernen)
|
||||
$this->entityManager->detach($user);
|
||||
|
||||
// Clear Identity Map (z.B. für Batch Processing)
|
||||
$this->entityManager->clear();
|
||||
|
||||
// Identity Map Statistics
|
||||
$stats = $this->entityManager->getIdentityMapStats();
|
||||
// Returns: ['total_entities' => 150, 'entities_by_class' => [...]]
|
||||
```
|
||||
|
||||
### Reference Loading
|
||||
|
||||
**getReference()** - Lade Entity-Referenz ohne Existenz-Check:
|
||||
|
||||
```php
|
||||
// Erstellt Proxy ohne Database Query
|
||||
$user = $this->entityManager->getReference(User::class, $userId);
|
||||
|
||||
// Query wird erst bei Zugriff ausgeführt
|
||||
$name = $user->getName(); // Jetzt wird geladen
|
||||
```
|
||||
|
||||
**Use Cases**:
|
||||
- Foreign Key Relationships
|
||||
- Performance-kritische Pfade
|
||||
- Wenn Existenz bereits bekannt ist
|
||||
|
||||
### Profiling und Debugging
|
||||
|
||||
```php
|
||||
// Profiling aktivieren
|
||||
$this->entityManager->setProfilingEnabled(true);
|
||||
|
||||
// Operationen ausführen
|
||||
$users = $this->entityManager->findAll(User::class);
|
||||
|
||||
// Profiling-Statistiken abrufen
|
||||
$stats = $this->entityManager->getProfilingStatistics();
|
||||
|
||||
// Profiling-Summary
|
||||
$summary = $this->entityManager->getProfilingSummary();
|
||||
// Returns: ProfileSummary mit total_queries, total_time, etc.
|
||||
|
||||
// Profiling-Daten löschen
|
||||
$this->entityManager->clearProfilingData();
|
||||
```
|
||||
|
||||
### Criteria API (Type-Safe Queries)
|
||||
|
||||
**Modern**: Verwendung von Criteria statt Arrays
|
||||
|
||||
```php
|
||||
use App\Framework\Database\Criteria\DetachedCriteria;
|
||||
|
||||
// Type-safe Criteria
|
||||
$criteria = DetachedCriteria::forEntity(User::class)
|
||||
->where('email', '=', $email->value)
|
||||
->andWhere('active', '=', true)
|
||||
->orderBy('created_at', 'DESC')
|
||||
->limit(10);
|
||||
|
||||
$users = $this->entityManager->findByCriteria($criteria);
|
||||
$user = $this->entityManager->findOneByCriteria($criteria);
|
||||
$count = $this->entityManager->countByCriteria($criteria);
|
||||
```
|
||||
|
||||
### QueryBuilder Integration
|
||||
|
||||
Für komplexe Queries:
|
||||
|
||||
```php
|
||||
// QueryBuilder für Entity-Klasse
|
||||
$queryBuilder = $this->entityManager->createQueryBuilderFor(User::class);
|
||||
|
||||
$users = $queryBuilder
|
||||
->select('*')
|
||||
->where('email LIKE ?', ['%@example.com'])
|
||||
->andWhere('created_at > ?', ['2024-01-01'])
|
||||
->orderBy('name', 'ASC')
|
||||
->limit(50)
|
||||
->fetchAll();
|
||||
|
||||
// QueryBuilder für Table (ohne Entity)
|
||||
$qb = $this->entityManager->createQueryBuilderForTable('users');
|
||||
```
|
||||
|
||||
### Domain Events Integration
|
||||
|
||||
```php
|
||||
// Domain Event aufzeichnen
|
||||
$this->entityManager->recordDomainEvent($user, new UserRegisteredEvent($user));
|
||||
|
||||
// Events für Entity dispatchen
|
||||
$this->entityManager->dispatchDomainEventsForEntity($user);
|
||||
|
||||
// Alle Events dispatchen
|
||||
$this->entityManager->dispatchAllDomainEvents();
|
||||
|
||||
// Event-Statistiken
|
||||
$stats = $this->entityManager->getDomainEventStats();
|
||||
// Returns: ['total_events' => 23, 'events_by_entity' => [...]]
|
||||
```
|
||||
|
||||
## Repository Pattern
|
||||
|
||||
TODO: Document repository implementation and usage
|
||||
### Overview
|
||||
|
||||
Das Repository Pattern abstrahiert Daten-Zugriff und bietet domain-spezifische Query-Methoden. Das Framework nutzt **Composition over Inheritance**.
|
||||
|
||||
### Base EntityRepository
|
||||
|
||||
**Framework-Service** für Common Operations:
|
||||
|
||||
```php
|
||||
namespace App\Framework\Database\Repository;
|
||||
|
||||
final readonly class EntityRepository
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManager $entityManager
|
||||
) {}
|
||||
|
||||
public function find(string $entityClass, string $id): ?object
|
||||
public function findAll(string $entityClass): array
|
||||
public function findBy(string $entityClass, array $criteria, ...): array
|
||||
public function findOneBy(string $entityClass, array $criteria): ?object
|
||||
public function save(object $entity): object
|
||||
public function delete(object $entity): void
|
||||
public function transaction(callable $callback): mixed
|
||||
|
||||
// Batch Loading (N+1 Prevention)
|
||||
public function findWithRelations(...): array
|
||||
|
||||
// Pagination
|
||||
public function findPaginated(...): PaginatedResult
|
||||
|
||||
// Batch Operations
|
||||
public function saveBatch(array $entities, int $batchSize = 100): array
|
||||
public function deleteBatch(array $entities, int $batchSize = 100): void
|
||||
}
|
||||
```
|
||||
|
||||
### Domain Repository Pattern
|
||||
|
||||
**Composition-basiert** statt Inheritance:
|
||||
|
||||
```php
|
||||
namespace App\Domain\User\Repositories;
|
||||
|
||||
use App\Framework\Database\Repository\EntityRepository;
|
||||
|
||||
final readonly class UserRepository
|
||||
{
|
||||
public function __construct(
|
||||
private EntityRepository $entityRepository
|
||||
) {}
|
||||
|
||||
public function findById(UserId $id): ?User
|
||||
{
|
||||
return $this->entityRepository->find(User::class, $id->value);
|
||||
}
|
||||
|
||||
public function findByEmail(Email $email): ?User
|
||||
{
|
||||
return $this->entityRepository->findOneBy(
|
||||
User::class,
|
||||
['email' => $email->value]
|
||||
);
|
||||
}
|
||||
|
||||
public function findActiveUsers(): array
|
||||
{
|
||||
return $this->entityRepository->findBy(
|
||||
User::class,
|
||||
criteria: ['active' => true],
|
||||
orderBy: ['created_at' => 'DESC']
|
||||
);
|
||||
}
|
||||
|
||||
public function save(User $user): User
|
||||
{
|
||||
return $this->entityRepository->save($user);
|
||||
}
|
||||
|
||||
public function delete(User $user): void
|
||||
{
|
||||
$this->entityRepository->delete($user);
|
||||
}
|
||||
|
||||
// Domain-spezifische Methode mit Batch Loading
|
||||
public function findUsersWithProfiles(array $userIds): array
|
||||
{
|
||||
return $this->entityRepository->findWithRelations(
|
||||
User::class,
|
||||
criteria: ['id' => $userIds],
|
||||
relations: ['profile', 'settings']
|
||||
);
|
||||
}
|
||||
|
||||
// Pagination
|
||||
public function findPaginated(int $page, int $limit = 20): PaginatedResult
|
||||
{
|
||||
return $this->entityRepository->findPaginated(
|
||||
User::class,
|
||||
page: $page,
|
||||
limit: $limit,
|
||||
orderBy: ['created_at' => 'DESC']
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Repository Best Practices
|
||||
|
||||
**✅ Composition Over Inheritance**:
|
||||
```php
|
||||
// ✅ Framework Pattern - Composition
|
||||
final readonly class OrderRepository
|
||||
{
|
||||
public function __construct(
|
||||
private EntityRepository $entityRepository
|
||||
) {}
|
||||
}
|
||||
|
||||
// ❌ Avoid - Inheritance
|
||||
class OrderRepository extends BaseRepository
|
||||
{
|
||||
// Problematisch: Tight coupling
|
||||
}
|
||||
```
|
||||
|
||||
**✅ Domain-Specific Methods**:
|
||||
```php
|
||||
// ✅ Sprechende Domain-Methoden
|
||||
public function findOverdueOrders(): array
|
||||
{
|
||||
return $this->entityRepository->findBy(
|
||||
Order::class,
|
||||
['status' => 'pending'],
|
||||
orderBy: ['due_date' => 'ASC']
|
||||
);
|
||||
}
|
||||
|
||||
// ❌ Generic Methods außerhalb Domain
|
||||
public function findBy(array $criteria): array
|
||||
{
|
||||
// Zu generisch - nutze EntityRepository direkt
|
||||
}
|
||||
```
|
||||
|
||||
**✅ N+1 Prevention**:
|
||||
```php
|
||||
// ✅ Batch Loading für Relations
|
||||
public function findOrdersWithItems(array $orderIds): array
|
||||
{
|
||||
return $this->entityRepository->findWithRelations(
|
||||
Order::class,
|
||||
criteria: ['id' => $orderIds],
|
||||
relations: ['items', 'customer'] // Lädt in Batches
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Unit of Work Pattern
|
||||
|
||||
### Overview
|
||||
|
||||
Der Unit of Work Pattern verwaltet Entities und deren Änderungen für transaktionale Konsistenz.
|
||||
|
||||
**Key Features**:
|
||||
- **Change Tracking**: Automatische Änderungserkennung
|
||||
- **Transactional Consistency**: Atomic Commits/Rollbacks
|
||||
- **Batch Operations**: Optimierte Bulk-Operationen
|
||||
- **Entity States**: NEW, MANAGED, DIRTY, DELETED, DETACHED
|
||||
|
||||
### Entity Lifecycle
|
||||
|
||||
```php
|
||||
use App\Framework\Database\UnitOfWork\UnitOfWork;
|
||||
|
||||
final readonly class OrderService
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManager $entityManager
|
||||
) {}
|
||||
|
||||
public function createOrder(CreateOrderCommand $command): Order
|
||||
{
|
||||
$unitOfWork = $this->entityManager->unitOfWork;
|
||||
|
||||
// 1. Create new entity (state: NEW)
|
||||
$order = Order::create($command);
|
||||
|
||||
// 2. Persist marks entity for INSERT (state: NEW → MANAGED)
|
||||
$unitOfWork->persist($order);
|
||||
|
||||
// 3. Flush writes to database (state: MANAGED)
|
||||
$unitOfWork->flush();
|
||||
|
||||
// 4. Commit transaction
|
||||
$unitOfWork->commit();
|
||||
|
||||
return $order;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Change Tracking
|
||||
|
||||
**Automatic Change Detection**:
|
||||
|
||||
```php
|
||||
$unitOfWork = $this->entityManager->unitOfWork;
|
||||
|
||||
// Load entity (state: MANAGED)
|
||||
$user = $this->entityManager->find(User::class, $userId);
|
||||
|
||||
// Modify entity
|
||||
$user->updateProfile($newName);
|
||||
|
||||
// Change Tracking erkennt Änderung automatisch
|
||||
$unitOfWork->flush(); // UPDATE query wird generiert
|
||||
$unitOfWork->commit();
|
||||
```
|
||||
|
||||
**Manual Change Tracking**:
|
||||
|
||||
```php
|
||||
// Entity aus externer Quelle (state: DETACHED)
|
||||
$user = unserialize($serializedUser);
|
||||
|
||||
// Merge in Unit of Work (state: DETACHED → MANAGED)
|
||||
$managedUser = $unitOfWork->merge($user);
|
||||
|
||||
// Änderungen werden getrackt
|
||||
$unitOfWork->flush();
|
||||
```
|
||||
|
||||
### Transaction Management
|
||||
|
||||
**Explicit Transactions**:
|
||||
|
||||
```php
|
||||
public function processOrder(Order $order): void
|
||||
{
|
||||
$unitOfWork = $this->entityManager->unitOfWork;
|
||||
|
||||
try {
|
||||
$unitOfWork->beginTransaction();
|
||||
|
||||
// Multiple operations
|
||||
$order->confirm();
|
||||
$unitOfWork->persist($order);
|
||||
|
||||
$this->inventoryService->reserve($order->items);
|
||||
$this->paymentService->charge($order->total);
|
||||
|
||||
// Write changes to database
|
||||
$unitOfWork->flush();
|
||||
|
||||
// Commit transaction
|
||||
$unitOfWork->commit();
|
||||
} catch (\Exception $e) {
|
||||
$unitOfWork->rollback();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Implicit Transactions (Auto-Commit)**:
|
||||
|
||||
```php
|
||||
// Auto-commit ist standardmäßig aktiviert
|
||||
$unitOfWork->persist($entity); // Automatisch committed
|
||||
```
|
||||
|
||||
**Disable Auto-Commit** für Batch Operations:
|
||||
|
||||
```php
|
||||
$unitOfWork->setAutoCommit(false);
|
||||
|
||||
try {
|
||||
$unitOfWork->beginTransaction();
|
||||
|
||||
foreach ($orders as $order) {
|
||||
$unitOfWork->persist($order); // Kein Auto-Commit
|
||||
}
|
||||
|
||||
$unitOfWork->flush(); // Batch-Write
|
||||
$unitOfWork->commit(); // Atomic Commit
|
||||
} catch (\Exception $e) {
|
||||
$unitOfWork->rollback();
|
||||
throw $e;
|
||||
} finally {
|
||||
$unitOfWork->setAutoCommit(true);
|
||||
}
|
||||
```
|
||||
|
||||
### Entity States
|
||||
|
||||
```php
|
||||
use App\Framework\Database\UnitOfWork\EntityState;
|
||||
|
||||
// Check Entity State
|
||||
$state = $unitOfWork->getEntityState($entity);
|
||||
|
||||
match ($state) {
|
||||
EntityState::NEW => 'Entity marked for INSERT',
|
||||
EntityState::MANAGED => 'Entity being tracked, no changes',
|
||||
EntityState::DIRTY => 'Entity modified, pending UPDATE',
|
||||
EntityState::DELETED => 'Entity marked for DELETE',
|
||||
EntityState::DETACHED => 'Entity not managed by UnitOfWork',
|
||||
};
|
||||
|
||||
// Check if entity is managed
|
||||
if ($unitOfWork->contains($entity)) {
|
||||
// Entity is managed (NEW, MANAGED, DIRTY, or DELETED)
|
||||
}
|
||||
```
|
||||
|
||||
### Detaching Entities
|
||||
|
||||
```php
|
||||
// Detach entity from Unit of Work
|
||||
$unitOfWork->detach($user);
|
||||
|
||||
// Entity state: DETACHED
|
||||
// Changes to entity are no longer tracked
|
||||
```
|
||||
|
||||
**Use Cases**:
|
||||
- Long-running processes
|
||||
- Serialization/Caching
|
||||
- Read-only operations
|
||||
- Memory management in batch processing
|
||||
|
||||
### Bulk Operations
|
||||
|
||||
```php
|
||||
// Bulk Insert
|
||||
$unitOfWork->bulkInsert($entities); // Optimized batch INSERT
|
||||
|
||||
// Bulk Update
|
||||
$unitOfWork->bulkUpdate($entities); // Optimized batch UPDATE
|
||||
|
||||
// Bulk Delete
|
||||
$unitOfWork->bulkDelete($entities); // Optimized batch DELETE
|
||||
```
|
||||
|
||||
## Query Optimization
|
||||
|
||||
TODO: Document N+1 prevention and batch loading
|
||||
### N+1 Query Problem
|
||||
|
||||
**Erkennnung**: Jede Iteration führt zusätzliche Query aus
|
||||
|
||||
```php
|
||||
// ❌ N+1 Problem
|
||||
$orders = $this->entityManager->findAll(Order::class); // 1 Query
|
||||
|
||||
foreach ($orders as $order) {
|
||||
echo $order->getCustomer()->name; // N Queries (1 pro Order)
|
||||
}
|
||||
// Total: 1 + N Queries
|
||||
```
|
||||
|
||||
### Solution: Batch Loading
|
||||
|
||||
```php
|
||||
// ✅ Batch Loading
|
||||
$orders = $this->entityManager->findWithRelations(
|
||||
Order::class,
|
||||
criteria: [],
|
||||
relations: ['customer', 'items'] // Preload relations
|
||||
);
|
||||
|
||||
foreach ($orders as $order) {
|
||||
echo $order->getCustomer()->name; // Kein Query - bereits geladen
|
||||
}
|
||||
// Total: 2-3 Queries (1 für Orders, 1-2 für Relations)
|
||||
```
|
||||
|
||||
### Eager vs Lazy Loading
|
||||
|
||||
**Strategic Loading**:
|
||||
|
||||
```php
|
||||
// Heavy Operation - Eager Loading
|
||||
public function getOrderDetails(OrderId $id): OrderDetails
|
||||
{
|
||||
$order = $this->entityManager->findEager(Order::class, $id->value);
|
||||
// Alle Relations sofort geladen
|
||||
return OrderDetails::fromOrder($order);
|
||||
}
|
||||
|
||||
// Light Operation - Lazy Loading
|
||||
public function listOrders(): array
|
||||
{
|
||||
// Relations werden nur bei Bedarf geladen
|
||||
return $this->entityManager->findAll(Order::class);
|
||||
}
|
||||
```
|
||||
|
||||
### Pagination
|
||||
|
||||
```php
|
||||
use App\Framework\Database\Repository\EntityRepository;
|
||||
|
||||
public function getUsers(int $page): PaginatedResult
|
||||
{
|
||||
return $this->entityRepository->findPaginated(
|
||||
User::class,
|
||||
page: $page,
|
||||
limit: 20,
|
||||
orderBy: ['created_at' => 'DESC']
|
||||
);
|
||||
}
|
||||
|
||||
// PaginatedResult enthält:
|
||||
// - items: array (Entities)
|
||||
// - total: int (Total count)
|
||||
// - page: int (Current page)
|
||||
// - limit: int (Items per page)
|
||||
// - totalPages: int (Calculated)
|
||||
```
|
||||
|
||||
### Query Caching
|
||||
|
||||
```php
|
||||
// EntityManager mit Cache
|
||||
$entityManager = new EntityManager(
|
||||
// ...
|
||||
cacheManager: $cacheManager
|
||||
);
|
||||
|
||||
// Find verwendet Cache automatisch
|
||||
$user = $entityManager->find(User::class, $id);
|
||||
// Beim zweiten Aufruf: Cache Hit
|
||||
```
|
||||
|
||||
## Connection Pooling
|
||||
|
||||
TODO: Document connection pool configuration
|
||||
### Overview
|
||||
|
||||
Connection Pooling verbessert Performance durch Wiederverwendung von Datenbankverbindungen.
|
||||
|
||||
**Features**:
|
||||
- Min/Max Connection Limits
|
||||
- Connection Health Monitoring
|
||||
- Automatic Retry mit Exponential Backoff
|
||||
- Idle Connection Cleanup
|
||||
- Connection Warmup
|
||||
|
||||
### Configuration
|
||||
|
||||
```php
|
||||
use App\Framework\Database\Config\PoolConfig;
|
||||
|
||||
$poolConfig = new PoolConfig(
|
||||
minConnections: 2, // Minimum pool size
|
||||
maxConnections: 10, // Maximum pool size
|
||||
maxIdleTime: 300, // Idle timeout (seconds)
|
||||
healthCheckInterval: 60, // Health check frequency
|
||||
enableWarmup: true // Pre-create min connections
|
||||
);
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```php
|
||||
use App\Framework\Database\ConnectionPool;
|
||||
|
||||
$pool = new ConnectionPool($driverConfig, $poolConfig, $timer);
|
||||
|
||||
// Get connection from pool
|
||||
$connection = $pool->getConnection();
|
||||
|
||||
try {
|
||||
// Use connection
|
||||
$result = $connection->query('SELECT * FROM users');
|
||||
} finally {
|
||||
// Return connection to pool (automatic with PooledConnection)
|
||||
$connection->release();
|
||||
}
|
||||
```
|
||||
|
||||
### Health Monitoring
|
||||
|
||||
```php
|
||||
// Pool automatically monitors connection health
|
||||
$pool->performHealthCheck();
|
||||
|
||||
// Get pool statistics
|
||||
$stats = $pool->getPoolStatistics();
|
||||
// Returns:
|
||||
[
|
||||
'current_connections' => 5,
|
||||
'max_connections' => 10,
|
||||
'connections_in_use' => 3,
|
||||
'healthy_connections' => 5,
|
||||
'total_created' => 12,
|
||||
'total_destroyed' => 7
|
||||
]
|
||||
```
|
||||
|
||||
### Retry Logic
|
||||
|
||||
```php
|
||||
// Automatic retry with exponential backoff
|
||||
$retryStrategy = ExponentialBackoffStrategy::create()
|
||||
->withMaxAttempts(3)
|
||||
->withBaseDelay(Duration::fromMilliseconds(100));
|
||||
|
||||
// Connection pool uses retry automatically
|
||||
$connection = $pool->getConnectionWithRetry($retryStrategy);
|
||||
```
|
||||
|
||||
### Production Configuration
|
||||
|
||||
```php
|
||||
// Production Pool Config
|
||||
$productionPoolConfig = new PoolConfig(
|
||||
minConnections: 5, // Keep 5 connections ready
|
||||
maxConnections: 50, // Scale up to 50 under load
|
||||
maxIdleTime: 600, // 10 minutes idle timeout
|
||||
healthCheckInterval: 30, // Check every 30 seconds
|
||||
enableWarmup: true, // Pre-warm connections
|
||||
connectionTimeout: 5000 // 5 second timeout
|
||||
);
|
||||
```
|
||||
|
||||
## Transaction Management
|
||||
|
||||
TODO: Document transaction patterns and best practices
|
||||
### Basic Transactions
|
||||
|
||||
```php
|
||||
// EntityManager Transaction Helper
|
||||
$this->entityManager->transaction(function (EntityManager $em) {
|
||||
$order = Order::create($command);
|
||||
$em->save($order);
|
||||
|
||||
$this->inventoryService->reserve($order->items);
|
||||
$this->paymentService->charge($order->total);
|
||||
|
||||
// Automatic commit on success, rollback on exception
|
||||
});
|
||||
```
|
||||
|
||||
### Manual Transaction Control
|
||||
|
||||
```php
|
||||
$unitOfWork = $this->entityManager->unitOfWork;
|
||||
|
||||
$unitOfWork->beginTransaction();
|
||||
|
||||
try {
|
||||
// Operations
|
||||
$unitOfWork->persist($entity1);
|
||||
$unitOfWork->persist($entity2);
|
||||
|
||||
$unitOfWork->flush();
|
||||
$unitOfWork->commit();
|
||||
} catch (\Exception $e) {
|
||||
$unitOfWork->rollback();
|
||||
throw $e;
|
||||
}
|
||||
```
|
||||
|
||||
### Nested Transactions (Savepoints)
|
||||
|
||||
```php
|
||||
$unitOfWork->beginTransaction();
|
||||
|
||||
try {
|
||||
$order = Order::create($command);
|
||||
$unitOfWork->persist($order);
|
||||
|
||||
// Nested transaction (savepoint)
|
||||
$unitOfWork->beginTransaction();
|
||||
try {
|
||||
$this->inventoryService->reserve($order->items);
|
||||
$unitOfWork->commit(); // Commit savepoint
|
||||
} catch (\Exception $e) {
|
||||
$unitOfWork->rollback(); // Rollback to savepoint
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$unitOfWork->commit(); // Commit main transaction
|
||||
} catch (\Exception $e) {
|
||||
$unitOfWork->rollback(); // Rollback main transaction
|
||||
throw $e;
|
||||
}
|
||||
```
|
||||
|
||||
### Transaction Isolation Levels
|
||||
|
||||
```php
|
||||
use App\Framework\Database\TransactionIsolation;
|
||||
|
||||
$unitOfWork->beginTransaction(
|
||||
isolation: TransactionIsolation::READ_COMMITTED
|
||||
);
|
||||
|
||||
// Available isolation levels:
|
||||
// - READ_UNCOMMITTED (lowest isolation)
|
||||
// - READ_COMMITTED (default)
|
||||
// - REPEATABLE_READ
|
||||
// - SERIALIZABLE (highest isolation)
|
||||
```
|
||||
|
||||
### Transaction Best Practices
|
||||
|
||||
**✅ Short Transactions**:
|
||||
```php
|
||||
// ✅ Keep transactions short
|
||||
$this->entityManager->transaction(fn(EntityManager $em) =>
|
||||
$em->save($entity)
|
||||
);
|
||||
```
|
||||
|
||||
**❌ Long Transactions**:
|
||||
```php
|
||||
// ❌ Avoid long-running transactions
|
||||
$this->entityManager->transaction(function (EntityManager $em) {
|
||||
$this->sendEmail($user); // External I/O - BAD
|
||||
$this->processHeavyComputation(); // CPU-intensive - BAD
|
||||
$em->save($entity);
|
||||
});
|
||||
```
|
||||
|
||||
**✅ Batch with Periodic Commits**:
|
||||
```php
|
||||
// ✅ Batch processing with periodic commits
|
||||
$unitOfWork->setAutoCommit(false);
|
||||
$batchSize = 100;
|
||||
|
||||
for ($i = 0; $i < count($items); $i++) {
|
||||
$unitOfWork->persist($items[$i]);
|
||||
|
||||
if (($i + 1) % $batchSize === 0) {
|
||||
$unitOfWork->flush();
|
||||
$unitOfWork->commit();
|
||||
$unitOfWork->beginTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
$unitOfWork->flush();
|
||||
$unitOfWork->commit();
|
||||
$unitOfWork->setAutoCommit(true);
|
||||
```
|
||||
|
||||
## Database Testing
|
||||
|
||||
|
||||
@@ -2,26 +2,962 @@
|
||||
|
||||
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
|
||||
|
||||
TODO: Document the differences and when to use each
|
||||
### 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
|
||||
|
||||
TODO: Document domain event creation and handling
|
||||
### 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
|
||||
|
||||
TODO: Document event handler attributes and 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
|
||||
|
||||
TODO: Document event middleware system
|
||||
**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
|
||||
|
||||
TODO: Document async event handling patterns
|
||||
### 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
|
||||
|
||||
TODO: List event system 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
|
||||
|
||||
@@ -354,6 +354,7 @@ $users = $this->userRepository->findWithProfiles($userIds);
|
||||
### Caching Strategy
|
||||
|
||||
**Framework Cache Interface mit Value Objects**:
|
||||
|
||||
```php
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\Cache\CacheKey;
|
||||
@@ -878,4 +879,4 @@ Database VOs folgen allen Framework-Prinzipien:
|
||||
- ✅ **Type Safety**: Union Types für Backwards Compatibility
|
||||
- ✅ **Framework Integration**: `__toString()` für seamless SQL interpolation
|
||||
- ✅ **Validation**: Constructor-basierte Validation
|
||||
- ✅ **Explicit**: Factory Methods (`fromString()`) für clarity
|
||||
- ✅ **Explicit**: Factory Methods (`fromString()`) für clarity
|
||||
|
||||
@@ -45,6 +45,7 @@ Alle Caches unterstützen **automatische Performance-Metriken** über Decorator
|
||||
**Performance**: ~70% schnellere Component-Initialisierung
|
||||
|
||||
**Verwendung**:
|
||||
|
||||
```php
|
||||
use App\Framework\LiveComponents\Cache\ComponentStateCache;
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -455,4 +455,4 @@ Die Pipeline nutzt konsequent Framework-Patterns:
|
||||
- **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
|
||||
- **Memory Efficiency**: Linear scaling mit Job-Complexity
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user