- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
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
readonlyKlassen - PHP verbietet Property Hooks in readonly Klassen - Nur in mutable Klassen verwenden
- Alternative: Normale Methoden in
readonlyKlassen 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);
}
}