Files
michaelschiemer/docs/claude/guidelines.md
Michael Schiemer 55a330b223 Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
2025-08-11 20:13:26 +02:00

14 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);
    }
}