Files
michaelschiemer/docs/claude/guidelines.md
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
2025-10-25 19:18:37 +02:00

24 KiB

Development Guidelines

Entwicklungsrichtlinien für das Custom PHP Framework.

Code Style Principles

Fundamental Code Standards

  • PSR-12 Coding Standards: Befolge PSR-12 für einheitliche Code-Formatierung
  • PHP-CS-Fixer: Automatische Code-Formatierung verwenden
  • Strict Types: declare(strict_types=1) in allen PHP-Dateien

Framework-Specific Principles

No Inheritance Principle:

// ❌ Avoid extends - problematisch für Wartbarkeit
class UserController extends BaseController
{
    // Problematische Vererbung
}

// ✅ Use composition - flexibler und testbarer
final readonly class UserController
{
    public function __construct(
        private readonly AuthService $auth,
        private readonly UserRepository $userRepository,
        private readonly Logger $logger
    ) {}
}

Immutable by Design:

// ✅ Unveränderliche Objekte bevorzugen
final readonly class User
{
    public function __construct(
        public string $id,
        public Email $email,
        public string $name
    ) {}
    
    // Neue Instanz für Änderungen
    public function changeName(string $newName): self
    {
        return new self($this->id, $this->email, $newName);
    }
}

Readonly Everywhere:

// ✅ Classes und Properties readonly wo möglich
final readonly class ProductService
{
    public function __construct(
        private readonly ProductRepository $repository,
        private readonly PriceCalculator $calculator
    ) {}
}

Final by Default:

// ✅ Klassen sind final außer wenn explizit für Erweiterung designt
final readonly class OrderProcessor
{
    // Implementation
}

// Nur wenn bewusst erweiterbar
readonly class BaseValidator
{
    // Designed for extension
}

Property Hooks Usage:

// ❌ Property Hooks in readonly Klassen - TECHNISCH NICHT MÖGLICH
final readonly class User
{
    public string $fullName {
        get => $this->firstName . ' ' . $this->lastName; // PHP Fehler!
    }
}

// ✅ Normale Methoden in readonly Klassen verwenden
final readonly class User
{
    public function getFullName(): string
    {
        return $this->firstName . ' ' . $this->lastName;
    }
}

// ✅ Property Hooks nur in mutable Klassen verwenden
final class ConfigManager
{
    private array $cache = [];
    
    public string $apiKey {
        set (string $value) {
            if (empty($value)) {
                throw new InvalidArgumentException('API key cannot be empty');
            }
            $this->apiKey = $value;
            $this->cache = []; // Clear cache on change
        }
    }
    
    public array $settings {
        get => $this->cache ?: $this->cache = $this->loadSettings();
    }
}

// ✅ private(set) für kontrollierte Mutation
final class EventStore
{
    public private(set) array $events = [];
    
    public function addEvent(DomainEvent $event): void
    {
        $this->events[] = $event;
    }
}

Property Hooks Richtlinien:

  • TECHNISCH NICHT MÖGLICH in readonly Klassen - PHP verbietet Property Hooks in readonly Klassen
  • Nur in mutable Klassen verwenden
  • Alternative: Normale Methoden in readonly Klassen für computed values
  • Use Cases: Validation beim Setzen, Lazy Loading, Cache Invalidation
  • private(set) für kontrollierte Array-Mutation in mutable Klassen

Value Objects over Primitives

Verwende Value Objects statt Arrays oder Primitives:

// ❌ Primitive Obsession vermeiden
function createUser(string $email, array $preferences): array
{
    // Problematisch: keine Typsicherheit, versteckte Struktur
}

// ✅ Value Objects für Domain-Konzepte
function createUser(Email $email, UserPreferences $preferences): User
{
    // Typsicher, selbstdokumentierend, validiert
}

Domain Modeling mit Value Objects:

