feat: CI/CD pipeline setup complete - Ansible playbooks updated, secrets configured, workflow ready

This commit is contained in:
2025-10-31 01:39:24 +01:00
parent 55c04e4fd0
commit e26eb2aa12
601 changed files with 44184 additions and 32477 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,381 @@
# Docker Swarm + Traefik Deployment Guide
Production deployment guide for the Custom PHP Framework using Docker Swarm orchestration with Traefik load balancer.
## Architecture Overview
```
Internet → Traefik (SSL Termination, Load Balancing)
[Web Service - 3 Replicas]
↓ ↓
Database Redis Queue Workers
(PostgreSQL) (Cache/Sessions) (2 Replicas)
```
**Key Components**:
- **Traefik v2.10**: Reverse proxy, SSL termination, automatic service discovery
- **Web Service**: 3 replicas of PHP-FPM + Nginx (HTTP only, Traefik handles HTTPS)
- **PostgreSQL 16**: Single instance database (manager node)
- **Redis 7**: Sessions and cache (manager node)
- **Queue Workers**: 2 replicas for background job processing
- **Docker Swarm**: Native container orchestration with rolling updates and health checks
## Prerequisites
1. **Docker Engine 28.0+** with Swarm mode enabled
2. **Production Server** with SSH access
3. **SSL Certificates** in `./ssl/` directory (cert.pem, key.pem)
4. **Environment Variables** in `.env` file on production server
5. **Docker Image** built and available
## Initial Setup
### 1. Initialize Docker Swarm
On production server:
```bash
docker swarm init
```
Verify:
```bash
docker node ls
# Should show 1 node as Leader
```
### 2. Create Docker Secrets
Create secrets from .env file values:
```bash
cd /home/deploy/framework
# Create secrets (one-time setup)
echo "$DB_PASSWORD" | docker secret create db_password -
echo "$APP_KEY" | docker secret create app_key -
echo "$VAULT_ENCRYPTION_KEY" | docker secret create vault_encryption_key -
echo "$SHOPIFY_WEBHOOK_SECRET" | docker secret create shopify_webhook_secret -
echo "$RAPIDMAIL_PASSWORD" | docker secret create rapidmail_password -
```
Or use the automated script:
```bash
./scripts/setup-production-secrets.sh
```
Verify secrets:
```bash
docker secret ls
```
### 3. Build and Transfer Docker Image
On local machine:
**Option A: Via Private Registry** (if available):
```bash
# Build image
docker build -f Dockerfile.production -t 94.16.110.151:5000/framework:latest .
# Push to registry
docker push 94.16.110.151:5000/framework:latest
```
**Option B: Direct Transfer via SSH** (recommended for now):
```bash
# Build image
docker build -f Dockerfile.production -t 94.16.110.151:5000/framework:latest .
# Save and transfer to production
docker save 94.16.110.151:5000/framework:latest | \
ssh -i ~/.ssh/production deploy@94.16.110.151 'docker load'
```
### 4. Deploy Stack
On production server:
```bash
cd /home/deploy/framework
# Deploy the stack
docker stack deploy -c docker-compose.prod.yml framework
# Monitor deployment
watch docker stack ps framework
# Check service status
docker stack services framework
```
## Health Monitoring
### Check Service Status
```bash
# List all services
docker stack services framework
# Check specific service
docker service ps framework_web
# View service logs
docker service logs framework_web -f
docker service logs framework_traefik -f
docker service logs framework_db -f
```
### Health Check Endpoints
- **Main Health**: http://localhost/health (via Traefik)
- **Traefik Dashboard**: http://traefik.localhost:8080 (manager node only)
### Expected Service Replicas
| Service | Replicas | Purpose |
|---------|----------|---------|
| traefik | 1 | Reverse proxy + SSL |
| web | 3 | Application servers |
| db | 1 | PostgreSQL database |
| redis | 1 | Cache + sessions |
| queue-worker | 2 | Background jobs |
## Rolling Updates
### Update Application
1. Build new image with updated code:
```bash
docker build -f Dockerfile.production -t 94.16.110.151:5000/framework:latest .
```
2. Transfer to production (if no registry):
```bash
docker save 94.16.110.151:5000/framework:latest | \
ssh -i ~/.ssh/production deploy@94.16.110.151 'docker load'
```
3. Update the service:
```bash
# On production server
docker service update --image 94.16.110.151:5000/framework:latest framework_web
```
The update will:
- Roll out to 1 container at a time (`parallelism: 1`)
- Wait 10 seconds between updates (`delay: 10s`)
- Start new container before stopping old one (`order: start-first`)
- Automatically rollback on failure (`failure_action: rollback`)
### Monitor Update Progress
```bash
# Watch update status
watch docker service ps framework_web
# View update logs
docker service logs framework_web -f --tail 50
```
### Manual Rollback
If needed, rollback to previous version:
```bash
docker service rollback framework_web
```
## Troubleshooting
### Service Won't Start
Check service logs:
```bash
docker service logs framework_web --tail 100
```
Check task failures:
```bash
docker service ps framework_web --no-trunc
```
### Container Crashing
Inspect individual container:
```bash
# Get container ID
docker ps -a | grep framework_web
# View logs
docker logs <container_id>
# Exec into running container
docker exec -it <container_id> bash
```
### SSL/TLS Issues
Traefik handles SSL termination. Check Traefik logs:
```bash
docker service logs framework_traefik -f
```
Verify SSL certificates are mounted in docker-compose.prod.yml:
```yaml
volumes:
- ./ssl:/ssl:ro
```
### Database Connection Issues
Check PostgreSQL health:
```bash
docker service logs framework_db --tail 50
# Exec into db container
docker exec -it $(docker ps -q -f name=framework_db) psql -U postgres -d framework_prod
```
### Redis Connection Issues
Check Redis availability:
```bash
docker service logs framework_redis --tail 50
# Test Redis connection
docker exec -it $(docker ps -q -f name=framework_redis) redis-cli ping
```
### Performance Issues
Check resource usage:
```bash
# Service resource limits
docker service inspect framework_web --format='{{json .Spec.TaskTemplate.Resources}}' | jq
# Container stats
docker stats
```
## Scaling
### Scale Web Service
```bash
# Scale up to 5 replicas
docker service scale framework_web=5
# Scale down to 2 replicas
docker service scale framework_web=2
```
### Scale Queue Workers
```bash
# Scale workers based on queue backlog
docker service scale framework_queue-worker=4
```
## Cleanup
### Remove Stack
```bash
# Remove entire stack
docker stack rm framework
# Verify removal
docker stack ls
```
### Remove Secrets
```bash
# List secrets
docker secret ls
# Remove specific secret
docker secret rm db_password
# Remove all framework secrets
docker secret ls | grep -E "db_password|app_key|vault_encryption_key" | awk '{print $2}' | xargs docker secret rm
```
### Leave Swarm
```bash
# Force leave Swarm (removes all services and secrets)
docker swarm leave --force
```
## Network Architecture
### Overlay Networks
- **traefik-public**: External network for Traefik ↔ Web communication
- **backend**: Internal network for Web ↔ Database/Redis communication
### Port Mappings
| Port | Service | Purpose |
|------|---------|---------|
| 80 | Traefik | HTTP (redirects to 443) |
| 443 | Traefik | HTTPS (production traffic) |
| 8080 | Traefik | Dashboard (manager node only) |
## Volume Management
### Named Volumes
| Volume | Purpose | Mounted In |
|--------|---------|------------|
| traefik-logs | Traefik access logs | traefik |
| storage-logs | Application logs | web, queue-worker |
| storage-uploads | User uploads | web |
| storage-queue | Queue data | queue-worker |
| db-data | PostgreSQL data | db |
| redis-data | Redis persistence | redis |
### Backup Volumes
```bash
# Backup database
docker exec $(docker ps -q -f name=framework_db) pg_dump -U postgres framework_prod > backup.sql
# Backup Redis (if persistence enabled)
docker exec $(docker ps -q -f name=framework_redis) redis-cli --rdb /data/dump.rdb
```
## Security Best Practices
1. **Secrets Management**: Never commit secrets to version control, use Docker Secrets
2. **Network Isolation**: Backend network is internal-only, no external access
3. **SSL/TLS**: Traefik enforces HTTPS, redirects HTTP → HTTPS
4. **Health Checks**: All services have health checks with automatic restart
5. **Resource Limits**: Production services have memory/CPU limits
6. **Least Privilege**: Containers run as www-data (not root) where possible
## Phase 2 - Monitoring (Coming Soon)
- Prometheus for metrics collection
- Grafana dashboards
- Automated PostgreSQL backups
- Email/Slack alerting
## Phase 3 - CI/CD (Coming Soon)
- Gitea Actions workflow
- Loki + Promtail for log aggregation
- Performance tuning
## Phase 4 - High Availability (Future)
- Multi-node Swarm cluster
- Varnish CDN cache layer
- PostgreSQL Primary/Replica with pgpool
- MinIO object storage
## References
- [Docker Swarm Documentation](https://docs.docker.com/engine/swarm/)
- [Traefik v2 Documentation](https://doc.traefik.io/traefik/)
- [Docker Secrets Management](https://docs.docker.com/engine/swarm/secrets/)

View File

@@ -201,12 +201,16 @@ use App\Framework\Logging\Processors\WebInfoProcessor;
$webInfoProcessor = new WebInfoProcessor($request);
```
**3. Exception Processor**:
**3. Exception Enrichment Processor**:
```php
use App\Framework\Logging\Processors\ExceptionProcessor;
use App\Framework\Logging\Processors\ExceptionEnrichmentProcessor;
// Adds exception class, file, line, stack trace
$exceptionProcessor = new ExceptionProcessor();
// Converts Throwables to ExceptionContext with enriched metadata
// - Exception hash for pattern recognition
// - Severity categorization
// - Short stack trace for quick overview
// - Exception chain length
$exceptionEnrichmentProcessor = new ExceptionEnrichmentProcessor();
```
**4. Interpolation Processor**:

View File

@@ -0,0 +1,800 @@
# Production Deployment Guide
Umfassende Anleitung für das Deployment der Custom PHP Framework Anwendung auf dem Production Server.
## Inhaltsverzeichnis
1. [Architektur-Übersicht](#architektur-übersicht)
2. [Voraussetzungen](#voraussetzungen)
3. [Sicherheits-Setup](#sicherheits-setup)
4. [Docker Registry Setup](#docker-registry-setup)
5. [Production Image Build](#production-image-build)
6. [Deployment Prozess](#deployment-prozess)
7. [Troubleshooting](#troubleshooting)
8. [Monitoring](#monitoring)
---
## Architektur-Übersicht
### Development vs Production
**Development** (docker-compose.yml):
- Separate Container: Nginx + PHP-FPM
- Source Code via Volume Mounts
- Hot-Reload für Development
- Xdebug aktiviert
**Production** (docker-compose.prod.yml):
- Single Container: Supervisor → Nginx + PHP-FPM
- Code im Image eingebacken
- Minimale Volume Mounts (nur logs/uploads)
- Optimiert für Performance
### Production Stack
```
┌─────────────────────────────────────────────────┐
│ Production Server (94.16.110.151) │
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐│
│ │ Web │ │ PHP │ │ Redis ││
│ │ (Supervisor│ │ │ │ Cache ││
│ │ Nginx + │ │ │ │ ││
│ │ PHP-FPM) │ │ │ │ ││
│ └────────────┘ └────────────┘ └────────────┘│
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐│
│ │ PostgreSQL │ │ Queue │ │ Watchtower ││
│ │ Database │ │ Worker │ │ Auto-Update││
│ └────────────┘ └────────────┘ └────────────┘│
│ │
│ Monitoring (VPN-only): │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐│
│ │ Prometheus │ │ Grafana │ │ Portainer ││
│ │ :9090 │ │ :3000 │ │ :9443 ││
│ └────────────┘ └────────────┘ └────────────┘│
└─────────────────────────────────────────────────┘
│ WireGuard VPN (10.8.0.0/24)
┌───┴────┐
│ Client │
└────────┘
```
---
## Voraussetzungen
### Server Requirements
- **OS**: Ubuntu 22.04 LTS (oder neuer)
- **RAM**: Minimum 4GB (empfohlen: 8GB+)
- **CPU**: 2+ Cores
- **Disk**: 50GB+ freier Speicherplatz
- **Network**: Statische IP oder DNS
### Installierte Software
```bash
# Docker & Docker Compose
docker --version # 24.0+
docker-compose --version # 2.20+
# WireGuard (für sicheren Zugriff)
wg --version
# SSL Tools
openssl version
```
### Ports
**Public (Firewall offen)**:
- `8888`: HTTP (optional, für HTTP→HTTPS Redirect)
- `8443`: HTTPS (Hauptzugang)
- `51820`: WireGuard VPN (UDP)
**VPN-only (über 10.8.0.1)**:
- `9090`: Prometheus
- `3000`: Grafana
- `9443`: Portainer
**Internal (nicht extern erreichbar)**:
- `5432`: PostgreSQL
- `6379`: Redis
- `9000`: PHP-FPM
---
## Sicherheits-Setup
### 1. WireGuard VPN
WireGuard bietet verschlüsselten Zugang zum Production Server für Administration und Monitoring.
**Server Installation**:
```bash
# Als root auf Production Server
apt update
apt install -y wireguard
# Schlüssel generieren
cd /etc/wireguard
umask 077
wg genkey | tee server_private.key | wg pubkey > server_public.key
# Server Config
cat > /etc/wireguard/wg0.conf <<'EOF'
[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = <server_private_key>
# Client (Development Machine)
[Peer]
PublicKey = <client_public_key>
AllowedIPs = 10.8.0.2/32
EOF
# Service starten
systemctl enable wg-quick@wg0
systemctl start wg-quick@wg0
systemctl status wg-quick@wg0
```
**Client Configuration** (`/etc/wireguard/wg0-production.conf`):
```ini
[Interface]
Address = 10.8.0.2/32
PrivateKey = <client_private_key>
DNS = 1.1.1.1
[Peer]
PublicKey = <server_public_key>
Endpoint = 94.16.110.151:51820
AllowedIPs = 10.8.0.0/24, 94.16.110.151/32
PersistentKeepalive = 25
```
**Client Start**:
```bash
sudo wg-quick up wg0-production
sudo wg show # Verify connection
ping 10.8.0.1 # Test connectivity
```
### 2. Firewall Configuration
**UFW Rules** (auf Production Server):
```bash
# Default policies
ufw default deny incoming
ufw default allow outgoing
# SSH (nur von spezifischen IPs)
ufw allow from <deine_ip> to any port 22
# WireGuard
ufw allow 51820/udp
# HTTP/HTTPS
ufw allow 8888/tcp # HTTP (optional)
ufw allow 8443/tcp # HTTPS
# Enable firewall
ufw enable
ufw status verbose
```
### 3. SSL/TLS Zertifikate
**Development/Testing** (Self-Signed):
```bash
# Bereits vorhanden in ./ssl/
# - cert.pem
# - key.pem
```
**Production** (Let's Encrypt empfohlen):
```bash
# Mit certbot
certbot certonly --standalone -d yourdomain.com
# Zertifikate nach ./ssl/ kopieren
```
---
## Docker Registry Setup
### Local Registry on Production Server
Für sichere, private Image-Verwaltung läuft eine lokale Docker Registry auf dem Production Server.
**Registry starten**:
```bash
docker run -d \
--restart=always \
--name registry \
-p 127.0.0.1:5000:5000 \
registry:2
```
**Verify**:
```bash
curl http://localhost:5000/v2/_catalog
```
**Registry in Docker konfigurieren**:
`/etc/docker/daemon.json`:
```json
{
"insecure-registries": ["94.16.110.151:5000"]
}
```
```bash
sudo systemctl restart docker
```
---
## Production Image Build
### Build-Prozess
Das Production Image wird lokal gebaut und dann zur Registry gepusht.
**1. Production Dockerfile** (`Dockerfile.production`):
```dockerfile
# Multi-stage build für optimale Image-Größe
FROM php:8.3-fpm-alpine AS base
# System dependencies
RUN apk add --no-cache \
nginx \
supervisor \
postgresql-dev \
libpq \
&& docker-php-ext-install pdo pdo_pgsql
# PHP Configuration
COPY docker/php/php.production.ini /usr/local/etc/php/conf.d/production.ini
COPY docker/php/opcache.ini /usr/local/etc/php/conf.d/opcache.ini
COPY docker/php/zz-docker.production.conf /usr/local/etc/php-fpm.d/zz-docker.conf
# Nginx Configuration
COPY docker/nginx/nginx.production.conf /etc/nginx/nginx.conf
COPY docker/nginx/default.production.conf /etc/nginx/http.d/default.conf
# Supervisor Configuration
COPY docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# Application Code
WORKDIR /var/www/html
COPY --chown=www-data:www-data . .
# Composer dependencies (Production only)
RUN composer install --no-dev --optimize-autoloader --no-interaction
# NPM build
RUN npm ci && npm run build
# Permissions
RUN chown -R www-data:www-data /var/www/html/storage \
&& chmod -R 775 /var/www/html/storage
# Start Supervisor (manages nginx + php-fpm)
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
```
**2. Build Command**:
```bash
# Im Projekt-Root
docker build \
-f Dockerfile.production \
-t 94.16.110.151:5000/framework:latest \
.
```
**3. Push to Registry**:
```bash
docker push 94.16.110.151:5000/framework:latest
```
**4. Verify Push**:
```bash
curl http://94.16.110.151:5000/v2/framework/tags/list
```
### Wichtige Konfigurationsdateien
#### Supervisor Configuration (`docker/supervisor/supervisord.conf`)
```ini
[supervisord]
nodaemon=true
silent=false
logfile=/dev/null
logfile_maxbytes=0
pidfile=/var/run/supervisord.pid
loglevel=info
[program:php-fpm]
command=php-fpm -F
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autorestart=true
startretries=3
[program:nginx]
command=nginx -g 'daemon off;'
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autorestart=true
startretries=3
depends_on=php-fpm
```
**Wichtige Änderungen**:
- `silent=false` + `logfile=/dev/null`: Supervisor loggt nach stdout/stderr statt Datei
- Grund: Python's logging kann `/dev/stdout` oder `/proc/self/fd/1` nicht im append-mode öffnen
#### PHP-FPM Production Config (`docker/php/zz-docker.production.conf`)
```ini
[www]
user = www-data
group = www-data
listen = 9000
listen.owner = www-data
listen.group = www-data
pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.max_requests = 500
```
**Wichtig**: User/Group explizit auf `www-data` setzen, da Container als root läuft.
---
## Deployment Prozess
### Docker Compose Setup
**Base Configuration** (`docker-compose.yml`):
- Definiert alle Services für Development
- Wird **nicht** auf Production Server deployed
**Production Overrides** (`docker-compose.prod.yml`):
- Merged mit base config
- Production-spezifische Einstellungen
### Production Override Highlights
**Web Service**:
```yaml
web:
image: 94.16.110.151:5000/framework:latest
pull_policy: always # Immer von Registry pullen, nie bauen
entrypoint: [] # Entrypoint von Base-Image clearen
command: ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
user: root # Container läuft als root, PHP-FPM workers als www-data
volumes:
- ./storage/logs:/var/www/html/storage/logs:rw
- ./storage/uploads:/var/www/html/storage/uploads:rw
- ./ssl:/var/www/ssl:ro
environment:
- APP_ENV=production
labels:
com.centurylinklabs.watchtower.enable: "true"
```
**Wichtige Overrides**:
1. `pull_policy: always`: Verhindert lokales Build, zwingt Registry-Pull
2. `entrypoint: []`: Clearen des inherited entrypoint vom Base PHP-Image
3. `command: [...]`: Expliziter Start-Command für Supervisor
4. `user: root`: Nötig für Supervisor, PHP-FPM läuft intern als www-data
### Deployment Steps
**1. Files auf Server kopieren**:
```bash
# Lokale Entwicklungsmaschine (via WireGuard)
scp docker-compose.prod.yml deploy@94.16.110.151:/home/deploy/framework/
scp .env.production deploy@94.16.110.151:/home/deploy/framework/.env
```
**2. Auf Server: Pull und Deploy**:
```bash
# SSH auf Production Server
ssh deploy@94.16.110.151
# In Projekt-Verzeichnis
cd /home/deploy/framework
# Pull latest image
docker pull 94.16.110.151:5000/framework:latest
# Deploy Stack
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
# Check Status
docker-compose -f docker-compose.yml -f docker-compose.prod.yml ps
```
**3. Logs überwachen**:
```bash
# Alle Container
docker-compose -f docker-compose.yml -f docker-compose.prod.yml logs -f
# Spezifischer Container
docker logs -f web
docker logs -f php
```
### Deployment Verification
**Container Health Checks**:
```bash
# Alle Container sollten "healthy" sein
docker-compose ps
# Output sollte zeigen:
# web Up (healthy)
# php Up (healthy)
# db Up (healthy)
# redis Up (healthy)
```
**Supervisor Status (im web container)**:
```bash
docker exec web supervisorctl status
# Output:
# nginx RUNNING pid 7, uptime 0:05:23
# php-fpm RUNNING pid 8, uptime 0:05:23
```
**Nginx & PHP-FPM Processes**:
```bash
docker exec web ps aux | grep -E 'nginx|php-fpm'
# Sollte zeigen:
# root 1 supervisor
# root 7 nginx: master
# www-data nginx: worker (mehrere)
# root 8 php-fpm: master
# www-data php-fpm: pool www (mehrere)
```
**Application Test**:
```bash
# Von lokalem Rechner (via WireGuard)
curl -k -I https://94.16.110.151:8443/
# Erwartete Response:
# HTTP/2 200
# server: nginx
# content-type: text/html
```
---
## Troubleshooting
### Problem 1: Supervisor Log File Permission Denied
**Symptom**:
```
PermissionError: [Errno 13] Permission denied: '/var/log/supervisor/supervisord.log'
```
**Ursache**: Supervisor kann nicht in `/var/log/supervisor/` schreiben, selbst als root.
**Lösung**: `supervisord.conf` ändern:
```ini
silent=false
logfile=/dev/null
logfile_maxbytes=0
```
**Grund**: Python's logging library kann `/dev/stdout` oder `/proc/self/fd/1` nicht im append-mode öffnen. `/dev/null` + `silent=false` macht Supervisor's logging auf stdout/stderr.
### Problem 2: EACCES Errors in Web Container
**Symptom**:
```
CRIT could not write pidfile /var/run/supervisord.pid
spawnerr: unknown error making dispatchers for 'nginx': EACCES
```
**Ursache**: Web container läuft nicht als root, sondern mit inherited user von base config.
**Lösung**: `docker-compose.prod.yml` - `user: root` setzen:
```yaml
web:
user: root
```
### Problem 3: Docker Entrypoint Override funktioniert nicht
**Symptom**: Container command zeigt entrypoint prepended:
```
/usr/local/bin/docker-entrypoint.sh /usr/bin/supervisord -c ...
```
**Ursache**: Base `docker-compose.yml` hat `web` service mit separate build context. Inherited ENTRYPOINT vom Base PHP-Image wird prepended.
**Lösung**: Explizit entrypoint clearen:
```yaml
web:
image: 94.16.110.151:5000/framework:latest
pull_policy: always
entrypoint: [] # WICHTIG: Entrypoint clearen
command: ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
```
### Problem 4: Queue Worker restarts kontinuierlich
**Symptom**:
```
docker ps # zeigt queue-worker als "Restarting"
```
**Ursache**: Base `docker-compose.yml` command sucht `/var/www/html/worker.php` das nicht existiert.
**Temporary Fix**: Service deaktivieren in `docker-compose.prod.yml`:
```yaml
queue-worker:
deploy:
replicas: 0
```
**Proper Fix**: Richtigen Worker-Command konfigurieren:
```yaml
queue-worker:
command: ["php", "/var/www/html/console.php", "queue:work"]
```
### Problem 5: HTTP Port 80 nicht erreichbar
**Symptom**: `curl http://94.16.110.151:8888/` → Connection refused
**Mögliche Ursachen**:
1. Nginx nicht auf Port 80 listening (nur 443)
2. Firewall blockiert Port 8888
3. Intentional (HTTPS-only Configuration)
**Debug**:
```bash
# Im Container checken
docker exec web netstat -tlnp | grep :80
# Nginx config testen
docker exec web nginx -t
# Nginx config anschauen
docker exec web cat /etc/nginx/http.d/default.conf
```
**Fix (falls HTTP→HTTPS Redirect gewünscht)**:
In `docker/nginx/default.production.conf`:
```nginx
server {
listen 80;
server_name _;
return 301 https://$host$request_uri;
}
```
---
## Monitoring
### Prometheus
**Zugang**: http://10.8.0.1:9090 (nur via WireGuard)
**Konfiguration**: `monitoring/prometheus/prometheus.yml`
**Scraped Targets**:
- Framework Application Metrics
- Container Metrics (cAdvisor)
- Node Exporter (Server Metrics)
### Grafana
**Zugang**: http://10.8.0.1:3000 (nur via WireGuard)
**Default Login**:
- User: `admin`
- Password: `${GRAFANA_PASSWORD}` (aus `.env`)
**Dashboards**: `monitoring/grafana/provisioning/dashboards/`
### Portainer
**Zugang**: https://10.8.0.1:9443 (nur via WireGuard)
**Features**:
- Container Management
- Stack Deployment
- Log Viewing
- Resource Usage
### Watchtower Auto-Update
Watchtower überwacht Container mit Label `com.centurylinklabs.watchtower.enable: "true"` und updated sie automatisch bei neuen Images.
**Konfiguration**:
```yaml
watchtower:
environment:
WATCHTOWER_CLEANUP: "true"
WATCHTOWER_POLL_INTERVAL: 300 # 5 Minuten
WATCHTOWER_LABEL_ENABLE: "true"
WATCHTOWER_NOTIFICATIONS: "shoutrrr"
WATCHTOWER_NOTIFICATION_URL: "${WATCHTOWER_NOTIFICATION_URL}"
```
**Monitoren**:
```bash
docker logs -f watchtower
```
---
## Maintenance
### Image Updates
**1. Lokal neues Image bauen**:
```bash
docker build -f Dockerfile.production -t 94.16.110.151:5000/framework:latest .
docker push 94.16.110.151:5000/framework:latest
```
**2. Auf Server**:
```bash
# Watchtower erkennt Update automatisch innerhalb von 5 Minuten
# Oder manuell:
docker-compose -f docker-compose.yml -f docker-compose.prod.yml pull
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
```
### Database Backups
```bash
# Manual Backup
docker exec db pg_dump -U framework_user framework_db > backup_$(date +%Y%m%d_%H%M%S).sql
# Automated (via cron)
0 2 * * * /home/deploy/scripts/backup-database.sh
```
### Log Rotation
Logs in `./storage/logs/` automatisch rotieren:
```bash
# /etc/logrotate.d/framework
/home/deploy/framework/storage/logs/*.log {
daily
rotate 14
compress
delaycompress
notifempty
missingok
create 0640 www-data www-data
}
```
### SSL Certificate Renewal
**Let's Encrypt** (automatisch via certbot):
```bash
certbot renew --deploy-hook "docker exec web nginx -s reload"
```
---
## Security Checklist
- [ ] WireGuard VPN konfiguriert und aktiv
- [ ] Firewall (UFW) konfiguriert und enabled
- [ ] Nur benötigte Ports offen (8443, 51820)
- [ ] Monitoring nur via VPN erreichbar (10.8.0.1:*)
- [ ] SSL/TLS Zertifikate gültig
- [ ] `.env` Secrets nicht in Git committed
- [ ] Database Credentials rotiert
- [ ] Redis Password gesetzt
- [ ] Docker Registry läuft lokal (nicht public)
- [ ] Container laufen mit minimal privileges
- [ ] Watchtower auto-updates aktiviert
- [ ] Backup-Strategie implementiert
- [ ] Log monitoring aktiv
---
## Performance Tuning
### PHP-FPM
`docker/php/zz-docker.production.conf`:
```ini
pm.max_children = 50 # Max. gleichzeitige Requests
pm.start_servers = 10 # Initial workers
pm.min_spare_servers = 5 # Min. idle workers
pm.max_spare_servers = 20 # Max. idle workers
pm.max_requests = 500 # Worker recycling
```
**Tuning basierend auf RAM**:
- 4GB RAM: max_children = 30
- 8GB RAM: max_children = 50
- 16GB RAM: max_children = 100
### OPcache
`docker/php/opcache.ini`:
```ini
opcache.enable=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=10000
opcache.validate_timestamps=0 # Production: keine Timestamp-Checks
opcache.revalidate_freq=0
```
### Nginx
```nginx
worker_processes auto;
worker_connections 1024;
keepalive_timeout 65;
client_max_body_size 20M;
```
---
## Contact & Support
**Production Server**: 94.16.110.151
**VPN Gateway**: 10.8.0.1
**Documentation**: `/home/deploy/framework/docs/`
**Issue Tracker**: [GitHub/GitLab URL]
---
## Change Log
### 2025-10-28 - Initial Production Deployment
**Changes**:
- Supervisor logging: `/dev/null` + `silent=false`
- docker-compose.prod.yml: `user: root` für web, php, queue-worker
- docker-compose.prod.yml: `entrypoint: []` für web service
- docker-compose.prod.yml: `pull_policy: always` für registry images
**Deployed**:
- Image: `94.16.110.151:5000/framework:latest`
- Digest: `sha256:eee1db20b9293cf611f53d01de68e94df1cfb3c748fe967849e080d19b9e4c8b`
**Status**: ✅ Deployment erfolgreich, Container healthy

View File

@@ -0,0 +1,227 @@
# Quick Deploy Guide
Schnellanleitung für Production Deployments.
## Voraussetzungen
- WireGuard VPN aktiv: `sudo wg-quick up wg0-production`
- SSH-Zugang konfiguriert
- Docker Registry läuft auf Production Server
## Deployment in 5 Schritten
### 1. Image bauen und pushen
```bash
# Im Projekt-Root
docker build -f Dockerfile.production -t 94.16.110.151:5000/framework:latest .
docker push 94.16.110.151:5000/framework:latest
```
**Verify Push**:
```bash
curl http://94.16.110.151:5000/v2/framework/tags/list
```
### 2. Config-Files auf Server kopieren
```bash
# Falls docker-compose.prod.yml oder .env geändert wurden
scp docker-compose.prod.yml deploy@94.16.110.151:/home/deploy/framework/
scp .env.production deploy@94.16.110.151:/home/deploy/framework/.env
```
### 3. Auf Server deployen
```bash
ssh deploy@94.16.110.151
cd /home/deploy/framework
# Pull und Deploy
docker-compose -f docker-compose.yml -f docker-compose.prod.yml pull
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
```
### 4. Status checken
```bash
# Container Status
docker-compose -f docker-compose.yml -f docker-compose.prod.yml ps
# Logs anschauen
docker-compose -f docker-compose.yml -f docker-compose.prod.yml logs -f web php
# Supervisor Status (im web container)
docker exec web supervisorctl status
```
### 5. Application testen
```bash
# Von lokaler Maschine (via WireGuard)
curl -k -I https://94.16.110.151:8443/
# Erwartetes Ergebnis:
# HTTP/2 200
# server: nginx
```
## Rollback
Falls Probleme auftreten:
```bash
# Auf Server
cd /home/deploy/framework
# Vorheriges Image ID finden
docker images 94.16.110.151:5000/framework
# Zu spezifischem Image wechseln
docker-compose -f docker-compose.yml -f docker-compose.prod.yml down
docker tag 94.16.110.151:5000/framework@sha256:<old-digest> 94.16.110.151:5000/framework:latest
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
```
## Monitoring URLs
**Zugang nur via WireGuard VPN (10.8.0.1)**:
- Prometheus: http://10.8.0.1:9090
- Grafana: http://10.8.0.1:3000 (admin / $GRAFANA_PASSWORD)
- Portainer: https://10.8.0.1:9443
## Watchtower Auto-Updates
Watchtower überwacht automatisch und updated Container mit neuem Image (alle 5 Minuten).
**Status checken**:
```bash
docker logs watchtower
```
**Manuell triggern**:
```bash
# Watchtower neu starten (triggert sofortigen Check)
docker restart watchtower
```
## Troubleshooting
### Container nicht healthy
```bash
# Logs anschauen
docker logs web
docker logs php
# Im Container debuggen
docker exec -it web sh
docker exec -it php sh
# Supervisor Status
docker exec web supervisorctl status
# Nginx/PHP-FPM Prozesse
docker exec web ps aux | grep -E 'nginx|php-fpm'
```
### Database Connection Issues
```bash
# PostgreSQL Connection testen
docker exec php php -r "new PDO('pgsql:host=db;dbname=framework_db', 'framework_user', 'password');"
# Database Logs
docker logs db
# In Database connecten
docker exec -it db psql -U framework_user -d framework_db
```
### Redis Connection Issues
```bash
# Redis Connection testen
docker exec php php -r "var_dump((new Redis())->connect('redis', 6379));"
# Redis Logs
docker logs redis
# Redis CLI
docker exec -it redis redis-cli
```
## Maintenance Commands
### Database Backup
```bash
# Manual Backup
docker exec db pg_dump -U framework_user framework_db > backup_$(date +%Y%m%d_%H%M%S).sql
```
### Logs Cleanup
```bash
# Storage Logs leeren (auf Server)
docker exec web sh -c 'rm -rf /var/www/html/storage/logs/*.log'
# Docker Logs cleanup
docker system prune -f
docker volume prune -f
```
### Image Cleanup
```bash
# Alte Images entfernen
docker image prune -a -f
# Nur untagged images
docker image prune -f
```
## Performance Check
```bash
# Container Resource Usage
docker stats
# PHP-FPM Status
docker exec web curl http://localhost/php-fpm-status
# Nginx Status
docker exec web curl http://localhost/nginx-status
# Database Connections
docker exec db psql -U framework_user -d framework_db -c "SELECT count(*) FROM pg_stat_activity;"
```
## SSL Certificate Renewal
```bash
# Let's Encrypt Renewal (auf Server als root)
certbot renew
docker exec web nginx -s reload
```
## Nützliche Aliases
Füge zu `~/.bashrc` auf Production Server hinzu:
```bash
alias dc='docker-compose -f docker-compose.yml -f docker-compose.prod.yml'
alias dcup='dc up -d'
alias dcdown='dc down'
alias dcps='dc ps'
alias dclogs='dc logs -f'
alias dcrestart='dc restart'
```
Dann kannst du einfach verwenden:
```bash
dcup # Deploy
dcps # Status
dclogs # Logs anschauen
```

View File

@@ -0,0 +1,581 @@
# Production Deployment Troubleshooting Checklist
Systematische Problemlösung für häufige Deployment-Issues.
## Issue 1: Supervisor Log File Permission Denied
### Symptom
```
PermissionError: [Errno 13] Permission denied: '/var/log/supervisor/supervisord.log'
```
Container startet nicht, Supervisor kann Logfile nicht schreiben.
### Diagnose
```bash
docker logs web # Zeigt Permission Error
docker exec web ls -la /var/log/supervisor/ # Directory existiert nicht oder keine Permissions
```
### Root Cause
- Supervisor versucht in `/var/log/supervisor/supervisord.log` zu schreiben
- Directory existiert nicht oder keine Write-Permissions
- Auch als root problematisch in containerisierter Umgebung
### Lösung 1 (FUNKTIONIERT NICHT)
**Versuch**: `/proc/self/fd/1` verwenden
`docker/supervisor/supervisord.conf`:
```ini
logfile=/proc/self/fd/1
```
**Fehler**: `PermissionError: [Errno 13] Permission denied: '/proc/self/fd/1'`
**Grund**: Python's logging library (verwendet von Supervisor) kann `/proc/self/fd/1` oder `/dev/stdout` nicht im append-mode öffnen.
### Lösung 2 (ERFOLGREICH)
**Fix**: `/dev/null` mit `silent=false`
`docker/supervisor/supervisord.conf`:
```ini
[supervisord]
nodaemon=true
silent=false # WICHTIG: Logging trotz /dev/null
logfile=/dev/null
logfile_maxbytes=0
pidfile=/var/run/supervisord.pid
loglevel=info
```
**Warum funktioniert das?**
- `logfile=/dev/null`: Kein File-Logging
- `silent=false`: Supervisor loggt nach stdout/stderr
- Logs erscheinen in `docker logs web`
### Verification
```bash
docker logs web
# Output:
# 2025-10-28 16:29:59,976 INFO supervisord started with pid 1
# 2025-10-28 16:30:00,980 INFO spawned: 'nginx' with pid 7
# 2025-10-28 16:30:00,982 INFO spawned: 'php-fpm' with pid 8
# 2025-10-28 16:30:02,077 INFO success: nginx entered RUNNING state
# 2025-10-28 16:30:02,077 INFO success: php-fpm entered RUNNING state
```
### Related Files
- `docker/supervisor/supervisord.conf`
- `Dockerfile.production` (COPY supervisord.conf)
---
## Issue 2: Web Container EACCES Errors
### Symptom
```
2025-10-28 16:16:52,152 CRIT could not write pidfile /var/run/supervisord.pid
2025-10-28 16:16:53,154 INFO spawnerr: unknown error making dispatchers for 'nginx': EACCES
2025-10-28 16:16:53,154 INFO spawnerr: unknown error making dispatchers for 'php-fpm': EACCES
```
### Diagnose
```bash
# Container User checken
docker exec web whoami
# Falls nicht "root", dann ist das der Issue
# Docker Compose Config checken
docker inspect web | grep -i user
# Zeigt inherited user von base config
```
### Root Cause
- `web` service in `docker-compose.prod.yml` hat **kein** `user: root` gesetzt
- Inherited `user: 1000:1000` oder `user: www-data` von base `docker-compose.yml`
- Supervisor benötigt root um nginx/php-fpm master processes zu starten
### Lösung
**Fix**: `user: root` explizit setzen
`docker-compose.prod.yml`:
```yaml
web:
image: 94.16.110.151:5000/framework:latest
user: root # ← HINZUFÜGEN
# ... rest der config
```
Auch für `php` und `queue-worker` services hinzufügen:
```yaml
php:
image: 94.16.110.151:5000/framework:latest
user: root # ← HINZUFÜGEN
queue-worker:
image: 94.16.110.151:5000/framework:latest
user: root # ← HINZUFÜGEN
```
### Warum user: root?
- **Container läuft als root**: Supervisor master process
- **Nginx master**: root (worker processes als www-data via nginx.conf)
- **PHP-FPM master**: root (pool workers als www-data via php-fpm.conf)
`docker/php/zz-docker.production.conf`:
```ini
[www]
user = www-data # ← Worker processes laufen als www-data
group = www-data
```
### Verification
```bash
docker exec web whoami
# root
docker exec web ps aux | grep -E 'nginx|php-fpm'
# root 1 supervisord
# root 7 nginx: master process
# www-data 10 nginx: worker process
# root 8 php-fpm: master process
# www-data 11 php-fpm: pool www
```
### Related Files
- `docker-compose.prod.yml` (web, php, queue-worker services)
- `docker/php/zz-docker.production.conf`
- `docker/nginx/nginx.production.conf`
---
## Issue 3: Docker Entrypoint Override funktioniert nicht
### Symptom
Container command zeigt Entrypoint prepended:
```bash
docker ps
# COMMAND: "/usr/local/bin/docker-entrypoint.sh /usr/bin/supervisord -c ..."
```
Supervisor wird nicht direkt gestartet, sondern durch einen wrapper script.
### Diagnose
```bash
# Container Command checken
docker inspect web --format='{{.Config.Entrypoint}}'
# [/usr/local/bin/docker-entrypoint.sh]
docker inspect web --format='{{.Config.Cmd}}'
# [/usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf]
```
### Root Cause
1. Base `docker-compose.yml` hat `web` service mit separate build:
```yaml
web:
build:
context: docker/nginx
dockerfile: Dockerfile
```
2. Production override setzt `image:` aber cleared **nicht** den inherited ENTRYPOINT:
```yaml
web:
image: 94.16.110.151:5000/framework:latest
command: ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
```
3. Base PHP image hat ENTRYPOINT der prepended wird
4. Docker Compose merge: ENTRYPOINT + CMD = final command
### Lösung - Iteration 1 (FUNKTIONIERT NICHT)
❌ **Versuch**: Nur `command:` setzen
`docker-compose.prod.yml`:
```yaml
web:
image: 94.16.110.151:5000/framework:latest
command: ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
```
**Result**: Entrypoint wird trotzdem prepended
### Lösung - Iteration 2 (FUNKTIONIERT NICHT)
❌ **Versuch**: `pull_policy: always` hinzufügen
`docker-compose.prod.yml`:
```yaml
web:
image: 94.16.110.151:5000/framework:latest
pull_policy: always # Force registry pull
command: ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
```
**Result**: Image wird von Registry gepullt, aber Entrypoint wird trotzdem prepended
### Lösung - Iteration 3 (ERFOLGREICH)
✅ **Fix**: `entrypoint: []` explizit clearen
`docker-compose.prod.yml`:
```yaml
web:
image: 94.16.110.151:5000/framework:latest
pull_policy: always # Always pull from registry, never build
entrypoint: [] # ← WICHTIG: Entrypoint clearen
command: ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
user: root
```
**Warum `entrypoint: []`?**
- Leeres Array cleared den inherited entrypoint komplett
- `command:` wird dann direkt als PID 1 gestartet
- Keine wrapper scripts, keine indirection
### Verification
```bash
docker inspect web --format='{{.Config.Entrypoint}}'
# [] ← Leer!
docker inspect web --format='{{.Config.Cmd}}'
# [/usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf]
docker exec web ps aux
# PID 1: /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
# Kein entrypoint wrapper!
```
### Related Files
- `docker-compose.prod.yml` (web service)
### Docker Compose Override Rules
```
Base Config + Override = Final Config
Base:
web:
build: docker/nginx
→ inherited ENTRYPOINT from base image
Override (insufficient):
web:
image: 94.16.110.151:5000/framework:latest
command: [...]
→ ENTRYPOINT still prepended to command
Override (correct):
web:
image: 94.16.110.151:5000/framework:latest
entrypoint: [] ← Clears inherited entrypoint
command: [...] ← Runs directly as PID 1
```
---
## Issue 4: Queue Worker Container Restarts
### Symptom
```bash
docker ps
# queue-worker Restarting (1) 5 seconds ago
```
Container restart loop, nie healthy.
### Diagnose
```bash
docker logs queue-worker
# Error: /var/www/html/worker.php not found
# oder
# php: command not found
```
### Root Cause
Base `docker-compose.yml` hat Queue Worker Command für Development:
```yaml
queue-worker:
command: ["php", "/var/www/html/worker.php"]
```
`worker.php` existiert nicht im Production Image.
### Lösung - Option 1: Service deaktivieren
✅ **Quick Fix**: Queue Worker deaktivieren
`docker-compose.prod.yml`:
```yaml
queue-worker:
deploy:
replicas: 0 # Disable service
```
### Lösung - Option 2: Richtigen Command setzen
✅ **Proper Fix**: Console Command verwenden
`docker-compose.prod.yml`:
```yaml
queue-worker:
image: 94.16.110.151:5000/framework:latest
user: root
command: ["php", "/var/www/html/console.php", "queue:work"]
# oder für Supervisor-managed:
# entrypoint: []
# command: ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/queue-worker-supervisord.conf"]
```
### Verification
```bash
docker logs queue-worker
# [timestamp] INFO Queue worker started
# [timestamp] INFO Processing job: ...
```
### Related Files
- `docker-compose.yml` (base queue-worker definition)
- `docker-compose.prod.yml` (production override)
- `console.php` (framework console application)
---
## Issue 5: HTTP Port 80 nicht erreichbar
### Symptom
```bash
curl http://94.16.110.151:8888/
# curl: (7) Failed to connect to 94.16.110.151 port 8888: Connection refused
docker exec web curl http://localhost/
# curl: (7) Failed to connect to localhost port 80: Connection refused
```
### Diagnose
```bash
# Nginx listening ports checken
docker exec web netstat -tlnp | grep nginx
# Zeigt nur: 0.0.0.0:443
# Nginx Config checken
docker exec web cat /etc/nginx/http.d/default.conf
# Kein "listen 80;" block
```
### Root Cause - Option 1: Intentional HTTPS-only
Möglicherweise ist HTTP absichtlich disabled (Security Best Practice).
### Root Cause - Option 2: Missing HTTP Block
Nginx config hat keinen HTTP listener, nur HTTPS.
### Lösung - HTTP→HTTPS Redirect hinzufügen
✅ **Fix**: HTTP Redirect konfigurieren
`docker/nginx/default.production.conf`:
```nginx
# HTTP → HTTPS Redirect
server {
listen 80;
server_name _;
location / {
return 301 https://$host$request_uri;
}
}
# HTTPS Server
server {
listen 443 ssl http2;
server_name _;
ssl_certificate /var/www/ssl/cert.pem;
ssl_certificate_key /var/www/ssl/key.pem;
root /var/www/html/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass php:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
```
### Verification
```bash
curl -I http://94.16.110.151:8888/
# HTTP/1.1 301 Moved Permanently
# Location: https://94.16.110.151:8888/
curl -k -I https://94.16.110.151:8443/
# HTTP/2 200
# server: nginx
```
### Related Files
- `docker/nginx/default.production.conf`
- `Dockerfile.production` (COPY nginx config)
---
## General Debugging Commands
### Container Inspection
```bash
# Alle Container Status
docker-compose -f docker-compose.yml -f docker-compose.prod.yml ps
# Container Details
docker inspect web
# Container Logs
docker logs -f web
docker logs --tail 100 web
# Inside Container
docker exec -it web sh
docker exec -it php sh
```
### Supervisor Debugging
```bash
# Supervisor Status
docker exec web supervisorctl status
# Supervisor Logs
docker exec web tail -f /dev/null # Logs gehen nach stdout/stderr
# Supervisor Config testen
docker exec web supervisord -c /etc/supervisor/conf.d/supervisord.conf -n
```
### Nginx Debugging
```bash
# Nginx Config testen
docker exec web nginx -t
# Nginx reload
docker exec web nginx -s reload
# Nginx listening ports
docker exec web netstat -tlnp | grep nginx
# Nginx processes
docker exec web ps aux | grep nginx
```
### PHP-FPM Debugging
```bash
# PHP-FPM Status
docker exec web curl http://localhost/php-fpm-status
# PHP-FPM Config testen
docker exec web php-fpm -t
# PHP-FPM processes
docker exec web ps aux | grep php-fpm
# PHP Version
docker exec web php -v
# PHP Modules
docker exec web php -m
```
### Network Debugging
```bash
# Port listening
docker exec web netstat -tlnp
# DNS resolution
docker exec web nslookup db
docker exec web nslookup redis
# Network connectivity
docker exec web ping db
docker exec web ping redis
# HTTP request
docker exec web curl http://localhost/
```
### Database Debugging
```bash
# PostgreSQL Connection
docker exec php php -r "new PDO('pgsql:host=db;dbname=framework_db', 'framework_user', 'password');"
# Database Logs
docker logs db
# Connect to DB
docker exec -it db psql -U framework_user -d framework_db
# Check connections
docker exec db psql -U framework_user -d framework_db -c "SELECT count(*) FROM pg_stat_activity;"
```
### Performance Monitoring
```bash
# Container Resource Usage
docker stats
# Disk Usage
docker system df
# Image Sizes
docker images
# Volume Sizes
docker system df -v
```
---
## Checklist für erfolgreichen Deploy
### Pre-Deployment
- [ ] Image gebaut: `docker build -f Dockerfile.production -t 94.16.110.151:5000/framework:latest .`
- [ ] Image gepusht: `docker push 94.16.110.151:5000/framework:latest`
- [ ] Registry verfügbar: `curl http://94.16.110.151:5000/v2/_catalog`
- [ ] WireGuard VPN aktiv: `wg show`
- [ ] `.env.production` auf Server aktuell
- [ ] `docker-compose.prod.yml` auf Server aktuell
### Deployment
- [ ] SSH auf Server: `ssh deploy@94.16.110.151`
- [ ] Image pullen: `docker-compose -f docker-compose.yml -f docker-compose.prod.yml pull`
- [ ] Stack starten: `docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d`
### Post-Deployment Verification
- [ ] Container laufen: `docker-compose ps` zeigt alle "Up (healthy)"
- [ ] Supervisor Status: `docker exec web supervisorctl status` zeigt nginx/php-fpm RUNNING
- [ ] Nginx lauscht: `docker exec web netstat -tlnp | grep :443`
- [ ] PHP-FPM lauscht: `docker exec web netstat -tlnp | grep :9000`
- [ ] Application erreichbar: `curl -k -I https://94.16.110.151:8443/` → HTTP/2 200
- [ ] Database erreichbar: `docker exec php php -r "new PDO(...);"`
- [ ] Redis erreichbar: `docker exec php php -r "new Redis()->connect('redis', 6379);"`
- [ ] Logs sauber: `docker logs web` zeigt keine Errors
### Monitoring
- [ ] Prometheus: http://10.8.0.1:9090 erreichbar
- [ ] Grafana: http://10.8.0.1:3000 erreichbar
- [ ] Portainer: https://10.8.0.1:9443 erreichbar
- [ ] Watchtower aktiv: `docker logs watchtower` zeigt Checks
---
## Quick Reference
### Häufigste Fehlerursachen
1. **Supervisor Logging**: Verwende `logfile=/dev/null` + `silent=false`
2. **User Permissions**: Setze `user: root` in docker-compose.prod.yml
3. **Entrypoint Override**: Setze `entrypoint: []` um inherited entrypoint zu clearen
4. **Pull Policy**: Verwende `pull_policy: always` um registry image zu forcen
### Wichtigste Config-Änderungen
- `docker/supervisor/supervisord.conf`: `logfile=/dev/null`, `silent=false`
- `docker-compose.prod.yml`: `user: root`, `entrypoint: []`, `pull_policy: always`
- `docker/php/zz-docker.production.conf`: `user = www-data`, `group = www-data`

View File

@@ -0,0 +1,609 @@
# Poll System Usage Examples
Das Poll-System ermöglicht es, beliebige Methoden als pollable zu markieren und automatisch in konfigurierbaren Intervallen auszuführen.
## Zwei Ansätze: Attribute vs. Closures
Das Framework bietet **zwei komplementäre Ansätze** für Polling:
1. **Attribute-basiert (#[Poll])**: Für langlebige, wiederverwendbare Poll-Methoden in Klassen
2. **Closure-basiert (PollableClosure)**: Für Template-spezifische, dynamische Polls
---
## Attribute-Based Polling
### 1. Methode mit #[Poll] Attribut markieren
```php
use App\Framework\LiveComponents\Attributes\Poll;
final readonly class NotificationComponent
{
public function __construct(
private NotificationService $notificationService
) {}
/**
* Poll for new notifications every second
*/
#[Poll(interval: 1000)]
public function checkNotifications(): array
{
return [
'count' => $this->notificationService->getUnreadCount(),
'latest' => $this->notificationService->getLatest(5)
];
}
}
```
### 2. Poll manuell ausführen
```php
use App\Framework\LiveComponents\Polling\PollExecutor;
$executor = $container->get(PollExecutor::class);
// Execute specific poll
$result = $executor->executePoll(
NotificationComponent::class,
'checkNotifications'
);
if ($result->isSuccess()) {
$data = $result->data;
echo "Unread notifications: {$data['count']}\n";
}
```
### Poll mit Event Dispatch
```php
use App\Framework\LiveComponents\Attributes\Poll;
final readonly class ServerHealthComponent
{
#[Poll(
interval: 5000, // Every 5 seconds
event: 'server.health.checked' // Dispatch event
)]
public function checkHealth(): array
{
return [
'cpu' => $this->metrics->getCpuUsage(),
'memory' => $this->metrics->getMemoryUsage(),
'disk' => $this->metrics->getDiskUsage()
];
}
}
```
**Event Handler**:
```php
use App\Framework\Core\Events\OnEvent;
use App\Framework\LiveComponents\Polling\PollExecutedEvent;
final readonly class HealthMonitor
{
#[OnEvent(PollExecutedEvent::class)]
public function onHealthChecked(PollExecutedEvent $event): void
{
if ($event->poll->event === 'server.health.checked') {
$health = $event->result;
if ($health['cpu'] > 80) {
$this->alerting->sendAlert('High CPU usage detected');
}
}
}
}
```
### Poll mit stopOnError
```php
final readonly class CriticalMonitor
{
#[Poll(
interval: 10000,
stopOnError: true // Stop polling if error occurs
)]
public function checkCriticalService(): array
{
// Will stop polling if this throws
return $this->criticalService->healthCheck();
}
}
```
---
## Closure-Based Polling
**Ideal für Template-spezifische, dynamische Polls ohne dedizierte Klassen.**
### Basic Usage in Templates
```php
use App\Framework\LiveComponents\Polling\PollableClosure;
// In Controller
final readonly class DashboardController
{
#[Route('/dashboard', Method::GET)]
public function dashboard(): ViewResult
{
return new ViewResult('dashboard', [
// ✅ PREFERRED: First-class callable syntax (...)
'notifications' => new PollableClosure(
closure: $this->notificationService->getUnread(...),
interval: 1000
),
'serverStatus' => new PollableClosure(
closure: $this->healthService->getStatus(...),
interval: 5000
)
]);
}
}
```
**Warum `...` bevorzugen?**
- ✅ Kürzer und lesbarer
- ✅ Bessere Performance (keine zusätzliche Closure)
- ✅ IDE kann Methodensignatur besser analysieren
- ✅ PHP 8.1+ Standard für first-class callables
### Advanced Closure Examples
**Mit Parametern (closure wrapper notwendig)**:
```php
// ✅ First-class callable mit Parametern
'userActivity' => new PollableClosure(
closure: fn() => $this->activityService->getRecent($userId, 10),
interval: 2000
)
// Wenn Methode Parameter braucht, MUSS closure wrapper verwendet werden
```
**Mit Event Dispatch**:
```php
'systemHealth' => new PollableClosure(
closure: $this->monitoring->getHealth(...),
interval: 10000,
event: 'system.health.polled' // Dispatches event
)
```
**Mit stopOnError**:
```php
'criticalData' => new PollableClosure(
closure: $this->dataService->getCritical(...),
interval: 1000,
stopOnError: true // Stops on first error
)
```
**Disabled by Default**:
```php
'debugInfo' => new PollableClosure(
closure: $this->debug->getInfo(...),
interval: 500,
enabled: false // Disabled until explicitly enabled
)
```
### Automatic Template Integration
**Der `PollableClosureTransformer` macht Closures automatisch pollbar:**
```php
// Template: dashboard.view.php
<div class="notifications">
<h3>Notifications ({{ $notifications['count'] }})</h3>
<!-- Data wird initial gerendert -->
<!-- JavaScript polled automatisch /poll/{pollId} -->
</div>
<div class="status">
<p>Server Status: {{ $serverStatus['status'] }}</p>
<!-- Wird automatisch alle 5s aktualisiert -->
</div>
```
**Generated JavaScript (automatisch)**:
```javascript
// Automatisch generiert vom PollableClosureTransformer
(function() {
function poll_notifications() {
fetch('/poll/{pollId}', {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
}
})
.then(response => response.json())
.then(data => {
// Update DOM elements with data-poll="notifications"
const elements = document.querySelectorAll('[data-poll="notifications"]');
elements.forEach(el => el.textContent = data);
});
}
// Start polling every 1000ms
setInterval(poll_notifications, 1000);
poll_notifications(); // Initial call
})();
```
### Manual Closure Registration
```php
use App\Framework\LiveComponents\Polling\PollableClosureRegistry;
use App\Framework\LiveComponents\ValueObjects\ComponentId;
$registry = $container->get(PollableClosureRegistry::class);
// Register closure for specific component
$registration = $registry->register(
templateKey: 'notifications',
closure: new PollableClosure(
closure: $this->notificationService->getUnread(...),
interval: 1000
),
componentId: ComponentId::fromString('dashboard-component')
);
// Get endpoint URL
$endpoint = $registration->getEndpoint(); // "/poll/{pollId}"
// Execute closure
$result = $registry->execute($registration->pollId);
```
---
## Unified Poll Controller
**Beide Poll-Typen nutzen denselben Endpoint:**
```
GET /poll/{pollId}
```
**PollId Format unterscheidet Typen:**
- `closure.*` → Closure-based Poll
- `attribute.*` → Attribute-based Poll
**Beispiel Request**:
```bash
# Closure-based poll
curl https://localhost/poll/closure.notifications.abc123
# Attribute-based poll
curl https://localhost/poll/attribute.App\\NotificationComponent::checkNotifications
```
**Response Format**:
```json
{
"count": 5,
"latest": [
{"id": 1, "message": "New order"},
{"id": 2, "message": "Payment received"}
]
}
```
---
## Poll Discovery
### List all registered polls
```php
use App\Framework\LiveComponents\Polling\PollService;
$pollService = $container->get(PollService::class);
// Get all attribute-based polls
$allPolls = $pollService->getAllPolls();
foreach ($allPolls as $item) {
$poll = $item['poll'];
$discovered = $item['discovered'];
echo sprintf(
"Poll: %s::%s - Interval: %dms - Enabled: %s\n",
$discovered->className->getFullyQualified(),
$discovered->methodName->toString(),
$poll->interval,
$poll->enabled ? 'Yes' : 'No'
);
}
```
### Get closure-based polls
```php
use App\Framework\LiveComponents\Polling\PollableClosureRegistry;
$registry = $container->get(PollableClosureRegistry::class);
// Get all registered closures
$registrations = $registry->getAll();
foreach ($registrations as $registration) {
echo sprintf(
"Closure Poll: %s - Interval: %dms\n",
$registration->templateKey,
$registration->closure->interval
);
}
// Get enabled closures only
$enabled = $registry->getEnabled();
```
---
## Execution Patterns
### Execute all enabled attribute polls
```php
$executor = $container->get(PollExecutor::class);
// Execute all enabled polls
$results = $executor->executeAllEnabledPolls();
foreach ($results as $result) {
if ($result->isSuccess()) {
echo "{$result->getPollId()} succeeded in {$result->executionTime->toMilliseconds()}ms\n";
} else {
echo "{$result->getPollId()} failed: {$result->error?->getMessage()}\n";
}
}
```
### Execute closure polls
```php
$registry = $container->get(PollableClosureRegistry::class);
// Execute specific closure by PollId
$pollId = PollId::fromString('closure.notifications.abc123');
$result = $registry->execute($pollId);
// Process result
echo json_encode($result);
```
---
## Integration with Scheduler
### Schedule attribute polls
```php
use App\Framework\Scheduler\Services\SchedulerService;
use App\Framework\Scheduler\Schedules\IntervalSchedule;
use App\Framework\Core\ValueObjects\Duration;
$scheduler = $container->get(SchedulerService::class);
$executor = $container->get(PollExecutor::class);
// Schedule poll execution
$scheduler->schedule(
'execute-all-polls',
IntervalSchedule::every(Duration::fromSeconds(1)),
fn() => ['executed' => count($executor->executeAllEnabledPolls())]
);
```
### Schedule closure polls
```php
$scheduler->schedule(
'execute-closure-polls',
IntervalSchedule::every(Duration::fromSeconds(1)),
function() use ($registry) {
$enabled = $registry->getEnabled();
$results = [];
foreach ($enabled as $registration) {
try {
$results[] = $registry->execute($registration->pollId);
} catch (\Throwable $e) {
// Handle error
}
}
return ['executed' => count($results)];
}
);
```
---
## Performance Monitoring
### Get execution statistics
```php
$stats = $executor->getExecutionStats();
echo "Total polls: {$stats['total_polls']}\n";
echo "Enabled polls: {$stats['enabled_polls']}\n";
echo "\nPolls by interval:\n";
foreach ($stats['polls_by_interval'] as $interval => $count) {
echo " {$interval}ms: {$count} polls\n";
}
```
### Monitor poll performance
```php
use App\Framework\Core\Events\OnEvent;
final readonly class PollPerformanceMonitor
{
#[OnEvent(PollExecutedEvent::class)]
public function onPollExecuted(PollExecutedEvent $event): void
{
$duration = $event->executionTime;
// Log slow polls
if ($duration->toMilliseconds() > 100) {
$this->logger->warning('Slow poll detected', [
'poll' => $event->getPollId(),
'duration_ms' => $duration->toMilliseconds()
]);
}
// Track metrics
$this->metrics->timing(
"poll.{$event->methodName->toString()}.duration",
$duration->toMilliseconds()
);
}
}
```
---
## Best Practices
### Wann Attribute, wann Closures?
**Attribute (#[Poll]) verwenden für:**
- ✅ Wiederverwendbare Business-Logic-Polls
- ✅ Polls, die von mehreren Templates genutzt werden
- ✅ Komplexe Polls mit Event-Dispatch
- ✅ Service-Layer Polls (Health Checks, Monitoring)
**Closures (PollableClosure) verwenden für:**
- ✅ Template-spezifische Polls
- ✅ Einmalige, nicht wiederverwendbare Polls
- ✅ Dynamische Polls mit Template-Kontext
- ✅ Quick Prototyping ohne dedizierte Klassen
### Interval Guidelines
- **Fast (100-1000ms)**: Critical real-time data, notifications
- **Medium (1000-10000ms)**: Health checks, status updates
- **Slow (10000-60000ms)**: Background sync, cache refresh
- **Very Slow (>60000ms)**: Analytics, cleanup tasks
### Method Requirements
- Must be `public`
- Return type should be serializable (`array`, `SerializableState`, scalars)
- Should be idempotent (safe to call multiple times)
- Should not have side effects that break on repeated execution
### Error Handling
- Use `stopOnError: true` for critical polls
- Implement proper logging in poll methods
- Return meaningful data structures
- Handle exceptions gracefully
### Performance
- Keep poll methods fast (<100ms ideal)
- Avoid heavy database queries
- Use caching where appropriate
- Monitor execution times via events
### First-Class Callable Syntax (...)
**✅ IMMER bevorzugen wenn möglich:**
```php
// ✅ BEST: First-class callable
new PollableClosure(
closure: $this->service->getData(...),
interval: 1000
)
// ⚠️ OK: Wenn Parameter nötig
new PollableClosure(
closure: fn() => $this->service->getData($userId),
interval: 1000
)
// ❌ AVOID: Unnötiger fn() wrapper
new PollableClosure(
closure: fn() => $this->service->getData(), // AVOID!
interval: 1000
)
```
**Vorteile von `...` Syntax:**
- Kürzer und lesbarer
- Keine zusätzliche Closure-Allokation
- Bessere IDE-Unterstützung
- Performance-Vorteil (minimal aber vorhanden)
### Testing
```php
it('executes poll and returns expected data', function () {
$component = new NotificationComponent($this->notificationService);
$result = $component->checkNotifications();
expect($result)->toHaveKey('count');
expect($result)->toHaveKey('latest');
});
it('poll attribute is discoverable', function () {
$pollService = $this->container->get(PollService::class);
expect($pollService->isPollable(
NotificationComponent::class,
'checkNotifications'
))->toBeTrue();
});
it('closure poll executes correctly', function () {
$closure = new PollableClosure(
closure: fn() => ['status' => 'ok'],
interval: 1000
);
$result = $closure->execute();
expect($result)->toHaveKey('status');
expect($result['status'])->toBe('ok');
});
it('closure poll is registered correctly', function () {
$registry = $this->container->get(PollableClosureRegistry::class);
$registration = $registry->register(
'test',
new PollableClosure(fn() => [], 1000)
);
expect($registry->has($registration->pollId))->toBeTrue();
});
```
---
## Framework Compliance
**Readonly Classes**: Poll, PollableClosure, PollResult, Events are all readonly
**Value Objects**: Uses ClassName, MethodName, Duration, ComponentId, PollId VOs
**Immutability**: All poll-related objects are immutable
**Discovery Integration**: Automatic via DiscoveryRegistry (attribute-based)
**Event System**: Full integration with EventDispatcher
**Type Safety**: Strong typing throughout
**First-Class Callables**: Supports PHP 8.1+ `...` syntax
**Unified API**: Single endpoint for both poll types (`/poll/{pollId}`)

View File

@@ -128,6 +128,7 @@ interface ModelRegistry
```
**Verwendung**:
```php
use App\Framework\MachineLearning\ModelManagement\ModelRegistry;
use App\Framework\MachineLearning\ModelManagement\ValueObjects\ModelMetadata;
@@ -343,6 +344,7 @@ $comparison = $performanceMonitor->compareVersions(
```
**Verwendung**:
```php
use App\Framework\MachineLearning\ModelManagement\ModelPerformanceMonitor;
use App\Framework\Core\ValueObjects\Version;