feat: CI/CD pipeline setup complete - Ansible playbooks updated, secrets configured, workflow ready
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -464,23 +464,937 @@ $schema->table('users', function (Blueprint $table) {
|
||||
|
||||
## EntityManager Usage
|
||||
|
||||
TODO: Document EntityManager and UnitOfWork pattern
|
||||
### Overview
|
||||
|
||||
Der EntityManager ist das zentrale Interface für alle Datenbank-Operationen. Er kombiniert mehrere Patterns:
|
||||
- **Unit of Work**: Change Tracking und Transaction Management
|
||||
- **Identity Map**: Vermeidung von Duplikaten
|
||||
- **Lazy Loading**: Performance-Optimierung durch verzögertes Laden
|
||||
- **Batch Loading**: N+1 Query Prevention
|
||||
|
||||
### Core Features
|
||||
|
||||
**Service Classes** (interne Organisation):
|
||||
- `EntityFinder`: Entity-Suche und Lazy Loading
|
||||
- `EntityPersister`: Insert, Update, Delete Operationen
|
||||
- `EntityQueryManager`: QueryBuilder Integration
|
||||
- `EntityUtilities`: Hilfsmethoden und Profiling
|
||||
|
||||
### Basic Operations
|
||||
|
||||
#### Finding Entities
|
||||
|
||||
```php
|
||||
use App\Framework\Database\EntityManager;
|
||||
|
||||
final readonly class UserService
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManager $entityManager
|
||||
) {}
|
||||
|
||||
public function getUserById(UserId $id): ?User
|
||||
{
|
||||
// Lazy Loading (Standard)
|
||||
return $this->entityManager->find(User::class, $id->value);
|
||||
}
|
||||
|
||||
public function getUserByIdEager(UserId $id): ?User
|
||||
{
|
||||
// Eager Loading - lädt Relations sofort
|
||||
return $this->entityManager->findEager(User::class, $id->value);
|
||||
}
|
||||
|
||||
public function getAllUsers(): array
|
||||
{
|
||||
return $this->entityManager->findAll(User::class);
|
||||
}
|
||||
|
||||
public function findUserByEmail(Email $email): ?User
|
||||
{
|
||||
return $this->entityManager->findOneBy(
|
||||
User::class,
|
||||
['email' => $email->value]
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Saving Entities
|
||||
|
||||
```php
|
||||
public function createUser(CreateUserCommand $command): User
|
||||
{
|
||||
$user = User::create(
|
||||
id: $this->entityManager->generateId(),
|
||||
email: $command->email,
|
||||
name: $command->name
|
||||
);
|
||||
|
||||
// save() erkennt automatisch INSERT vs UPDATE
|
||||
return $this->entityManager->save($user);
|
||||
}
|
||||
|
||||
public function updateUser(User $user, UpdateUserCommand $command): User
|
||||
{
|
||||
// Explizites UPDATE
|
||||
$user->updateProfile($command->name);
|
||||
return $this->entityManager->update($user);
|
||||
}
|
||||
```
|
||||
|
||||
#### Batch Operations
|
||||
|
||||
```php
|
||||
public function createMultipleUsers(array $commands): array
|
||||
{
|
||||
$users = [];
|
||||
foreach ($commands as $command) {
|
||||
$users[] = User::create(
|
||||
id: $this->entityManager->generateId(),
|
||||
email: $command->email,
|
||||
name: $command->name
|
||||
);
|
||||
}
|
||||
|
||||
// Batch-Insert für bessere Performance
|
||||
return $this->entityManager->saveAll(...$users);
|
||||
}
|
||||
```
|
||||
|
||||
#### Deleting Entities
|
||||
|
||||
```php
|
||||
public function deleteUser(User $user): void
|
||||
{
|
||||
$this->entityManager->delete($user);
|
||||
}
|
||||
```
|
||||
|
||||
### N+1 Query Prevention
|
||||
|
||||
**Problem**: Lazy Loading führt zu N+1 Queries
|
||||
|
||||
```php
|
||||
// ❌ N+1 Problem
|
||||
$users = $this->entityManager->findAll(User::class);
|
||||
|
||||
foreach ($users as $user) {
|
||||
// Jede Iteration führt eine separate Query aus (N+1)
|
||||
echo $user->getProfile()->bio;
|
||||
}
|
||||
```
|
||||
|
||||
**Solution**: Batch Loading mit Relations
|
||||
|
||||
```php
|
||||
// ✅ Single Query mit Batch Loading
|
||||
$users = $this->entityManager->findWithRelations(
|
||||
User::class,
|
||||
criteria: [],
|
||||
relations: ['profile', 'posts'] // Lädt Relations in Batches
|
||||
);
|
||||
|
||||
foreach ($users as $user) {
|
||||
// Kein zusätzlicher Query - Profile bereits geladen
|
||||
echo $user->getProfile()->bio;
|
||||
}
|
||||
```
|
||||
|
||||
### Lazy Loading vs Eager Loading
|
||||
|
||||
**Lazy Loading** (Standard):
|
||||
- Relations werden bei Zugriff geladen
|
||||
- Performance-Vorteil bei ungenutzten Relations
|
||||
- Potentielles N+1 Problem
|
||||
|
||||
```php
|
||||
$user = $this->entityManager->find(User::class, $id);
|
||||
// Profile wird erst bei Zugriff geladen:
|
||||
$bio = $user->getProfile()->bio; // Separate Query
|
||||
```
|
||||
|
||||
**Eager Loading**:
|
||||
- Relations werden sofort geladen
|
||||
- Kein N+1 Problem
|
||||
- Overhead bei ungenutzten Relations
|
||||
|
||||
```php
|
||||
$user = $this->entityManager->findEager(User::class, $id);
|
||||
// Profile ist bereits geladen - kein zusätzlicher Query
|
||||
$bio = $user->getProfile()->bio;
|
||||
```
|
||||
|
||||
### Identity Map
|
||||
|
||||
Der EntityManager nutzt eine Identity Map zur Vermeidung von Duplikaten:
|
||||
|
||||
```php
|
||||
// Beide Aufrufe liefern die GLEICHE Instanz
|
||||
$user1 = $this->entityManager->find(User::class, '123');
|
||||
$user2 = $this->entityManager->find(User::class, '123');
|
||||
|
||||
var_dump($user1 === $user2); // true
|
||||
```
|
||||
|
||||
**Vorteile**:
|
||||
- Keine Duplikate im Speicher
|
||||
- Konsistente Object Identity
|
||||
- Automatisches Change Tracking
|
||||
|
||||
**Utility Methods**:
|
||||
|
||||
```php
|
||||
// Detach Entity (aus Identity Map entfernen)
|
||||
$this->entityManager->detach($user);
|
||||
|
||||
// Clear Identity Map (z.B. für Batch Processing)
|
||||
$this->entityManager->clear();
|
||||
|
||||
// Identity Map Statistics
|
||||
$stats = $this->entityManager->getIdentityMapStats();
|
||||
// Returns: ['total_entities' => 150, 'entities_by_class' => [...]]
|
||||
```
|
||||
|
||||
### Reference Loading
|
||||
|
||||
**getReference()** - Lade Entity-Referenz ohne Existenz-Check:
|
||||
|
||||
```php
|
||||
// Erstellt Proxy ohne Database Query
|
||||
$user = $this->entityManager->getReference(User::class, $userId);
|
||||
|
||||
// Query wird erst bei Zugriff ausgeführt
|
||||
$name = $user->getName(); // Jetzt wird geladen
|
||||
```
|
||||
|
||||
**Use Cases**:
|
||||
- Foreign Key Relationships
|
||||
- Performance-kritische Pfade
|
||||
- Wenn Existenz bereits bekannt ist
|
||||
|
||||
### Profiling und Debugging
|
||||
|
||||
```php
|
||||
// Profiling aktivieren
|
||||
$this->entityManager->setProfilingEnabled(true);
|
||||
|
||||
// Operationen ausführen
|
||||
$users = $this->entityManager->findAll(User::class);
|
||||
|
||||
// Profiling-Statistiken abrufen
|
||||
$stats = $this->entityManager->getProfilingStatistics();
|
||||
|
||||
// Profiling-Summary
|
||||
$summary = $this->entityManager->getProfilingSummary();
|
||||
// Returns: ProfileSummary mit total_queries, total_time, etc.
|
||||
|
||||
// Profiling-Daten löschen
|
||||
$this->entityManager->clearProfilingData();
|
||||
```
|
||||
|
||||
### Criteria API (Type-Safe Queries)
|
||||
|
||||
**Modern**: Verwendung von Criteria statt Arrays
|
||||
|
||||
```php
|
||||
use App\Framework\Database\Criteria\DetachedCriteria;
|
||||
|
||||
// Type-safe Criteria
|
||||
$criteria = DetachedCriteria::forEntity(User::class)
|
||||
->where('email', '=', $email->value)
|
||||
->andWhere('active', '=', true)
|
||||
->orderBy('created_at', 'DESC')
|
||||
->limit(10);
|
||||
|
||||
$users = $this->entityManager->findByCriteria($criteria);
|
||||
$user = $this->entityManager->findOneByCriteria($criteria);
|
||||
$count = $this->entityManager->countByCriteria($criteria);
|
||||
```
|
||||
|
||||
### QueryBuilder Integration
|
||||
|
||||
Für komplexe Queries:
|
||||
|
||||
```php
|
||||
// QueryBuilder für Entity-Klasse
|
||||
$queryBuilder = $this->entityManager->createQueryBuilderFor(User::class);
|
||||
|
||||
$users = $queryBuilder
|
||||
->select('*')
|
||||
->where('email LIKE ?', ['%@example.com'])
|
||||
->andWhere('created_at > ?', ['2024-01-01'])
|
||||
->orderBy('name', 'ASC')
|
||||
->limit(50)
|
||||
->fetchAll();
|
||||
|
||||
// QueryBuilder für Table (ohne Entity)
|
||||
$qb = $this->entityManager->createQueryBuilderForTable('users');
|
||||
```
|
||||
|
||||
### Domain Events Integration
|
||||
|
||||
```php
|
||||
// Domain Event aufzeichnen
|
||||
$this->entityManager->recordDomainEvent($user, new UserRegisteredEvent($user));
|
||||
|
||||
// Events für Entity dispatchen
|
||||
$this->entityManager->dispatchDomainEventsForEntity($user);
|
||||
|
||||
// Alle Events dispatchen
|
||||
$this->entityManager->dispatchAllDomainEvents();
|
||||
|
||||
// Event-Statistiken
|
||||
$stats = $this->entityManager->getDomainEventStats();
|
||||
// Returns: ['total_events' => 23, 'events_by_entity' => [...]]
|
||||
```
|
||||
|
||||
## Repository Pattern
|
||||
|
||||
TODO: Document repository implementation and usage
|
||||
### Overview
|
||||
|
||||
Das Repository Pattern abstrahiert Daten-Zugriff und bietet domain-spezifische Query-Methoden. Das Framework nutzt **Composition over Inheritance**.
|
||||
|
||||
### Base EntityRepository
|
||||
|
||||
**Framework-Service** für Common Operations:
|
||||
|
||||
```php
|
||||
namespace App\Framework\Database\Repository;
|
||||
|
||||
final readonly class EntityRepository
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManager $entityManager
|
||||
) {}
|
||||
|
||||
public function find(string $entityClass, string $id): ?object
|
||||
public function findAll(string $entityClass): array
|
||||
public function findBy(string $entityClass, array $criteria, ...): array
|
||||
public function findOneBy(string $entityClass, array $criteria): ?object
|
||||
public function save(object $entity): object
|
||||
public function delete(object $entity): void
|
||||
public function transaction(callable $callback): mixed
|
||||
|
||||
// Batch Loading (N+1 Prevention)
|
||||
public function findWithRelations(...): array
|
||||
|
||||
// Pagination
|
||||
public function findPaginated(...): PaginatedResult
|
||||
|
||||
// Batch Operations
|
||||
public function saveBatch(array $entities, int $batchSize = 100): array
|
||||
public function deleteBatch(array $entities, int $batchSize = 100): void
|
||||
}
|
||||
```
|
||||
|
||||
### Domain Repository Pattern
|
||||
|
||||
**Composition-basiert** statt Inheritance:
|
||||
|
||||
```php
|
||||
namespace App\Domain\User\Repositories;
|
||||
|
||||
use App\Framework\Database\Repository\EntityRepository;
|
||||
|
||||
final readonly class UserRepository
|
||||
{
|
||||
public function __construct(
|
||||
private EntityRepository $entityRepository
|
||||
) {}
|
||||
|
||||
public function findById(UserId $id): ?User
|
||||
{
|
||||
return $this->entityRepository->find(User::class, $id->value);
|
||||
}
|
||||
|
||||
public function findByEmail(Email $email): ?User
|
||||
{
|
||||
return $this->entityRepository->findOneBy(
|
||||
User::class,
|
||||
['email' => $email->value]
|
||||
);
|
||||
}
|
||||
|
||||
public function findActiveUsers(): array
|
||||
{
|
||||
return $this->entityRepository->findBy(
|
||||
User::class,
|
||||
criteria: ['active' => true],
|
||||
orderBy: ['created_at' => 'DESC']
|
||||
);
|
||||
}
|
||||
|
||||
public function save(User $user): User
|
||||
{
|
||||
return $this->entityRepository->save($user);
|
||||
}
|
||||
|
||||
public function delete(User $user): void
|
||||
{
|
||||
$this->entityRepository->delete($user);
|
||||
}
|
||||
|
||||
// Domain-spezifische Methode mit Batch Loading
|
||||
public function findUsersWithProfiles(array $userIds): array
|
||||
{
|
||||
return $this->entityRepository->findWithRelations(
|
||||
User::class,
|
||||
criteria: ['id' => $userIds],
|
||||
relations: ['profile', 'settings']
|
||||
);
|
||||
}
|
||||
|
||||
// Pagination
|
||||
public function findPaginated(int $page, int $limit = 20): PaginatedResult
|
||||
{
|
||||
return $this->entityRepository->findPaginated(
|
||||
User::class,
|
||||
page: $page,
|
||||
limit: $limit,
|
||||
orderBy: ['created_at' => 'DESC']
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Repository Best Practices
|
||||
|
||||
**✅ Composition Over Inheritance**:
|
||||
```php
|
||||
// ✅ Framework Pattern - Composition
|
||||
final readonly class OrderRepository
|
||||
{
|
||||
public function __construct(
|
||||
private EntityRepository $entityRepository
|
||||
) {}
|
||||
}
|
||||
|
||||
// ❌ Avoid - Inheritance
|
||||
class OrderRepository extends BaseRepository
|
||||
{
|
||||
// Problematisch: Tight coupling
|
||||
}
|
||||
```
|
||||
|
||||
**✅ Domain-Specific Methods**:
|
||||
```php
|
||||
// ✅ Sprechende Domain-Methoden
|
||||
public function findOverdueOrders(): array
|
||||
{
|
||||
return $this->entityRepository->findBy(
|
||||
Order::class,
|
||||
['status' => 'pending'],
|
||||
orderBy: ['due_date' => 'ASC']
|
||||
);
|
||||
}
|
||||
|
||||
// ❌ Generic Methods außerhalb Domain
|
||||
public function findBy(array $criteria): array
|
||||
{
|
||||
// Zu generisch - nutze EntityRepository direkt
|
||||
}
|
||||
```
|
||||
|
||||
**✅ N+1 Prevention**:
|
||||
```php
|
||||
// ✅ Batch Loading für Relations
|
||||
public function findOrdersWithItems(array $orderIds): array
|
||||
{
|
||||
return $this->entityRepository->findWithRelations(
|
||||
Order::class,
|
||||
criteria: ['id' => $orderIds],
|
||||
relations: ['items', 'customer'] // Lädt in Batches
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Unit of Work Pattern
|
||||
|
||||
### Overview
|
||||
|
||||
Der Unit of Work Pattern verwaltet Entities und deren Änderungen für transaktionale Konsistenz.
|
||||
|
||||
**Key Features**:
|
||||
- **Change Tracking**: Automatische Änderungserkennung
|
||||
- **Transactional Consistency**: Atomic Commits/Rollbacks
|
||||
- **Batch Operations**: Optimierte Bulk-Operationen
|
||||
- **Entity States**: NEW, MANAGED, DIRTY, DELETED, DETACHED
|
||||
|
||||
### Entity Lifecycle
|
||||
|
||||
```php
|
||||
use App\Framework\Database\UnitOfWork\UnitOfWork;
|
||||
|
||||
final readonly class OrderService
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManager $entityManager
|
||||
) {}
|
||||
|
||||
public function createOrder(CreateOrderCommand $command): Order
|
||||
{
|
||||
$unitOfWork = $this->entityManager->unitOfWork;
|
||||
|
||||
// 1. Create new entity (state: NEW)
|
||||
$order = Order::create($command);
|
||||
|
||||
// 2. Persist marks entity for INSERT (state: NEW → MANAGED)
|
||||
$unitOfWork->persist($order);
|
||||
|
||||
// 3. Flush writes to database (state: MANAGED)
|
||||
$unitOfWork->flush();
|
||||
|
||||
// 4. Commit transaction
|
||||
$unitOfWork->commit();
|
||||
|
||||
return $order;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Change Tracking
|
||||
|
||||
**Automatic Change Detection**:
|
||||
|
||||
```php
|
||||
$unitOfWork = $this->entityManager->unitOfWork;
|
||||
|
||||
// Load entity (state: MANAGED)
|
||||
$user = $this->entityManager->find(User::class, $userId);
|
||||
|
||||
// Modify entity
|
||||
$user->updateProfile($newName);
|
||||
|
||||
// Change Tracking erkennt Änderung automatisch
|
||||
$unitOfWork->flush(); // UPDATE query wird generiert
|
||||
$unitOfWork->commit();
|
||||
```
|
||||
|
||||
**Manual Change Tracking**:
|
||||
|
||||
```php
|
||||
// Entity aus externer Quelle (state: DETACHED)
|
||||
$user = unserialize($serializedUser);
|
||||
|
||||
// Merge in Unit of Work (state: DETACHED → MANAGED)
|
||||
$managedUser = $unitOfWork->merge($user);
|
||||
|
||||
// Änderungen werden getrackt
|
||||
$unitOfWork->flush();
|
||||
```
|
||||
|
||||
### Transaction Management
|
||||
|
||||
**Explicit Transactions**:
|
||||
|
||||
```php
|
||||
public function processOrder(Order $order): void
|
||||
{
|
||||
$unitOfWork = $this->entityManager->unitOfWork;
|
||||
|
||||
try {
|
||||
$unitOfWork->beginTransaction();
|
||||
|
||||
// Multiple operations
|
||||
$order->confirm();
|
||||
$unitOfWork->persist($order);
|
||||
|
||||
$this->inventoryService->reserve($order->items);
|
||||
$this->paymentService->charge($order->total);
|
||||
|
||||
// Write changes to database
|
||||
$unitOfWork->flush();
|
||||
|
||||
// Commit transaction
|
||||
$unitOfWork->commit();
|
||||
} catch (\Exception $e) {
|
||||
$unitOfWork->rollback();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Implicit Transactions (Auto-Commit)**:
|
||||
|
||||
```php
|
||||
// Auto-commit ist standardmäßig aktiviert
|
||||
$unitOfWork->persist($entity); // Automatisch committed
|
||||
```
|
||||
|
||||
**Disable Auto-Commit** für Batch Operations:
|
||||
|
||||
```php
|
||||
$unitOfWork->setAutoCommit(false);
|
||||
|
||||
try {
|
||||
$unitOfWork->beginTransaction();
|
||||
|
||||
foreach ($orders as $order) {
|
||||
$unitOfWork->persist($order); // Kein Auto-Commit
|
||||
}
|
||||
|
||||
$unitOfWork->flush(); // Batch-Write
|
||||
$unitOfWork->commit(); // Atomic Commit
|
||||
} catch (\Exception $e) {
|
||||
$unitOfWork->rollback();
|
||||
throw $e;
|
||||
} finally {
|
||||
$unitOfWork->setAutoCommit(true);
|
||||
}
|
||||
```
|
||||
|
||||
### Entity States
|
||||
|
||||
```php
|
||||
use App\Framework\Database\UnitOfWork\EntityState;
|
||||
|
||||
// Check Entity State
|
||||
$state = $unitOfWork->getEntityState($entity);
|
||||
|
||||
match ($state) {
|
||||
EntityState::NEW => 'Entity marked for INSERT',
|
||||
EntityState::MANAGED => 'Entity being tracked, no changes',
|
||||
EntityState::DIRTY => 'Entity modified, pending UPDATE',
|
||||
EntityState::DELETED => 'Entity marked for DELETE',
|
||||
EntityState::DETACHED => 'Entity not managed by UnitOfWork',
|
||||
};
|
||||
|
||||
// Check if entity is managed
|
||||
if ($unitOfWork->contains($entity)) {
|
||||
// Entity is managed (NEW, MANAGED, DIRTY, or DELETED)
|
||||
}
|
||||
```
|
||||
|
||||
### Detaching Entities
|
||||
|
||||
```php
|
||||
// Detach entity from Unit of Work
|
||||
$unitOfWork->detach($user);
|
||||
|
||||
// Entity state: DETACHED
|
||||
// Changes to entity are no longer tracked
|
||||
```
|
||||
|
||||
**Use Cases**:
|
||||
- Long-running processes
|
||||
- Serialization/Caching
|
||||
- Read-only operations
|
||||
- Memory management in batch processing
|
||||
|
||||
### Bulk Operations
|
||||
|
||||
```php
|
||||
// Bulk Insert
|
||||
$unitOfWork->bulkInsert($entities); // Optimized batch INSERT
|
||||
|
||||
// Bulk Update
|
||||
$unitOfWork->bulkUpdate($entities); // Optimized batch UPDATE
|
||||
|
||||
// Bulk Delete
|
||||
$unitOfWork->bulkDelete($entities); // Optimized batch DELETE
|
||||
```
|
||||
|
||||
## Query Optimization
|
||||
|
||||
TODO: Document N+1 prevention and batch loading
|
||||
### N+1 Query Problem
|
||||
|
||||
**Erkennnung**: Jede Iteration führt zusätzliche Query aus
|
||||
|
||||
```php
|
||||
// ❌ N+1 Problem
|
||||
$orders = $this->entityManager->findAll(Order::class); // 1 Query
|
||||
|
||||
foreach ($orders as $order) {
|
||||
echo $order->getCustomer()->name; // N Queries (1 pro Order)
|
||||
}
|
||||
// Total: 1 + N Queries
|
||||
```
|
||||
|
||||
### Solution: Batch Loading
|
||||
|
||||
```php
|
||||
// ✅ Batch Loading
|
||||
$orders = $this->entityManager->findWithRelations(
|
||||
Order::class,
|
||||
criteria: [],
|
||||
relations: ['customer', 'items'] // Preload relations
|
||||
);
|
||||
|
||||
foreach ($orders as $order) {
|
||||
echo $order->getCustomer()->name; // Kein Query - bereits geladen
|
||||
}
|
||||
// Total: 2-3 Queries (1 für Orders, 1-2 für Relations)
|
||||
```
|
||||
|
||||
### Eager vs Lazy Loading
|
||||
|
||||
**Strategic Loading**:
|
||||
|
||||
```php
|
||||
// Heavy Operation - Eager Loading
|
||||
public function getOrderDetails(OrderId $id): OrderDetails
|
||||
{
|
||||
$order = $this->entityManager->findEager(Order::class, $id->value);
|
||||
// Alle Relations sofort geladen
|
||||
return OrderDetails::fromOrder($order);
|
||||
}
|
||||
|
||||
// Light Operation - Lazy Loading
|
||||
public function listOrders(): array
|
||||
{
|
||||
// Relations werden nur bei Bedarf geladen
|
||||
return $this->entityManager->findAll(Order::class);
|
||||
}
|
||||
```
|
||||
|
||||
### Pagination
|
||||
|
||||
```php
|
||||
use App\Framework\Database\Repository\EntityRepository;
|
||||
|
||||
public function getUsers(int $page): PaginatedResult
|
||||
{
|
||||
return $this->entityRepository->findPaginated(
|
||||
User::class,
|
||||
page: $page,
|
||||
limit: 20,
|
||||
orderBy: ['created_at' => 'DESC']
|
||||
);
|
||||
}
|
||||
|
||||
// PaginatedResult enthält:
|
||||
// - items: array (Entities)
|
||||
// - total: int (Total count)
|
||||
// - page: int (Current page)
|
||||
// - limit: int (Items per page)
|
||||
// - totalPages: int (Calculated)
|
||||
```
|
||||
|
||||
### Query Caching
|
||||
|
||||
```php
|
||||
// EntityManager mit Cache
|
||||
$entityManager = new EntityManager(
|
||||
// ...
|
||||
cacheManager: $cacheManager
|
||||
);
|
||||
|
||||
// Find verwendet Cache automatisch
|
||||
$user = $entityManager->find(User::class, $id);
|
||||
// Beim zweiten Aufruf: Cache Hit
|
||||
```
|
||||
|
||||
## Connection Pooling
|
||||
|
||||
TODO: Document connection pool configuration
|
||||
### Overview
|
||||
|
||||
Connection Pooling verbessert Performance durch Wiederverwendung von Datenbankverbindungen.
|
||||
|
||||
**Features**:
|
||||
- Min/Max Connection Limits
|
||||
- Connection Health Monitoring
|
||||
- Automatic Retry mit Exponential Backoff
|
||||
- Idle Connection Cleanup
|
||||
- Connection Warmup
|
||||
|
||||
### Configuration
|
||||
|
||||
```php
|
||||
use App\Framework\Database\Config\PoolConfig;
|
||||
|
||||
$poolConfig = new PoolConfig(
|
||||
minConnections: 2, // Minimum pool size
|
||||
maxConnections: 10, // Maximum pool size
|
||||
maxIdleTime: 300, // Idle timeout (seconds)
|
||||
healthCheckInterval: 60, // Health check frequency
|
||||
enableWarmup: true // Pre-create min connections
|
||||
);
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```php
|
||||
use App\Framework\Database\ConnectionPool;
|
||||
|
||||
$pool = new ConnectionPool($driverConfig, $poolConfig, $timer);
|
||||
|
||||
// Get connection from pool
|
||||
$connection = $pool->getConnection();
|
||||
|
||||
try {
|
||||
// Use connection
|
||||
$result = $connection->query('SELECT * FROM users');
|
||||
} finally {
|
||||
// Return connection to pool (automatic with PooledConnection)
|
||||
$connection->release();
|
||||
}
|
||||
```
|
||||
|
||||
### Health Monitoring
|
||||
|
||||
```php
|
||||
// Pool automatically monitors connection health
|
||||
$pool->performHealthCheck();
|
||||
|
||||
// Get pool statistics
|
||||
$stats = $pool->getPoolStatistics();
|
||||
// Returns:
|
||||
[
|
||||
'current_connections' => 5,
|
||||
'max_connections' => 10,
|
||||
'connections_in_use' => 3,
|
||||
'healthy_connections' => 5,
|
||||
'total_created' => 12,
|
||||
'total_destroyed' => 7
|
||||
]
|
||||
```
|
||||
|
||||
### Retry Logic
|
||||
|
||||
```php
|
||||
// Automatic retry with exponential backoff
|
||||
$retryStrategy = ExponentialBackoffStrategy::create()
|
||||
->withMaxAttempts(3)
|
||||
->withBaseDelay(Duration::fromMilliseconds(100));
|
||||
|
||||
// Connection pool uses retry automatically
|
||||
$connection = $pool->getConnectionWithRetry($retryStrategy);
|
||||
```
|
||||
|
||||
### Production Configuration
|
||||
|
||||
```php
|
||||
// Production Pool Config
|
||||
$productionPoolConfig = new PoolConfig(
|
||||
minConnections: 5, // Keep 5 connections ready
|
||||
maxConnections: 50, // Scale up to 50 under load
|
||||
maxIdleTime: 600, // 10 minutes idle timeout
|
||||
healthCheckInterval: 30, // Check every 30 seconds
|
||||
enableWarmup: true, // Pre-warm connections
|
||||
connectionTimeout: 5000 // 5 second timeout
|
||||
);
|
||||
```
|
||||
|
||||
## Transaction Management
|
||||
|
||||
TODO: Document transaction patterns and best practices
|
||||
### Basic Transactions
|
||||
|
||||
```php
|
||||
// EntityManager Transaction Helper
|
||||
$this->entityManager->transaction(function (EntityManager $em) {
|
||||
$order = Order::create($command);
|
||||
$em->save($order);
|
||||
|
||||
$this->inventoryService->reserve($order->items);
|
||||
$this->paymentService->charge($order->total);
|
||||
|
||||
// Automatic commit on success, rollback on exception
|
||||
});
|
||||
```
|
||||
|
||||
### Manual Transaction Control
|
||||
|
||||
```php
|
||||
$unitOfWork = $this->entityManager->unitOfWork;
|
||||
|
||||
$unitOfWork->beginTransaction();
|
||||
|
||||
try {
|
||||
// Operations
|
||||
$unitOfWork->persist($entity1);
|
||||
$unitOfWork->persist($entity2);
|
||||
|
||||
$unitOfWork->flush();
|
||||
$unitOfWork->commit();
|
||||
} catch (\Exception $e) {
|
||||
$unitOfWork->rollback();
|
||||
throw $e;
|
||||
}
|
||||
```
|
||||
|
||||
### Nested Transactions (Savepoints)
|
||||
|
||||
```php
|
||||
$unitOfWork->beginTransaction();
|
||||
|
||||
try {
|
||||
$order = Order::create($command);
|
||||
$unitOfWork->persist($order);
|
||||
|
||||
// Nested transaction (savepoint)
|
||||
$unitOfWork->beginTransaction();
|
||||
try {
|
||||
$this->inventoryService->reserve($order->items);
|
||||
$unitOfWork->commit(); // Commit savepoint
|
||||
} catch (\Exception $e) {
|
||||
$unitOfWork->rollback(); // Rollback to savepoint
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$unitOfWork->commit(); // Commit main transaction
|
||||
} catch (\Exception $e) {
|
||||
$unitOfWork->rollback(); // Rollback main transaction
|
||||
throw $e;
|
||||
}
|
||||
```
|
||||
|
||||
### Transaction Isolation Levels
|
||||
|
||||
```php
|
||||
use App\Framework\Database\TransactionIsolation;
|
||||
|
||||
$unitOfWork->beginTransaction(
|
||||
isolation: TransactionIsolation::READ_COMMITTED
|
||||
);
|
||||
|
||||
// Available isolation levels:
|
||||
// - READ_UNCOMMITTED (lowest isolation)
|
||||
// - READ_COMMITTED (default)
|
||||
// - REPEATABLE_READ
|
||||
// - SERIALIZABLE (highest isolation)
|
||||
```
|
||||
|
||||
### Transaction Best Practices
|
||||
|
||||
**✅ Short Transactions**:
|
||||
```php
|
||||
// ✅ Keep transactions short
|
||||
$this->entityManager->transaction(fn(EntityManager $em) =>
|
||||
$em->save($entity)
|
||||
);
|
||||
```
|
||||
|
||||
**❌ Long Transactions**:
|
||||
```php
|
||||
// ❌ Avoid long-running transactions
|
||||
$this->entityManager->transaction(function (EntityManager $em) {
|
||||
$this->sendEmail($user); // External I/O - BAD
|
||||
$this->processHeavyComputation(); // CPU-intensive - BAD
|
||||
$em->save($entity);
|
||||
});
|
||||
```
|
||||
|
||||
**✅ Batch with Periodic Commits**:
|
||||
```php
|
||||
// ✅ Batch processing with periodic commits
|
||||
$unitOfWork->setAutoCommit(false);
|
||||
$batchSize = 100;
|
||||
|
||||
for ($i = 0; $i < count($items); $i++) {
|
||||
$unitOfWork->persist($items[$i]);
|
||||
|
||||
if (($i + 1) % $batchSize === 0) {
|
||||
$unitOfWork->flush();
|
||||
$unitOfWork->commit();
|
||||
$unitOfWork->beginTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
$unitOfWork->flush();
|
||||
$unitOfWork->commit();
|
||||
$unitOfWork->setAutoCommit(true);
|
||||
```
|
||||
|
||||
## Database Testing
|
||||
|
||||
|
||||
@@ -2,26 +2,962 @@
|
||||
|
||||
This guide covers the event-driven architecture of the framework.
|
||||
|
||||
## Overview
|
||||
|
||||
The framework provides a sophisticated event system built on two core components:
|
||||
|
||||
- **EventBus**: Simple, lightweight interface for basic event dispatching
|
||||
- **EventDispatcher**: Full-featured implementation with handler management, priorities, and propagation control
|
||||
|
||||
**Key Features**:
|
||||
- Attribute-based event handler registration via `#[OnEvent]`
|
||||
- Priority-based handler execution
|
||||
- Event propagation control (stop propagation)
|
||||
- Support for inheritance and interface-based event matching
|
||||
- Automatic handler discovery via framework's attribute system
|
||||
- Domain events for business logic decoupling
|
||||
- Application lifecycle events for framework integration
|
||||
|
||||
## EventBus vs EventDispatcher
|
||||
|
||||
TODO: Document the differences and when to use each
|
||||
### EventBus Interface
|
||||
|
||||
**Purpose**: Simple contract for event dispatching
|
||||
|
||||
```php
|
||||
interface EventBus
|
||||
{
|
||||
public function dispatch(object $event): void;
|
||||
}
|
||||
```
|
||||
|
||||
**When to use**:
|
||||
- Simple event dispatching without return values
|
||||
- Fire-and-forget events
|
||||
- When you don't need handler results
|
||||
- Dependency injection when you want interface-based type hints
|
||||
|
||||
**Example Usage**:
|
||||
```php
|
||||
final readonly class OrderService
|
||||
{
|
||||
public function __construct(
|
||||
private EventBus $eventBus
|
||||
) {}
|
||||
|
||||
public function createOrder(CreateOrderCommand $command): Order
|
||||
{
|
||||
$order = Order::create($command);
|
||||
|
||||
// Fire event without caring about results
|
||||
$this->eventBus->dispatch(new OrderCreatedEvent($order));
|
||||
|
||||
return $order;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### EventDispatcher Implementation
|
||||
|
||||
**Purpose**: Full-featured event system with handler management and results
|
||||
|
||||
**Key Features**:
|
||||
- Returns array of handler results
|
||||
- Priority-based handler execution (highest priority first)
|
||||
- Stop propagation support
|
||||
- Manual handler registration via `addHandler()` or `listen()`
|
||||
- Automatic handler discovery via `#[OnEvent]` attribute
|
||||
- Inheritance and interface matching
|
||||
|
||||
**When to use**:
|
||||
- Need handler return values for processing
|
||||
- Want priority control over execution order
|
||||
- Need to stop event propagation conditionally
|
||||
- Require manual handler registration
|
||||
- Building complex event workflows
|
||||
|
||||
**Example Usage**:
|
||||
```php
|
||||
final readonly class PaymentProcessor
|
||||
{
|
||||
public function __construct(
|
||||
private EventDispatcher $dispatcher
|
||||
) {}
|
||||
|
||||
public function processPayment(Payment $payment): PaymentResult
|
||||
{
|
||||
// Dispatch and collect results from all handlers
|
||||
$results = $this->dispatcher->dispatch(
|
||||
new PaymentProcessingEvent($payment)
|
||||
);
|
||||
|
||||
// Process handler results
|
||||
foreach ($results as $result) {
|
||||
if ($result instanceof PaymentValidationFailure) {
|
||||
throw new PaymentException($result->reason);
|
||||
}
|
||||
}
|
||||
|
||||
return new PaymentResult($payment, $results);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Implementation Relationship
|
||||
|
||||
```php
|
||||
// EventDispatcher implements EventBus interface
|
||||
final class EventDispatcher implements EventBus
|
||||
{
|
||||
public function dispatch(object $event): array
|
||||
{
|
||||
// Full implementation with handler management
|
||||
}
|
||||
}
|
||||
|
||||
// In DI Container
|
||||
$container->singleton(EventBus::class, EventDispatcher::class);
|
||||
$container->singleton(EventDispatcher::class, EventDispatcher::class);
|
||||
```
|
||||
|
||||
**Recommendation**: Use `EventDispatcher` for type hints when you need full features, use `EventBus` interface when you want loose coupling and don't need results.
|
||||
|
||||
## Domain Events
|
||||
|
||||
TODO: Document domain event creation and handling
|
||||
### What are Domain Events?
|
||||
|
||||
Domain Events represent significant state changes or occurrences in your business domain. They enable loose coupling between domain components.
|
||||
|
||||
**Characteristics**:
|
||||
- Immutable (`readonly` classes)
|
||||
- Named in past tense (OrderCreated, UserRegistered, PaymentCompleted)
|
||||
- Contain only relevant domain data
|
||||
- No business logic (pure data objects)
|
||||
- Include timestamp for audit trail
|
||||
|
||||
### Creating Domain Events
|
||||
|
||||
```php
|
||||
namespace App\Domain\Order\Events;
|
||||
|
||||
use App\Domain\Order\ValueObjects\OrderId;
|
||||
use App\Framework\Core\ValueObjects\Timestamp;
|
||||
|
||||
final readonly class OrderCreatedEvent
|
||||
{
|
||||
public readonly Timestamp $occurredAt;
|
||||
|
||||
public function __construct(
|
||||
public OrderId $orderId,
|
||||
public UserId $userId,
|
||||
public Money $total,
|
||||
?Timestamp $occurredAt = null
|
||||
) {
|
||||
$this->occurredAt = $occurredAt ?? Timestamp::now();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Best Practices**:
|
||||
- Use Value Objects for event properties
|
||||
- Include timestamp for audit trail
|
||||
- Keep events focused (single responsibility)
|
||||
- Version events for backward compatibility
|
||||
- Document event payload in PHPDoc
|
||||
|
||||
### Framework Lifecycle Events
|
||||
|
||||
The framework provides built-in events for application lifecycle:
|
||||
|
||||
#### ApplicationBooted
|
||||
|
||||
**Triggered**: After application bootstrap completes
|
||||
|
||||
```php
|
||||
final readonly class ApplicationBooted
|
||||
{
|
||||
public function __construct(
|
||||
public \DateTimeImmutable $bootTime,
|
||||
public string $environment,
|
||||
public Version $version,
|
||||
public \DateTimeImmutable $occurredAt = new \DateTimeImmutable()
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
**Use Cases**:
|
||||
- Initialize services after boot
|
||||
- Start background workers
|
||||
- Setup scheduled tasks
|
||||
- Warm caches
|
||||
|
||||
#### BeforeHandleRequest
|
||||
|
||||
**Triggered**: Before HTTP request processing
|
||||
|
||||
```php
|
||||
final readonly class BeforeHandleRequest
|
||||
{
|
||||
public function __construct(
|
||||
public Request $request,
|
||||
public Timestamp $timestamp,
|
||||
public array $context = []
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
**Use Cases**:
|
||||
- Request logging
|
||||
- Performance monitoring
|
||||
- Security checks
|
||||
- Request transformation
|
||||
|
||||
#### AfterHandleRequest
|
||||
|
||||
**Triggered**: After HTTP request processing
|
||||
|
||||
```php
|
||||
final readonly class AfterHandleRequest
|
||||
{
|
||||
public function __construct(
|
||||
public Request $request,
|
||||
public Response $response,
|
||||
public Duration $processingTime,
|
||||
public Timestamp $timestamp,
|
||||
public array $context = []
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
**Use Cases**:
|
||||
- Response logging
|
||||
- Performance metrics
|
||||
- Analytics collection
|
||||
- Cleanup operations
|
||||
|
||||
### Example: User Registration Event
|
||||
|
||||
```php
|
||||
namespace App\Domain\User\Events;
|
||||
|
||||
final readonly class UserRegisteredEvent
|
||||
{
|
||||
public readonly Timestamp $occurredAt;
|
||||
|
||||
public function __construct(
|
||||
public UserId $userId,
|
||||
public Email $email,
|
||||
public UserName $userName,
|
||||
?Timestamp $occurredAt = null
|
||||
) {
|
||||
$this->occurredAt = $occurredAt ?? Timestamp::now();
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatching the event
|
||||
final readonly class UserService
|
||||
{
|
||||
public function __construct(
|
||||
private UserRepository $repository,
|
||||
private EventBus $eventBus
|
||||
) {}
|
||||
|
||||
public function register(RegisterUserCommand $command): User
|
||||
{
|
||||
$user = User::create(
|
||||
$command->email,
|
||||
$command->userName,
|
||||
$command->password
|
||||
);
|
||||
|
||||
$this->repository->save($user);
|
||||
|
||||
// Dispatch domain event
|
||||
$this->eventBus->dispatch(
|
||||
new UserRegisteredEvent(
|
||||
$user->id,
|
||||
$user->email,
|
||||
$user->userName
|
||||
)
|
||||
);
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Event Handler Registration
|
||||
|
||||
TODO: Document event handler attributes and registration
|
||||
### Attribute-Based Registration
|
||||
|
||||
**Primary Method**: Use `#[OnEvent]` attribute for automatic discovery
|
||||
|
||||
```php
|
||||
use App\Framework\Core\Events\OnEvent;
|
||||
|
||||
final readonly class UserEventHandlers
|
||||
{
|
||||
public function __construct(
|
||||
private EmailService $emailService,
|
||||
private Logger $logger
|
||||
) {}
|
||||
|
||||
#[OnEvent(priority: 100)]
|
||||
public function sendWelcomeEmail(UserRegisteredEvent $event): void
|
||||
{
|
||||
$this->emailService->send(
|
||||
to: $event->email,
|
||||
template: 'welcome',
|
||||
data: ['userName' => $event->userName->value]
|
||||
);
|
||||
}
|
||||
|
||||
#[OnEvent(priority: 50)]
|
||||
public function logUserRegistration(UserRegisteredEvent $event): void
|
||||
{
|
||||
$this->logger->info('User registered', [
|
||||
'user_id' => $event->userId->toString(),
|
||||
'email' => $event->email->value,
|
||||
'occurred_at' => $event->occurredAt->format('Y-m-d H:i:s')
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Attribute Parameters**:
|
||||
- `priority` (optional): Handler execution order (higher = earlier, default: 0)
|
||||
- `stopPropagation` (optional): Stop execution after this handler (default: false)
|
||||
|
||||
### Priority-Based Execution
|
||||
|
||||
Handlers execute in **priority order** (highest to lowest):
|
||||
|
||||
```php
|
||||
#[OnEvent(priority: 200)] // Executes first
|
||||
public function criticalHandler(OrderCreatedEvent $event): void { }
|
||||
|
||||
#[OnEvent(priority: 100)] // Executes second
|
||||
public function highPriorityHandler(OrderCreatedEvent $event): void { }
|
||||
|
||||
#[OnEvent(priority: 50)] // Executes third
|
||||
public function normalHandler(OrderCreatedEvent $event): void { }
|
||||
|
||||
#[OnEvent] // Executes last (default priority: 0)
|
||||
public function defaultHandler(OrderCreatedEvent $event): void { }
|
||||
```
|
||||
|
||||
**Use Cases for Priorities**:
|
||||
- **Critical (200+)**: Security checks, validation, fraud detection
|
||||
- **High (100-199)**: Transaction logging, audit trail
|
||||
- **Normal (50-99)**: Business logic, notifications
|
||||
- **Low (0-49)**: Analytics, metrics, cleanup
|
||||
|
||||
### Stop Propagation
|
||||
|
||||
**Purpose**: Prevent subsequent handlers from executing
|
||||
|
||||
```php
|
||||
#[OnEvent(priority: 100, stopPropagation: true)]
|
||||
public function validatePayment(PaymentProcessingEvent $event): PaymentValidationResult
|
||||
{
|
||||
$result = $this->validator->validate($event->payment);
|
||||
|
||||
if (!$result->isValid()) {
|
||||
// Stop propagation - no further handlers execute
|
||||
return new PaymentValidationFailure($result->errors);
|
||||
}
|
||||
|
||||
return new PaymentValidationSuccess();
|
||||
}
|
||||
|
||||
#[OnEvent(priority: 50)]
|
||||
public function processPayment(PaymentProcessingEvent $event): void
|
||||
{
|
||||
// This handler only executes if validation passes
|
||||
// (previous handler didn't set stopPropagation result)
|
||||
$this->gateway->charge($event->payment);
|
||||
}
|
||||
```
|
||||
|
||||
**Important**: `stopPropagation` stops execution **after** the current handler completes.
|
||||
|
||||
### Manual Handler Registration
|
||||
|
||||
**Alternative**: Register handlers programmatically
|
||||
|
||||
```php
|
||||
final readonly class EventInitializer
|
||||
{
|
||||
public function __construct(
|
||||
private EventDispatcher $dispatcher
|
||||
) {}
|
||||
|
||||
#[Initializer]
|
||||
public function registerHandlers(): void
|
||||
{
|
||||
// Using listen() method
|
||||
$this->dispatcher->listen(
|
||||
OrderCreatedEvent::class,
|
||||
function (OrderCreatedEvent $event) {
|
||||
// Handle event
|
||||
},
|
||||
priority: 100
|
||||
);
|
||||
|
||||
// Using addHandler() method
|
||||
$this->dispatcher->addHandler(
|
||||
eventClass: UserRegisteredEvent::class,
|
||||
handler: [$this->userService, 'onUserRegistered'],
|
||||
priority: 50
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**When to use manual registration**:
|
||||
- Dynamic handler registration based on configuration
|
||||
- Third-party library integration
|
||||
- Closure-based handlers for simple cases
|
||||
- Runtime handler modification
|
||||
|
||||
### Handler Discovery
|
||||
|
||||
The framework automatically discovers handlers marked with `#[OnEvent]`:
|
||||
|
||||
```php
|
||||
// In Application Bootstrap
|
||||
final readonly class EventSystemInitializer
|
||||
{
|
||||
#[Initializer]
|
||||
public function initialize(EventDispatcher $dispatcher): void
|
||||
{
|
||||
// Automatic discovery finds all #[OnEvent] methods
|
||||
$discoveredHandlers = $this->attributeScanner->findMethodsWithAttribute(
|
||||
OnEvent::class
|
||||
);
|
||||
|
||||
foreach ($discoveredHandlers as $handler) {
|
||||
$dispatcher->addHandler(
|
||||
eventClass: $handler->getEventClass(),
|
||||
handler: [$handler->instance, $handler->method],
|
||||
priority: $handler->attribute->priority ?? 0
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**No Manual Setup Required**: Framework handles discovery automatically during initialization.
|
||||
|
||||
## Event Middleware
|
||||
|
||||
TODO: Document event middleware system
|
||||
**Concept**: Middleware pattern for event processing (transform, filter, log, etc.)
|
||||
|
||||
### Custom Event Middleware
|
||||
|
||||
```php
|
||||
interface EventMiddleware
|
||||
{
|
||||
public function process(object $event, callable $next): mixed;
|
||||
}
|
||||
|
||||
final readonly class LoggingEventMiddleware implements EventMiddleware
|
||||
{
|
||||
public function __construct(
|
||||
private Logger $logger
|
||||
) {}
|
||||
|
||||
public function process(object $event, callable $next): mixed
|
||||
{
|
||||
$eventClass = get_class($event);
|
||||
$startTime = microtime(true);
|
||||
|
||||
$this->logger->debug("Event dispatched: {$eventClass}");
|
||||
|
||||
try {
|
||||
$result = $next($event);
|
||||
|
||||
$duration = (microtime(true) - $startTime) * 1000;
|
||||
$this->logger->debug("Event processed: {$eventClass}", [
|
||||
'duration_ms' => $duration
|
||||
]);
|
||||
|
||||
return $result;
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->error("Event failed: {$eventClass}", [
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Middleware Pipeline
|
||||
|
||||
```php
|
||||
final class EventDispatcherWithMiddleware
|
||||
{
|
||||
/** @var EventMiddleware[] */
|
||||
private array $middleware = [];
|
||||
|
||||
public function addMiddleware(EventMiddleware $middleware): void
|
||||
{
|
||||
$this->middleware[] = $middleware;
|
||||
}
|
||||
|
||||
public function dispatch(object $event): array
|
||||
{
|
||||
$pipeline = array_reduce(
|
||||
array_reverse($this->middleware),
|
||||
fn($next, $middleware) => fn($event) => $middleware->process($event, $next),
|
||||
fn($event) => $this->eventDispatcher->dispatch($event)
|
||||
);
|
||||
|
||||
return $pipeline($event);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Validation Middleware
|
||||
|
||||
```php
|
||||
final readonly class ValidationEventMiddleware implements EventMiddleware
|
||||
{
|
||||
public function __construct(
|
||||
private ValidatorInterface $validator
|
||||
) {}
|
||||
|
||||
public function process(object $event, callable $next): mixed
|
||||
{
|
||||
// Validate event before dispatching
|
||||
$errors = $this->validator->validate($event);
|
||||
|
||||
if (!empty($errors)) {
|
||||
throw new InvalidEventException(
|
||||
"Event validation failed: " . implode(', ', $errors)
|
||||
);
|
||||
}
|
||||
|
||||
return $next($event);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Async Event Processing
|
||||
|
||||
TODO: Document async event handling patterns
|
||||
### Queue-Based Async Handling
|
||||
|
||||
```php
|
||||
use App\Framework\Queue\Queue;
|
||||
use App\Framework\Queue\ValueObjects\JobPayload;
|
||||
|
||||
final readonly class AsyncEventHandler
|
||||
{
|
||||
public function __construct(
|
||||
private Queue $queue
|
||||
) {}
|
||||
|
||||
#[OnEvent(priority: 50)]
|
||||
public function processAsync(OrderCreatedEvent $event): void
|
||||
{
|
||||
// Dispatch to queue for async processing
|
||||
$job = new ProcessOrderEmailJob(
|
||||
orderId: $event->orderId,
|
||||
userEmail: $event->userEmail
|
||||
);
|
||||
|
||||
$this->queue->push(
|
||||
JobPayload::immediate($job)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Background Job
|
||||
final readonly class ProcessOrderEmailJob
|
||||
{
|
||||
public function __construct(
|
||||
public OrderId $orderId,
|
||||
public Email $userEmail
|
||||
) {}
|
||||
|
||||
public function handle(EmailService $emailService): void
|
||||
{
|
||||
// Process email asynchronously
|
||||
$emailService->sendOrderConfirmation(
|
||||
$this->orderId,
|
||||
$this->userEmail
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Event Buffering Pattern
|
||||
|
||||
```php
|
||||
final class EventBuffer
|
||||
{
|
||||
private array $bufferedEvents = [];
|
||||
|
||||
#[OnEvent]
|
||||
public function bufferEvent(DomainEvent $event): void
|
||||
{
|
||||
$this->bufferedEvents[] = $event;
|
||||
}
|
||||
|
||||
public function flush(EventDispatcher $dispatcher): void
|
||||
{
|
||||
foreach ($this->bufferedEvents as $event) {
|
||||
$dispatcher->dispatch($event);
|
||||
}
|
||||
|
||||
$this->bufferedEvents = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Usage in transaction
|
||||
try {
|
||||
$this->entityManager->beginTransaction();
|
||||
|
||||
// Buffer events during transaction
|
||||
$this->eventBuffer->bufferEvent(new OrderCreatedEvent(/* ... */));
|
||||
$this->eventBuffer->bufferEvent(new InventoryReservedEvent(/* ... */));
|
||||
|
||||
$this->entityManager->commit();
|
||||
|
||||
// Flush events after successful commit
|
||||
$this->eventBuffer->flush($this->dispatcher);
|
||||
} catch (\Exception $e) {
|
||||
$this->entityManager->rollback();
|
||||
// Events are not flushed on rollback
|
||||
throw $e;
|
||||
}
|
||||
```
|
||||
|
||||
## Event Best Practices
|
||||
|
||||
TODO: List event system best practices
|
||||
### 1. Event Naming
|
||||
|
||||
**✅ Good**: Past tense, descriptive
|
||||
```php
|
||||
OrderCreatedEvent
|
||||
UserRegisteredEvent
|
||||
PaymentCompletedEvent
|
||||
InventoryReservedEvent
|
||||
```
|
||||
|
||||
**❌ Bad**: Present/future tense, vague
|
||||
```php
|
||||
CreateOrderEvent
|
||||
RegisterUserEvent
|
||||
CompletePayment
|
||||
ReserveInventory
|
||||
```
|
||||
|
||||
### 2. Event Granularity
|
||||
|
||||
**✅ Focused Events**:
|
||||
```php
|
||||
final readonly class OrderCreatedEvent { /* ... */ }
|
||||
final readonly class OrderShippedEvent { /* ... */ }
|
||||
final readonly class OrderCancelledEvent { /* ... */ }
|
||||
```
|
||||
|
||||
**❌ God Events**:
|
||||
```php
|
||||
final readonly class OrderEvent
|
||||
{
|
||||
public function __construct(
|
||||
public string $action, // 'created', 'shipped', 'cancelled'
|
||||
public Order $order
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Immutability
|
||||
|
||||
**✅ Readonly Events**:
|
||||
```php
|
||||
final readonly class UserUpdatedEvent
|
||||
{
|
||||
public function __construct(
|
||||
public UserId $userId,
|
||||
public Email $newEmail
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
**❌ Mutable Events**:
|
||||
```php
|
||||
final class UserUpdatedEvent
|
||||
{
|
||||
public UserId $userId;
|
||||
public Email $newEmail;
|
||||
|
||||
public function setEmail(Email $email): void
|
||||
{
|
||||
$this->newEmail = $email; // Bad: Events should be immutable
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Handler Independence
|
||||
|
||||
**✅ Independent Handlers**:
|
||||
```php
|
||||
#[OnEvent]
|
||||
public function sendEmail(OrderCreatedEvent $event): void
|
||||
{
|
||||
// Self-contained - doesn't depend on other handlers
|
||||
$this->emailService->send(/* ... */);
|
||||
}
|
||||
|
||||
#[OnEvent]
|
||||
public function updateInventory(OrderCreatedEvent $event): void
|
||||
{
|
||||
// Independent - no shared state with email handler
|
||||
$this->inventoryService->reserve(/* ... */);
|
||||
}
|
||||
```
|
||||
|
||||
**❌ Coupled Handlers**:
|
||||
```php
|
||||
private bool $emailSent = false;
|
||||
|
||||
#[OnEvent(priority: 100)]
|
||||
public function sendEmail(OrderCreatedEvent $event): void
|
||||
{
|
||||
$this->emailService->send(/* ... */);
|
||||
$this->emailSent = true; // Bad: Shared state
|
||||
}
|
||||
|
||||
#[OnEvent(priority: 50)]
|
||||
public function logEmailSent(OrderCreatedEvent $event): void
|
||||
{
|
||||
if ($this->emailSent) { // Bad: Depends on other handler
|
||||
$this->logger->info('Email sent');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Error Handling
|
||||
|
||||
**✅ Graceful Degradation**:
|
||||
```php
|
||||
#[OnEvent]
|
||||
public function sendNotification(OrderCreatedEvent $event): void
|
||||
{
|
||||
try {
|
||||
$this->notificationService->send(/* ... */);
|
||||
} catch (NotificationException $e) {
|
||||
// Log error but don't fail the entire event dispatch
|
||||
$this->logger->error('Notification failed', [
|
||||
'order_id' => $event->orderId->toString(),
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Avoid Circular Events
|
||||
|
||||
**❌ Circular Dependency**:
|
||||
```php
|
||||
#[OnEvent]
|
||||
public function onOrderCreated(OrderCreatedEvent $event): void
|
||||
{
|
||||
// Bad: Dispatching event from event handler can cause loops
|
||||
$this->eventBus->dispatch(new OrderProcessedEvent($event->orderId));
|
||||
}
|
||||
|
||||
#[OnEvent]
|
||||
public function onOrderProcessed(OrderProcessedEvent $event): void
|
||||
{
|
||||
// Bad: Creates circular dependency
|
||||
$this->eventBus->dispatch(new OrderCreatedEvent(/* ... */));
|
||||
}
|
||||
```
|
||||
|
||||
**✅ Use Command/Service Layer**:
|
||||
```php
|
||||
#[OnEvent]
|
||||
public function onOrderCreated(OrderCreatedEvent $event): void
|
||||
{
|
||||
// Good: Use service to perform additional work
|
||||
$this->orderProcessingService->processOrder($event->orderId);
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Performance Considerations
|
||||
|
||||
**Heavy Operations → Queue**:
|
||||
```php
|
||||
#[OnEvent]
|
||||
public function generateInvoicePdf(OrderCreatedEvent $event): void
|
||||
{
|
||||
// Heavy operation - push to queue
|
||||
$this->queue->push(
|
||||
JobPayload::immediate(
|
||||
new GenerateInvoiceJob($event->orderId)
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Light Operations → Synchronous**:
|
||||
```php
|
||||
#[OnEvent]
|
||||
public function logOrderCreation(OrderCreatedEvent $event): void
|
||||
{
|
||||
// Light operation - execute immediately
|
||||
$this->logger->info('Order created', [
|
||||
'order_id' => $event->orderId->toString()
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Event Sourcing Pattern
|
||||
|
||||
```php
|
||||
final class OrderEventStore
|
||||
{
|
||||
#[OnEvent]
|
||||
public function appendEvent(DomainEvent $event): void
|
||||
{
|
||||
$this->eventStore->append([
|
||||
'event_type' => get_class($event),
|
||||
'event_data' => json_encode($event),
|
||||
'occurred_at' => $event->occurredAt->format('Y-m-d H:i:s'),
|
||||
'aggregate_id' => $event->getAggregateId()
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Saga Pattern
|
||||
|
||||
```php
|
||||
final readonly class OrderSaga
|
||||
{
|
||||
#[OnEvent(priority: 100)]
|
||||
public function onOrderCreated(OrderCreatedEvent $event): void
|
||||
{
|
||||
// Step 1: Reserve inventory
|
||||
$this->inventoryService->reserve($event->items);
|
||||
}
|
||||
|
||||
#[OnEvent(priority: 90)]
|
||||
public function onInventoryReserved(InventoryReservedEvent $event): void
|
||||
{
|
||||
// Step 2: Process payment
|
||||
$this->paymentService->charge($event->orderId);
|
||||
}
|
||||
|
||||
#[OnEvent(priority: 80)]
|
||||
public function onPaymentCompleted(PaymentCompletedEvent $event): void
|
||||
{
|
||||
// Step 3: Ship order
|
||||
$this->shippingService->ship($event->orderId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### CQRS Pattern
|
||||
|
||||
```php
|
||||
// Command Side - Dispatches Events
|
||||
final readonly class CreateOrderHandler
|
||||
{
|
||||
public function handle(CreateOrderCommand $command): Order
|
||||
{
|
||||
$order = Order::create($command);
|
||||
$this->repository->save($order);
|
||||
|
||||
$this->eventBus->dispatch(
|
||||
new OrderCreatedEvent($order)
|
||||
);
|
||||
|
||||
return $order;
|
||||
}
|
||||
}
|
||||
|
||||
// Query Side - Maintains Read Model
|
||||
#[OnEvent]
|
||||
public function updateOrderReadModel(OrderCreatedEvent $event): void
|
||||
{
|
||||
$this->orderReadModel->insert([
|
||||
'order_id' => $event->orderId->toString(),
|
||||
'user_id' => $event->userId->toString(),
|
||||
'total' => $event->total->toDecimal(),
|
||||
'status' => 'created'
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
## Framework Integration
|
||||
|
||||
### With Queue System
|
||||
|
||||
```php
|
||||
#[OnEvent]
|
||||
public function queueHeavyTask(OrderCreatedEvent $event): void
|
||||
{
|
||||
$this->queue->push(
|
||||
JobPayload::immediate(
|
||||
new ProcessOrderAnalyticsJob($event->orderId)
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### With Scheduler
|
||||
|
||||
```php
|
||||
#[OnEvent]
|
||||
public function scheduleReminder(OrderCreatedEvent $event): void
|
||||
{
|
||||
$this->scheduler->schedule(
|
||||
'send-order-reminder-' . $event->orderId,
|
||||
OneTimeSchedule::at(Timestamp::now()->addDays(3)),
|
||||
fn() => $this->emailService->sendReminder($event->orderId)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### With Cache System
|
||||
|
||||
```php
|
||||
#[OnEvent]
|
||||
public function invalidateCache(OrderCreatedEvent $event): void
|
||||
{
|
||||
$this->cache->forget(
|
||||
CacheTag::fromString("user_{$event->userId}")
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
The Event System provides:
|
||||
- ✅ **Decoupled Architecture**: Loose coupling via events
|
||||
- ✅ **Flexible Handling**: Priority-based, propagation control
|
||||
- ✅ **Automatic Discovery**: `#[OnEvent]` attribute registration
|
||||
- ✅ **Multiple Patterns**: Domain events, application events, async processing
|
||||
- ✅ **Framework Integration**: Works with Queue, Scheduler, Cache
|
||||
- ✅ **Testing Support**: Easy to unit test and integration test
|
||||
- ✅ **Performance**: Efficient handler execution with priority optimization
|
||||
|
||||
**When to Use Events**:
|
||||
- Decoupling domain logic from side effects (email, notifications)
|
||||
- Cross-module communication without tight coupling
|
||||
- Audit trail and event sourcing
|
||||
- Async processing of non-critical operations
|
||||
- CQRS and saga patterns
|
||||
|
||||
**When NOT to Use Events**:
|
||||
- Synchronous business logic requiring immediate results
|
||||
- Simple function calls (don't over-engineer)
|
||||
- Performance-critical paths (events have overhead)
|
||||
- Operations requiring transactional guarantees across handlers
|
||||
|
||||
@@ -354,6 +354,7 @@ $users = $this->userRepository->findWithProfiles($userIds);
|
||||
### Caching Strategy
|
||||
|
||||
**Framework Cache Interface mit Value Objects**:
|
||||
|
||||
```php
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\Cache\CacheKey;
|
||||
@@ -878,4 +879,4 @@ Database VOs folgen allen Framework-Prinzipien:
|
||||
- ✅ **Type Safety**: Union Types für Backwards Compatibility
|
||||
- ✅ **Framework Integration**: `__toString()` für seamless SQL interpolation
|
||||
- ✅ **Validation**: Constructor-basierte Validation
|
||||
- ✅ **Explicit**: Factory Methods (`fromString()`) für clarity
|
||||
- ✅ **Explicit**: Factory Methods (`fromString()`) für clarity
|
||||
|
||||
@@ -45,6 +45,7 @@ Alle Caches unterstützen **automatische Performance-Metriken** über Decorator
|
||||
**Performance**: ~70% schnellere Component-Initialisierung
|
||||
|
||||
**Verwendung**:
|
||||
|
||||
```php
|
||||
use App\Framework\LiveComponents\Cache\ComponentStateCache;
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -455,4 +455,4 @@ Die Pipeline nutzt konsequent Framework-Patterns:
|
||||
- **Horizontal Scaling**: Multi-Worker Support
|
||||
- **Queue Capacity**: 100,000+ Jobs (Database-backed)
|
||||
- **Scheduler Load**: 10,000+ concurrent scheduled tasks
|
||||
- **Memory Efficiency**: Linear scaling mit Job-Complexity
|
||||
- **Memory Efficiency**: Linear scaling mit Job-Complexity
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
381
docs/deployment/docker-swarm-deployment.md
Normal file
381
docs/deployment/docker-swarm-deployment.md
Normal 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/)
|
||||
@@ -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**:
|
||||
|
||||
800
docs/deployment/production-deployment-guide.md
Normal file
800
docs/deployment/production-deployment-guide.md
Normal 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
|
||||
227
docs/deployment/quick-deploy.md
Normal file
227
docs/deployment/quick-deploy.md
Normal 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
|
||||
```
|
||||
581
docs/deployment/troubleshooting-checklist.md
Normal file
581
docs/deployment/troubleshooting-checklist.md
Normal 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`
|
||||
609
docs/examples/poll-system-usage.md
Normal file
609
docs/examples/poll-system-usage.md
Normal 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}`)
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user