final readonly class Price
{
    public function __construct(
        public int $cents,
        public Currency $currency
    ) {
        if ($cents < 0) {
            throw new \InvalidArgumentException('Preis kann nicht negativ sein');
        }
    }
    
    public function toEuros(): float
    {
        return $this->cents / 100.0;
    }
    
    public function add(self $other): self
    {
        if (!$this->currency->equals($other->currency)) {
            throw new \InvalidArgumentException('Currencies must match');
        }
        
        return new self($this->cents + $other->cents, $this->currency);
    }
}

Testing Standards

Test Organization

Mixed Testing Approach:

  • Pest Framework: Bevorzugt für neue Tests (moderne Syntax)
  • PHPUnit: Traditionelle Tests beibehalten
  • Test Structure: Tests spiegeln Source-Verzeichnisstruktur wider
// ✅ Pest Test Beispiel
it('can calculate order total with tax', function () {
    $order = new Order([
        new OrderItem(new Price(1000, Currency::EUR), quantity: 2),
        new OrderItem(new Price(500, Currency::EUR), quantity: 1)
    ]);
    
    $calculator = new OrderCalculator(new TaxRate(0.19));
    $total = $calculator->calculateTotal($order);
    
    expect($total->cents)->toBe(2975); // 25€ + 19% tax
});

Test Categories

  • Unit Tests: Domain Logic, Value Objects, Services
  • Integration Tests: Web Controller, Database Operations
  • Feature Tests: End-to-End User Workflows
  • Performance Tests: Critical Path Performance

Security Guidelines

Authentication & Authorization

IP-based Authentication für Admin Routes:

#[Route(path: '/admin/dashboard', method: Method::GET)]
#[Auth(strategy: 'ip', allowedIps: ['127.0.0.1', '::1'])]
public function dashboard(HttpRequest $request): ViewResult
{
    // Nur von erlaubten IP-Adressen erreichbar
}

Route Protection:

#[Route(path: '/api/users', method: Method::POST)]
#[Auth(strategy: 'session', roles: ['admin'])]
public function createUser(CreateUserRequest $request): JsonResult
{
    // Authentifizierung und Autorisierung erforderlich
}

Input Validation

Request Objects für Validation:

final readonly class CreateUserRequest implements ControllerRequest
{
    public function __construct(
        public Email $email,
        public string $name,
        public ?string $company = null
    ) {}
    
    public static function fromHttpRequest(HttpRequest $request): self
    {
        $data = $request->parsedBody->toArray();
        
        return new self(
            email: new Email($data['email'] ?? ''),
            name: trim($data['name'] ?? ''),
            company: !empty($data['company']) ? trim($data['company']) : null
        );
    }
}

Server Data Access

Verwende Request::server statt Superglobals:

// ❌ Keine Superglobals verwenden
public function handleRequest(): JsonResult
{
    $userAgent = $_SERVER['HTTP_USER_AGENT']; // Schlecht
    $clientIp = $_SERVER['REMOTE_ADDR'];      // Schlecht
}

// ✅ Request::server verwenden
public function handleRequest(HttpRequest $request): JsonResult
{
    $userAgent = $request->server->getUserAgent();
    $clientIp = $request->server->getClientIp();
    $referer = $request->server->getSafeRefererUrl('/dashboard');
    
    return new JsonResult([
        'user_agent' => $userAgent->toString(),
        'client_ip' => (string) $clientIp,
        'safe_referer' => $referer
    ]);
}

### OWASP Security Events

