# 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**: ```php // ❌ 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**: ```php // ✅ 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**: ```php // ✅ 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**: ```php // ✅ 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**: ```php // ❌ 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 **Clone With Syntax (PHP 8.5)**: - ✅ **Verwenden für State-Transformationen** - Reduziert Boilerplate-Code erheblich - ✅ **Syntax**: `clone($object, ['property' => $value])` - Funktioniert perfekt mit `readonly` Klassen - ✅ **Best Practice**: Für einfache und mittlere Transformationen verwenden - ⚠️ **Komplexe Array-Manipulationen**: Können explizit bleiben, wenn lesbarer ```php // ✅ Clone With für einfache Transformationen public function withCount(int $count): self { return clone($this, ['count' => $count]); } // ✅ Clone With für mehrere Properties public function increment(): self { return clone($this, [ 'count' => $this->count + 1, 'lastUpdate' => date('H:i:s') ]); } // ⚠️ Komplexe Transformationen können explizit bleiben public function withTodoRemoved(string $todoId): self { $newTodos = array_filter($this->todos, fn($todo) => $todo['id'] !== $todoId); return clone($this, ['todos' => array_values($newTodos)]); } ``` ## Value Objects over Primitives **Verwende Value Objects statt Arrays oder Primitives**: ```php // ❌ 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**: ```php 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 ```php // ✅ 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: ```php #[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**: ```php #[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: ```php 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**: ```php // ❌ 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**: ```php // ✅ 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**: ```php // ✅ 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**: ```php 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**: ```php // 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**: ```php // 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**: ```php 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**: ```php // ✅ 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**: ```php /** * 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**: ```php // ✅ 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**: ```php // ❌ 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**: ```php 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): ```php // Einfache Tabellen ohne komplexe Constraints $schema->create('logs', function (Blueprint $table) { $table->id(); $table->string('message'); $table->timestamps(); }); ``` **Complex Migrations** (VOs empfohlen): ```php 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}`: ```php IndexName::fromString('idx_users_email') IndexName::fromString('idx_users_email_active') IndexName::fromString('idx_orders_user_created') ``` **Unique Constraints** folgen Pattern `uk_{table}_{columns}`: ```php IndexName::fromString('uk_users_email') IndexName::fromString('uk_users_username') ``` **Foreign Key Constraints** folgen Pattern `fk_{table}_{referenced_table}`: ```php 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}`: ```php ConstraintName::fromString('ck_users_age_positive') ConstraintName::fromString('ck_orders_total_min') ``` ### Validation Patterns **Automatic Validation** on Construction: ```php // ✅ 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**: ```php 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: ```php // 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: ```php $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: ```php // ✅ 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: ```php 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: ```php 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: ```php 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: ```php 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