Some checks failed
Deploy Application / deploy (push) Has been cancelled
913 lines
25 KiB
Markdown
913 lines
25 KiB
Markdown
# 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
|