**Security Event Logging**:
```php
// Automatisches Security Event Logging
final readonly class AuthenticationGuard
{
    public function authenticate(LoginAttempt $attempt): AuthResult
    {
        if ($attempt->isRateLimited()) {
            $this->eventLogger->logSecurityEvent(
                new AuthenticationFailedEvent(
                    reason: 'Rate limit exceeded',
                    ipAddress: $attempt->ipAddress,
                    userAgent: $attempt->userAgent
                )
            );
            
            throw new AuthenticationException('Too many attempts');
        }
        
        // Authentication logic
    }
}

Performance Guidelines

Database Optimization

EntityManager Usage:

// ✅ Bulk Operations verwenden
public function updateMultipleUsers(array $userUpdates): void
{
    $this->entityManager->beginTransaction();
    
    try {
        foreach ($userUpdates as $update) {
            $user = $this->entityManager->find(User::class, $update->userId);
            $user->updateProfile($update->profileData);
            // Keine sofortige Persistierung
        }
        
        $this->entityManager->flush(); // Bulk flush
        $this->entityManager->commit();
    } catch (\Exception $e) {
        $this->entityManager->rollback();
        throw $e;
    }
}

N+1 Query Prevention:

// ✅ Eager Loading verwenden
$users = $this->userRepository->findWithProfiles($userIds);

// Statt lazy loading in Schleife
// foreach ($users as $user) {
//     $profile = $user->getProfile(); // N+1 Problem
// }

Caching Strategy

Framework Cache Interface mit Value Objects:

use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheKey;
use App\Framework\Cache\CacheItem;
use App\Framework\Core\ValueObjects\Duration;

final readonly class UserService
{
    public function __construct(
        private readonly Cache $cache,  // SmartCache ist Standard-Implementation
        private readonly UserRepository $repository
    ) {}
    
    public function getUser(string $userId): User
    {
        $cacheKey = CacheKey::fromString("user_{$userId}");
        $ttl = Duration::fromHours(1);
        
        // Remember Pattern mit Value Objects
        $cacheItem = $this->cache->remember(
            key: $cacheKey,
            callback: fn() => $this->repository->find($userId),
            ttl: $ttl
        );
        
        return $cacheItem->value;
    }
    
    public function cacheMultipleUsers(array $users): bool
    {
        $cacheItems = [];
        
        foreach ($users as $user) {
            $cacheItems[] = CacheItem::forSetting(
                key: CacheKey::fromString("user_{$user->id}"),
                value: $user,
                ttl: Duration::fromHours(1)
            );
        }
        
        // Batch-Operation mit SmartCache
        return $this->cache->set(...$cacheItems);
    }
}

Advanced Cache Patterns:

// Cache mit Tags für gruppierte Invalidierung
$userKey = CacheKey::fromString("user_{$userId}");
$teamTag = CacheTag::fromString("team_{$teamId}");

$cacheItem = CacheItem::forSetting(
    key: $userKey,
    value: $user,
    ttl: Duration::fromHours(2),
    tags: [$teamTag]
);

$this->cache->set($cacheItem);

// Alle Team-bezogenen Caches invalidieren
$this->cache->forget($teamTag);

## Configuration Management

### Typed Configuration mit Environment Klasse

**Framework Environment Klasse verwenden**:
```php
final readonly class DatabaseConfig
{
    public function __construct(
        public string $host,
        public int $port,
        public string $database,
        public string $username,
        public string $password,
        public string $driver = 'mysql'
    ) {}
    
    public static function fromEnvironment(Environment $env): self
    {
        return new self(
            host: $env->get(EnvKey::DB_HOST, 'localhost'),
            port: $env->getInt(EnvKey::DB_PORT, 3306),
            database: $env->require(EnvKey::DB_NAME),
            username: $env->require(EnvKey::DB_USER),
            password: $env->require(EnvKey::DB_PASS),
            driver: $env->get(EnvKey::DB_DRIVER, 'mysql')
        );
    }
}

EnvKey Enum für Type Safety:

// Environment Keys als Enum definieren
enum EnvKey: string 
{
    case DB_HOST = 'DB_HOST';
    case DB_PORT = 'DB_PORT';
    case DB_NAME = 'DB_NAME';
    case DB_USER = 'DB_USER';
    case DB_PASS = 'DB_PASS';
    case DB_DRIVER = 'DB_DRIVER';
    case APP_ENV = 'APP_ENV';
}

Configuration Initializer Pattern:

final readonly class DatabaseConfigInitializer implements Initializer
{
    public function __construct(
        private readonly Environment $environment
    ) {}
    
    public function initialize(Container $container): void
    {
        $config = DatabaseConfig::fromEnvironment($this->environment);
        $container->singleton(DatabaseConfig::class, $config);
    }
}

Documentation Standards

Code Documentation

Self-Documenting Code bevorzugen:

// ✅ Code, der sich selbst erklärt
final readonly class OrderTotalCalculator
{
    public function calculateTotalWithTax(
        Order $order,
        TaxRate $taxRate
    ): Money {
        $subtotal = $this->calculateSubtotal($order);
        $taxAmount = $subtotal->multiply($taxRate->asDecimal());
        
        return $subtotal->add($taxAmount);
    }
}

PHPDoc nur wenn notwendig:

/**
 * Berechnet Gesamtsumme nur für komplexe Business Logic
 * 
 * @param Order $order - Customer order with line items
 * @param TaxRate $taxRate - Applicable tax rate (0.0-1.0)
 * @throws InvalidOrderException wenn Order leer ist
 */
public function calculateComplexTotal(Order $order, TaxRate $taxRate): Money

Dependency Injection Best Practices

Constructor Injection

Explizite Dependencies:

// ✅ Alle Dependencies im Constructor
final readonly class OrderProcessor
{
    public function __construct(
        private readonly PaymentGateway $paymentGateway,
        private readonly InventoryService $inventory,
        private readonly EmailService $emailService,
        private readonly Logger $logger
    ) {}
}

Service Locator Anti-Pattern vermeiden:

// ❌ Service Locator Pattern
public function processOrder(Order $order): void
{
    $gateway = ServiceLocator::get(PaymentGateway::class); // Schlecht
}

// ✅ Dependency Injection
public function processOrder(Order $order): void
{
    $this->paymentGateway->charge($order->getTotal()); // Gut
}

Framework Integration Patterns

Environment-Aware Services

Environment in Service Initialization:

final readonly class EmailServiceInitializer implements Initializer
{
    public function __construct(
        private readonly Environment $environment
    ) {}
    
    public function initialize(Container $container): void
    {
        $service = match ($this->environment->get(EnvKey::APP_ENV)) {
            'production' => new SmtpEmailService(
                host: $this->environment->require(EnvKey::SMTP_HOST),
                username: $this->environment->require(EnvKey::SMTP_USER),
                password: $this->environment->require(EnvKey::SMTP_PASS)
            ),
            'development' => new LogEmailService(),
            default => new NullEmailService()
        };
        
        $container->singleton(EmailService::class, $service);
    }
}

Database Value Objects Best Practices

Overview

Das Framework verwendet Value Objects für alle Database-Identifier, um Type Safety und SQL-Injection-Prevention zu gewährleisten. Diese Best Practices ergänzen die allgemeinen Value Object Patterns um database-spezifische Anforderungen.

Verfügbare Database VOs: TableName, ColumnName, IndexName, ConstraintName, DatabaseName, SchemaName

When to Use Database VOs

Simple Migrations (String-basiert OK):

// Einfache Tabellen ohne komplexe Constraints
$schema->create('logs', function (Blueprint $table) {
    $table->id();
    $table->string('message');
    $table->timestamps();
});

Complex Migrations (VOs empfohlen):

use App\Framework\Database\ValueObjects\{TableName, ColumnName, IndexName};

// Tabellen mit Relationships, Constraints, Composite Indexes
$schema->create(TableName::fromString('orders'), function (Blueprint $table) {
    $table->string(ColumnName::fromString('ulid'), 26)->primary();
    $table->string(ColumnName::fromString('user_id'), 26);

    $table->foreign(ColumnName::fromString('user_id'))
        ->references(ColumnName::fromString('ulid'))
        ->on(TableName::fromString('users'))
        ->onDelete(ForeignKeyAction::CASCADE);

    // Composite Index mit explicit naming
    $table->index(
        ColumnName::fromString('user_id'),
        ColumnName::fromString('status'),
        ColumnName::fromString('created_at'),
        IndexName::fromString('idx_orders_user_status_created')
    );
});

Decision Criteria:

  • Use VOs when: Foreign keys, composite indexes, PostgreSQL schemas, security-critical tables
  • String OK when: Simple logs, cache tables, temporary tables, development prototypes

Naming Conventions

Index Names folgen Pattern idx_{table}_{columns}:

IndexName::fromString('idx_users_email')
IndexName::fromString('idx_users_email_active')
IndexName::fromString('idx_orders_user_created')

Unique Constraints folgen Pattern uk_{table}_{columns}:

IndexName::fromString('uk_users_email')
IndexName::fromString('uk_users_username')

Foreign Key Constraints folgen Pattern fk_{table}_{referenced_table}:

ConstraintName::fromString('fk_user_profiles_users')
ConstraintName::fromString('fk_orders_users')
ConstraintName::fromString('fk_order_items_orders')

Check Constraints folgen Pattern ck_{table}_{column}_{condition}:

ConstraintName::fromString('ck_users_age_positive')
ConstraintName::fromString('ck_orders_total_min')

Validation Patterns

Automatic Validation on Construction:

// ✅ Valid - alphanumerisch + underscore, beginnt mit Buchstabe
TableName::fromString('users');
TableName::fromString('user_profiles');
ColumnName::fromString('email_address');

// ❌ Invalid - wirft Exception
TableName::fromString('');           // Leer
TableName::fromString('123users');   // Beginnt mit Ziffer
TableName::fromString('user-table'); // Ungültiges Zeichen
TableName::fromString('select');     // Reserviertes Keyword

Validation Rules für alle Database VOs:

  • Keine leeren Strings
  • Alphanumerisch + Underscore nur
  • Beginnt nicht mit Ziffer
  • Max. 63 Zeichen (PostgreSQL Limit)
  • Keine reservierten SQL Keywords

PostgreSQL Schema Support

TableName mit Schema-Prefix:

use App\Framework\Database\ValueObjects\{TableName, SchemaName};

// Simple table name
$table = TableName::fromString('users');
// Output: "users"

// With schema prefix
$table = new TableName(
    value: 'users',
    schema: SchemaName::fromString('public')
);
// Output: "public.users"

// In Migration
$schema->create(
    new TableName('audit_logs', SchemaName::fromString('audit')),
    function (Blueprint $table) {
        // Schema-qualifizierte Tabelle
    }
);

Backwards Compatibility

Union Types ermöglichen schrittweise Migration:

// Blueprint akzeptiert string|TableName
public function create(string|TableName $table, Closure $callback): void

// Legacy-Code funktioniert weiterhin
$schema->create('users', function (Blueprint $table) {
    $table->string('email'); // String-basiert OK
});

// Neuer Code kann VOs verwenden
$schema->create(TableName::fromString('users'), function (Blueprint $table) {
    $table->string(ColumnName::fromString('email')); // VO-basiert
});

// Mischung ist möglich
$schema->create('users', function (Blueprint $table) {
    $table->string('email'); // String
    $table->unique(
        ColumnName::fromString('email'),  // VO
        'uk_users_email'                  // String
    );
});

Drop Operations mit Variadic Parameters

Natural API statt Array-Parameter:

$schema->table('users', function (Blueprint $table) {
    // ✅ Variadic - Natural
    $table->dropColumn('old_field', 'deprecated_field', 'unused_field');

    // ❌ Array - Clunky
    // $table->dropColumn(['old_field', 'deprecated_field']);

    // ✅ Auch mit VOs
    $table->dropColumn(
        ColumnName::fromString('old_field'),
        ColumnName::fromString('deprecated_field')
    );

    // Drop index by name
    $table->dropIndex('idx_users_email');

    // Drop index by columns (finds index automatically)
    $table->dropIndex('email', 'active');

    // Drop foreign key by constraint name
    $table->dropForeign('fk_users_company');

    // Drop foreign key by column (finds constraint automatically)
    $table->dropForeign('company_id');
});

SQL Injection Prevention

Built-in Protection durch VO Validation:

// ✅ VOs validieren Input - SQL Injection unmöglich
$tableName = TableName::fromString($_GET['table']);
// Wirft Exception bei Injection-Versuch

// Query-Building ist sicher
$query = "SELECT * FROM {$tableName}"; // Validated and safe

// ❌ Raw strings sind gefährlich
$query = "SELECT * FROM {$_GET['table']}"; // SQL Injection möglich!

Validation verhindert:

  • SQL Keywords als Identifier (select, drop, union)
  • Special Characters (;, --, /*, */)
  • Path Traversal (../, ..\\)
  • Control Characters

Testing Database VOs

Unit Tests für VO Validation:

use App\Framework\Database\ValueObjects\TableName;

it('validates table name format', function () {
    TableName::fromString('123invalid'); // Should throw
})->throws(\InvalidArgumentException::class);

it('accepts valid table names', function () {
    $table = TableName::fromString('users');
    expect($table->value)->toBe('users');
});

it('supports schema-qualified names', function () {
    $table = new TableName('users', SchemaName::fromString('public'));
    expect((string) $table)->toBe('public.users');
});

Integration Tests für Migration mit VOs:

it('creates table with value objects', function () {
    $tableName = TableName::fromString('test_users');

    $this->schema->create($tableName, function (Blueprint $table) {
        $table->string(ColumnName::fromString('email'));
        $table->unique(
            ColumnName::fromString('email'),
            IndexName::fromString('uk_test_users_email')
        );
    });

    expect($this->schema->hasTable($tableName))->toBeTrue();
    expect($this->schema->hasIndex('test_users', 'uk_test_users_email'))->toBeTrue();
});

Test Cleanup mit VOs:

afterEach(function () {
    // Cleanup mit VOs
    $this->schema->dropIfExists(TableName::fromString('test_users'));
    $this->schema->dropIfExists(TableName::fromString('test_profiles'));
});

Performance Considerations

VO Overhead ist minimal:

  • Creation: ~0.01ms pro VO (Validation einmalig)
  • String Conversion: ~0.001ms via __toString()
  • Memory: ~200 bytes pro VO Instance
  • Recommendation: VOs sind für alle Database Operations akzeptabel

Caching Strategy für häufig verwendete VOs:

final class TableNameCache
{
    private static array $cache = [];

    public static function get(string $name): TableName
    {
        return self::$cache[$name] ??= TableName::fromString($name);
    }
}

// Usage in Migration
$users = TableNameCache::get('users');
$profiles = TableNameCache::get('user_profiles');

Migration Code Review Checklist

Before Merge:

  • Naming Conventions befolgt (idx_, uk_, fk_, ck_)
  • Foreign Keys haben explizite ConstraintNames
  • Composite Indexes haben aussagekräftige IndexNames
  • PostgreSQL Schema-Prefix wo benötigt
  • Drop Operations nutzen Variadic Parameters
  • Backwards Compatibility: String-basiert OK für simple tables
  • Test Coverage: Migration Up/Down getestet

Security Review:

  • Keine User Input in TableName/ColumnName ohne VO Validation
  • Keine Dynamic Table Names aus unvalidiertem Input
  • Keine SQL Keywords als Identifier

Framework Compliance

Database VOs folgen allen Framework-Prinzipien:

  • Readonly Classes: Alle VOs sind final readonly
  • Immutability: Keine State-Mutation nach Construction
  • No Inheritance: final classes, composition only
  • Value Objects: Keine Primitive Obsession
  • 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