Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
- Remove middleware reference from Gitea Traefik labels (caused routing issues) - Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s) - Add explicit service reference in Traefik labels - Fix intermittent 504 timeouts by improving PostgreSQL connection handling Fixes Gitea unreachability via git.michaelschiemer.de
1527 lines
38 KiB
Markdown
1527 lines
38 KiB
Markdown
# Database Patterns
|
||
|
||
Umfassende Dokumentation der Database-Patterns im Custom PHP Framework.
|
||
|
||
## Migration System: Safe Rollback Architecture
|
||
|
||
Das Framework verwendet ein intelligentes Migration-System, das zwischen sicheren und unsicheren Rollbacks unterscheidet.
|
||
|
||
### Core Concepts
|
||
|
||
**Forward-Only by Default**: Alle Migrations sind standardmäßig forward-only (nur `up()` Methode erforderlich).
|
||
|
||
**Optional Safe Rollback**: Migrations, die SICHER rückgängig gemacht werden können (ohne Datenverlust), implementieren zusätzlich das `SafelyReversible` Interface.
|
||
|
||
### Migration Interface
|
||
|
||
```php
|
||
/**
|
||
* Base migration interface - Forward-only by default
|
||
*/
|
||
interface Migration
|
||
{
|
||
public function up(ConnectionInterface $connection): void;
|
||
public function getVersion(): MigrationVersion;
|
||
public function getDescription(): string;
|
||
}
|
||
```
|
||
|
||
### SafelyReversible Interface
|
||
|
||
```php
|
||
/**
|
||
* Optional interface for migrations that support safe rollback
|
||
*
|
||
* ONLY implement this if rollback is safe (no data loss)!
|
||
*/
|
||
interface SafelyReversible
|
||
{
|
||
public function down(ConnectionInterface $connection): void;
|
||
}
|
||
```
|
||
|
||
### Safe vs Unsafe Migrations
|
||
|
||
**✅ Safe for Rollback** (implement SafelyReversible):
|
||
- Creating new tables (can be dropped)
|
||
- Adding nullable columns (can be removed)
|
||
- Creating/dropping indexes (no data affected)
|
||
- Renaming columns (data preserved)
|
||
- Adding/removing foreign keys (constraints only)
|
||
|
||
**❌ Unsafe for Rollback** (only Migration):
|
||
- Dropping columns with data
|
||
- Transforming data formats
|
||
- Changing column types (data loss risk)
|
||
- Merging/splitting tables
|
||
- Deleting data
|
||
|
||
### Example: Safe Rollback
|
||
|
||
```php
|
||
use App\Framework\Database\Migration\{Migration, SafelyReversible};
|
||
|
||
/**
|
||
* Safe: New table can be dropped without data loss
|
||
*/
|
||
final readonly class CreateSessionsTable implements Migration, SafelyReversible
|
||
{
|
||
public function up(ConnectionInterface $connection): void
|
||
{
|
||
$schema = new Schema($connection);
|
||
$schema->create('sessions', function ($table) {
|
||
$table->string('id', 255)->primary();
|
||
$table->text('data');
|
||
});
|
||
$schema->execute();
|
||
}
|
||
|
||
public function down(ConnectionInterface $connection): void
|
||
{
|
||
$schema = new Schema($connection);
|
||
$schema->dropIfExists('sessions');
|
||
$schema->execute();
|
||
}
|
||
}
|
||
```
|
||
|
||
### Example: Unsafe Migration
|
||
|
||
```php
|
||
/**
|
||
* UNSAFE: Dropping column with data - NOT reversible
|
||
*/
|
||
final readonly class RemoveLegacyIdColumn implements Migration
|
||
{
|
||
public function up(ConnectionInterface $connection): void
|
||
{
|
||
$schema = new Schema($connection);
|
||
$schema->table('users', function ($table) {
|
||
$table->dropColumn('legacy_id'); // Data is LOST!
|
||
});
|
||
$schema->execute();
|
||
}
|
||
|
||
// NO down() method - data cannot be recovered
|
||
}
|
||
```
|
||
|
||
### Rollback Safety Check
|
||
|
||
Der `MigrationRunner` prüft automatisch, ob eine Migration sicher rollbar ist:
|
||
|
||
```bash
|
||
$ php console.php db:rollback 1
|
||
|
||
🔄 Rolling back migrations...
|
||
⚠️ Safety Check: Only migrations implementing SafelyReversible will be rolled back.
|
||
|
||
❌ Rollback failed: Migration 2024_12_19_150000 does not support safe rollback
|
||
|
||
ℹ️ This migration cannot be safely rolled back.
|
||
Reason: Data loss would occur during rollback.
|
||
|
||
💡 Recommendation:
|
||
Create a new forward migration to undo the changes instead:
|
||
php console.php make:migration FixYourChanges
|
||
|
||
📖 See docs/claude/examples/migrations/SafeVsUnsafeMigrations.md for guidelines.
|
||
```
|
||
|
||
### Fix-Forward Strategy
|
||
|
||
Statt unsichere Rollbacks: Neue Forward-Migration erstellen.
|
||
|
||
```php
|
||
// Original migration (dropped column)
|
||
final readonly class RemoveDeprecatedField implements Migration
|
||
{
|
||
public function up(ConnectionInterface $connection): void
|
||
{
|
||
$schema = new Schema($connection);
|
||
$schema->table('users', function ($table) {
|
||
$table->dropColumn('deprecated_field');
|
||
});
|
||
$schema->execute();
|
||
}
|
||
}
|
||
|
||
// Fix-Forward migration (restore column)
|
||
final readonly class RestoreDeprecatedField implements Migration, SafelyReversible
|
||
{
|
||
public function up(ConnectionInterface $connection): void
|
||
{
|
||
$schema = new Schema($connection);
|
||
$schema->table('users', function ($table) {
|
||
$table->string('deprecated_field')->nullable();
|
||
});
|
||
$schema->execute();
|
||
}
|
||
|
||
public function down(ConnectionInterface $connection): void
|
||
{
|
||
// Safe: Column is empty after restoration
|
||
$schema = new Schema($connection);
|
||
$schema->table('users', function ($table) {
|
||
$table->dropColumn('deprecated_field');
|
||
});
|
||
$schema->execute();
|
||
}
|
||
}
|
||
```
|
||
|
||
### Best Practices
|
||
|
||
1. **Default to Forward-Only**: Nur `Migration` implementieren, außer du bist SICHER, dass Rollback safe ist
|
||
2. **Document Why Safe**: Kommentiere, warum eine Migration `SafelyReversible` ist
|
||
3. **Test Rollback**: Teste `up()` → `down()` → `up()` Cycle in Development
|
||
4. **Production: Forward-Only**: Auch "safe" Rollbacks sollten in Production vermieden werden
|
||
5. **See Examples**: Vollständige Guidelines in `docs/claude/examples/migrations/SafeVsUnsafeMigrations.md`
|
||
|
||
---
|
||
|
||
## Database Value Objects
|
||
|
||
Das Framework verwendet Value Objects für alle Database-Identifier, um Type Safety und SQL-Injection-Prevention zu gewährleisten.
|
||
|
||
### Verfügbare Database Value Objects
|
||
|
||
**Core Database VOs** (`src/Framework/Database/ValueObjects/`):
|
||
|
||
- **TableName**: Validierte Tabellennamen mit optionalem Schema-Prefix
|
||
- **ColumnName**: Validierte Spaltennamen
|
||
- **IndexName**: Validierte Index-Namen
|
||
- **ConstraintName**: Validierte Constraint-Namen (Foreign Keys, Check Constraints)
|
||
- **DatabaseName**: Validierte Datenbank-Namen
|
||
- **SchemaName**: Validierte Schema-Namen (PostgreSQL)
|
||
|
||
### Value Object Patterns
|
||
|
||
Alle Database VOs folgen einheitlichen Patterns:
|
||
|
||
```php
|
||
// Immutable readonly classes
|
||
final readonly class TableName
|
||
{
|
||
public function __construct(
|
||
public string $value,
|
||
public ?SchemaName $schema = null
|
||
) {
|
||
$this->validate();
|
||
}
|
||
|
||
// Factory Method Pattern
|
||
public static function fromString(string $value): self
|
||
{
|
||
return new self($value);
|
||
}
|
||
|
||
// __toString() für String-Interpolation
|
||
public function __toString(): string
|
||
{
|
||
return $this->schema
|
||
? "{$this->schema}.{$this->value}"
|
||
: $this->value;
|
||
}
|
||
|
||
// Equality Comparison
|
||
public function equals(self $other): bool
|
||
{
|
||
return $this->value === $other->value
|
||
&& $this->schema?->equals($other->schema ?? null);
|
||
}
|
||
}
|
||
```
|
||
|
||
### Schema Builder Integration
|
||
|
||
Der Schema Builder unterstützt sowohl VOs als auch Strings (Union Types) für Backwards Compatibility:
|
||
|
||
```php
|
||
use App\Framework\Database\Schema\Blueprint;
|
||
use App\Framework\Database\ValueObjects\{TableName, ColumnName, IndexName};
|
||
|
||
// ✅ Modern: Value Objects (Type Safe)
|
||
$schema->create(TableName::fromString('users'), function (Blueprint $table) {
|
||
$table->string(ColumnName::fromString('email'), 255);
|
||
$table->unique(ColumnName::fromString('email'), IndexName::fromString('uk_users_email'));
|
||
});
|
||
|
||
// ✅ Legacy: Strings (Backwards Compatible)
|
||
$schema->create('users', function (Blueprint $table) {
|
||
$table->string('email', 255);
|
||
$table->unique('email', 'uk_users_email');
|
||
});
|
||
|
||
// ✅ Mixed: Kombiniert möglich
|
||
$schema->create('users', function (Blueprint $table) {
|
||
$table->string('email', 255); // String
|
||
$table->unique(
|
||
ColumnName::fromString('email'), // VO
|
||
'uk_users_email' // String
|
||
);
|
||
});
|
||
```
|
||
|
||
### Variadic Parameters
|
||
|
||
Command-Methoden nutzen variadic parameters statt Arrays für natürlichere API:
|
||
|
||
```php
|
||
// ✅ Variadic Parameters - Natural
|
||
$table->dropColumn('email', 'phone', 'address');
|
||
|
||
// ❌ Array Parameters - Clunky
|
||
$table->dropColumn(['email', 'phone', 'address']);
|
||
|
||
// ✅ Auch mit VOs
|
||
$table->dropColumn(
|
||
ColumnName::fromString('email'),
|
||
ColumnName::fromString('phone')
|
||
);
|
||
```
|
||
|
||
### TableName mit Schema-Support
|
||
|
||
PostgreSQL unterstützt Schema-Prefixe:
|
||
|
||
```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"
|
||
|
||
// Schema extraction
|
||
if ($table->hasSchema()) {
|
||
echo $table->schema->value; // "public"
|
||
}
|
||
```
|
||
|
||
### Validation Rules
|
||
|
||
Alle Database VOs validieren ihre Eingaben:
|
||
|
||
**TableName Validation**:
|
||
- Keine leeren Strings
|
||
- Alphanumerisch + Underscore
|
||
- Beginnt nicht mit Ziffer
|
||
- Max. 63 Zeichen (PostgreSQL Limit)
|
||
- Reservierte Keywords verboten (siehe `RESERVED_KEYWORDS`)
|
||
|
||
**ColumnName Validation**:
|
||
- Keine leeren Strings
|
||
- Alphanumerisch + Underscore
|
||
- Beginnt nicht mit Ziffer
|
||
- Max. 63 Zeichen
|
||
- Reservierte Keywords verboten
|
||
|
||
**IndexName/ConstraintName**:
|
||
- Gleiche Rules wie ColumnName
|
||
- Prefix-Conventions: `idx_`, `uk_`, `fk_`, `ck_` empfohlen
|
||
|
||
**Example Validation**:
|
||
```php
|
||
// ✅ Valid
|
||
TableName::fromString('users');
|
||
TableName::fromString('user_profiles');
|
||
|
||
// ❌ Invalid - Exception geworfen
|
||
TableName::fromString(''); // Leer
|
||
TableName::fromString('123users'); // Beginnt mit Ziffer
|
||
TableName::fromString('user-table'); // Ungültiges Zeichen
|
||
TableName::fromString('select'); // Reserviertes Keyword
|
||
```
|
||
|
||
## Migration Best Practices
|
||
|
||
### Migration mit Database VOs
|
||
|
||
Neue Migrationen können VOs verwenden für zusätzliche Type Safety:
|
||
|
||
```php
|
||
use App\Framework\Database\Migration\Migration;
|
||
use App\Framework\Database\Schema\{Blueprint, Schema};
|
||
use App\Framework\Database\ValueObjects\{TableName, ColumnName, IndexName, ConstraintName};
|
||
use App\Framework\Database\Schema\ForeignKeyAction;
|
||
|
||
final class CreateUserProfilesTable implements Migration
|
||
{
|
||
public function up(Schema $schema): void
|
||
{
|
||
$schema->create(TableName::fromString('user_profiles'), function (Blueprint $table) {
|
||
// Primary Key
|
||
$table->string(ColumnName::fromString('ulid'), 26)
|
||
->primary();
|
||
|
||
// Foreign Key
|
||
$table->string(ColumnName::fromString('user_id'), 26);
|
||
|
||
// Relationship
|
||
$table->foreign(ColumnName::fromString('user_id'))
|
||
->references(ColumnName::fromString('ulid'))
|
||
->on(TableName::fromString('users'))
|
||
->onDelete(ForeignKeyAction::CASCADE);
|
||
|
||
// Index
|
||
$table->index(
|
||
ColumnName::fromString('user_id'),
|
||
IndexName::fromString('idx_user_profiles_user_id')
|
||
);
|
||
|
||
// Composite Index mit variadic parameters
|
||
$table->index(
|
||
ColumnName::fromString('user_id'),
|
||
ColumnName::fromString('created_at'),
|
||
IndexName::fromString('idx_user_profiles_lookup')
|
||
);
|
||
|
||
// Timestamps
|
||
$table->timestamps();
|
||
});
|
||
}
|
||
|
||
public function down(Schema $schema): void
|
||
{
|
||
$schema->dropIfExists(TableName::fromString('user_profiles'));
|
||
}
|
||
}
|
||
```
|
||
|
||
### Migration Naming Conventions
|
||
|
||
**Index Names**:
|
||
```php
|
||
// Pattern: idx_{table}_{columns}
|
||
IndexName::fromString('idx_users_email')
|
||
IndexName::fromString('idx_users_email_active')
|
||
|
||
// Unique constraints: uk_{table}_{columns}
|
||
IndexName::fromString('uk_users_email')
|
||
|
||
// Composite
|
||
IndexName::fromString('idx_orders_user_created')
|
||
```
|
||
|
||
**Foreign Key Constraints**:
|
||
```php
|
||
// Pattern: fk_{table}_{referenced_table}
|
||
ConstraintName::fromString('fk_user_profiles_users')
|
||
ConstraintName::fromString('fk_orders_users')
|
||
```
|
||
|
||
**Check Constraints**:
|
||
```php
|
||
// Pattern: ck_{table}_{column}_{condition}
|
||
ConstraintName::fromString('ck_users_age_positive')
|
||
ConstraintName::fromString('ck_orders_total_min')
|
||
```
|
||
|
||
### Backwards Compatibility
|
||
|
||
Bestehende Migrationen funktionieren ohne Änderungen:
|
||
|
||
```php
|
||
// ✅ Legacy-Code bleibt voll funktionsfähig
|
||
$schema->create('users', function (Blueprint $table) {
|
||
$table->string('email');
|
||
$table->unique('email', 'uk_users_email');
|
||
});
|
||
|
||
// Union Types ermöglichen schrittweise Migration
|
||
// string|TableName, string|ColumnName, etc.
|
||
```
|
||
|
||
### Drop Operations
|
||
|
||
Variadic parameters machen Drop-Operationen natürlich:
|
||
|
||
```php
|
||
$schema->table('users', function (Blueprint $table) {
|
||
// Drop multiple columns
|
||
$table->dropColumn('old_field', 'deprecated_field', 'unused_field');
|
||
|
||
// Drop foreign key by constraint name
|
||
$table->dropForeign('fk_users_company');
|
||
|
||
// Drop foreign key by column (finds constraint automatically)
|
||
$table->dropForeign('company_id');
|
||
|
||
// Drop index by name
|
||
$table->dropIndex('idx_users_email');
|
||
|
||
// Drop index by columns (finds index automatically)
|
||
$table->dropIndex('email', 'active');
|
||
});
|
||
```
|
||
|
||
## EntityManager Usage
|
||
|
||
### Overview
|
||
|
||
Der EntityManager ist das zentrale Interface für alle Datenbank-Operationen. Er kombiniert mehrere Patterns:
|
||
- **Unit of Work**: Change Tracking und Transaction Management
|
||
- **Identity Map**: Vermeidung von Duplikaten
|
||
- **Lazy Loading**: Performance-Optimierung durch verzögertes Laden
|
||
- **Batch Loading**: N+1 Query Prevention
|
||
|
||
### Core Features
|
||
|
||
**Service Classes** (interne Organisation):
|
||
- `EntityFinder`: Entity-Suche und Lazy Loading
|
||
- `EntityPersister`: Insert, Update, Delete Operationen
|
||
- `EntityQueryManager`: QueryBuilder Integration
|
||
- `EntityUtilities`: Hilfsmethoden und Profiling
|
||
|
||
### Basic Operations
|
||
|
||
#### Finding Entities
|
||
|
||
```php
|
||
use App\Framework\Database\EntityManager;
|
||
|
||
final readonly class UserService
|
||
{
|
||
public function __construct(
|
||
private EntityManager $entityManager
|
||
) {}
|
||
|
||
public function getUserById(UserId $id): ?User
|
||
{
|
||
// Lazy Loading (Standard)
|
||
return $this->entityManager->find(User::class, $id->value);
|
||
}
|
||
|
||
public function getUserByIdEager(UserId $id): ?User
|
||
{
|
||
// Eager Loading - lädt Relations sofort
|
||
return $this->entityManager->findEager(User::class, $id->value);
|
||
}
|
||
|
||
public function getAllUsers(): array
|
||
{
|
||
return $this->entityManager->findAll(User::class);
|
||
}
|
||
|
||
public function findUserByEmail(Email $email): ?User
|
||
{
|
||
return $this->entityManager->findOneBy(
|
||
User::class,
|
||
['email' => $email->value]
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
#### Saving Entities
|
||
|
||
```php
|
||
public function createUser(CreateUserCommand $command): User
|
||
{
|
||
$user = User::create(
|
||
id: $this->entityManager->generateId(),
|
||
email: $command->email,
|
||
name: $command->name
|
||
);
|
||
|
||
// save() erkennt automatisch INSERT vs UPDATE
|
||
return $this->entityManager->save($user);
|
||
}
|
||
|
||
public function updateUser(User $user, UpdateUserCommand $command): User
|
||
{
|
||
// Explizites UPDATE
|
||
$user->updateProfile($command->name);
|
||
return $this->entityManager->update($user);
|
||
}
|
||
```
|
||
|
||
#### Batch Operations
|
||
|
||
```php
|
||
public function createMultipleUsers(array $commands): array
|
||
{
|
||
$users = [];
|
||
foreach ($commands as $command) {
|
||
$users[] = User::create(
|
||
id: $this->entityManager->generateId(),
|
||
email: $command->email,
|
||
name: $command->name
|
||
);
|
||
}
|
||
|
||
// Batch-Insert für bessere Performance
|
||
return $this->entityManager->saveAll(...$users);
|
||
}
|
||
```
|
||
|
||
#### Deleting Entities
|
||
|
||
```php
|
||
public function deleteUser(User $user): void
|
||
{
|
||
$this->entityManager->delete($user);
|
||
}
|
||
```
|
||
|
||
### N+1 Query Prevention
|
||
|
||
**Problem**: Lazy Loading führt zu N+1 Queries
|
||
|
||
```php
|
||
// ❌ N+1 Problem
|
||
$users = $this->entityManager->findAll(User::class);
|
||
|
||
foreach ($users as $user) {
|
||
// Jede Iteration führt eine separate Query aus (N+1)
|
||
echo $user->getProfile()->bio;
|
||
}
|
||
```
|
||
|
||
**Solution**: Batch Loading mit Relations
|
||
|
||
```php
|
||
// ✅ Single Query mit Batch Loading
|
||
$users = $this->entityManager->findWithRelations(
|
||
User::class,
|
||
criteria: [],
|
||
relations: ['profile', 'posts'] // Lädt Relations in Batches
|
||
);
|
||
|
||
foreach ($users as $user) {
|
||
// Kein zusätzlicher Query - Profile bereits geladen
|
||
echo $user->getProfile()->bio;
|
||
}
|
||
```
|
||
|
||
### Lazy Loading vs Eager Loading
|
||
|
||
**Lazy Loading** (Standard):
|
||
- Relations werden bei Zugriff geladen
|
||
- Performance-Vorteil bei ungenutzten Relations
|
||
- Potentielles N+1 Problem
|
||
|
||
```php
|
||
$user = $this->entityManager->find(User::class, $id);
|
||
// Profile wird erst bei Zugriff geladen:
|
||
$bio = $user->getProfile()->bio; // Separate Query
|
||
```
|
||
|
||
**Eager Loading**:
|
||
- Relations werden sofort geladen
|
||
- Kein N+1 Problem
|
||
- Overhead bei ungenutzten Relations
|
||
|
||
```php
|
||
$user = $this->entityManager->findEager(User::class, $id);
|
||
// Profile ist bereits geladen - kein zusätzlicher Query
|
||
$bio = $user->getProfile()->bio;
|
||
```
|
||
|
||
### Identity Map
|
||
|
||
Der EntityManager nutzt eine Identity Map zur Vermeidung von Duplikaten:
|
||
|
||
```php
|
||
// Beide Aufrufe liefern die GLEICHE Instanz
|
||
$user1 = $this->entityManager->find(User::class, '123');
|
||
$user2 = $this->entityManager->find(User::class, '123');
|
||
|
||
var_dump($user1 === $user2); // true
|
||
```
|
||
|
||
**Vorteile**:
|
||
- Keine Duplikate im Speicher
|
||
- Konsistente Object Identity
|
||
- Automatisches Change Tracking
|
||
|
||
**Utility Methods**:
|
||
|
||
```php
|
||
// Detach Entity (aus Identity Map entfernen)
|
||
$this->entityManager->detach($user);
|
||
|
||
// Clear Identity Map (z.B. für Batch Processing)
|
||
$this->entityManager->clear();
|
||
|
||
// Identity Map Statistics
|
||
$stats = $this->entityManager->getIdentityMapStats();
|
||
// Returns: ['total_entities' => 150, 'entities_by_class' => [...]]
|
||
```
|
||
|
||
### Reference Loading
|
||
|
||
**getReference()** - Lade Entity-Referenz ohne Existenz-Check:
|
||
|
||
```php
|
||
// Erstellt Proxy ohne Database Query
|
||
$user = $this->entityManager->getReference(User::class, $userId);
|
||
|
||
// Query wird erst bei Zugriff ausgeführt
|
||
$name = $user->getName(); // Jetzt wird geladen
|
||
```
|
||
|
||
**Use Cases**:
|
||
- Foreign Key Relationships
|
||
- Performance-kritische Pfade
|
||
- Wenn Existenz bereits bekannt ist
|
||
|
||
### Profiling und Debugging
|
||
|
||
```php
|
||
// Profiling aktivieren
|
||
$this->entityManager->setProfilingEnabled(true);
|
||
|
||
// Operationen ausführen
|
||
$users = $this->entityManager->findAll(User::class);
|
||
|
||
// Profiling-Statistiken abrufen
|
||
$stats = $this->entityManager->getProfilingStatistics();
|
||
|
||
// Profiling-Summary
|
||
$summary = $this->entityManager->getProfilingSummary();
|
||
// Returns: ProfileSummary mit total_queries, total_time, etc.
|
||
|
||
// Profiling-Daten löschen
|
||
$this->entityManager->clearProfilingData();
|
||
```
|
||
|
||
### Criteria API (Type-Safe Queries)
|
||
|
||
**Modern**: Verwendung von Criteria statt Arrays
|
||
|
||
```php
|
||
use App\Framework\Database\Criteria\DetachedCriteria;
|
||
|
||
// Type-safe Criteria
|
||
$criteria = DetachedCriteria::forEntity(User::class)
|
||
->where('email', '=', $email->value)
|
||
->andWhere('active', '=', true)
|
||
->orderBy('created_at', 'DESC')
|
||
->limit(10);
|
||
|
||
$users = $this->entityManager->findByCriteria($criteria);
|
||
$user = $this->entityManager->findOneByCriteria($criteria);
|
||
$count = $this->entityManager->countByCriteria($criteria);
|
||
```
|
||
|
||
### QueryBuilder Integration
|
||
|
||
Für komplexe Queries:
|
||
|
||
```php
|
||
// QueryBuilder für Entity-Klasse
|
||
$queryBuilder = $this->entityManager->createQueryBuilderFor(User::class);
|
||
|
||
$users = $queryBuilder
|
||
->select('*')
|
||
->where('email LIKE ?', ['%@example.com'])
|
||
->andWhere('created_at > ?', ['2024-01-01'])
|
||
->orderBy('name', 'ASC')
|
||
->limit(50)
|
||
->fetchAll();
|
||
|
||
// QueryBuilder für Table (ohne Entity)
|
||
$qb = $this->entityManager->createQueryBuilderForTable('users');
|
||
```
|
||
|
||
### Domain Events Integration
|
||
|
||
```php
|
||
// Domain Event aufzeichnen
|
||
$this->entityManager->recordDomainEvent($user, new UserRegisteredEvent($user));
|
||
|
||
// Events für Entity dispatchen
|
||
$this->entityManager->dispatchDomainEventsForEntity($user);
|
||
|
||
// Alle Events dispatchen
|
||
$this->entityManager->dispatchAllDomainEvents();
|
||
|
||
// Event-Statistiken
|
||
$stats = $this->entityManager->getDomainEventStats();
|
||
// Returns: ['total_events' => 23, 'events_by_entity' => [...]]
|
||
```
|
||
|
||
## Repository Pattern
|
||
|
||
### Overview
|
||
|
||
Das Repository Pattern abstrahiert Daten-Zugriff und bietet domain-spezifische Query-Methoden. Das Framework nutzt **Composition over Inheritance**.
|
||
|
||
### Base EntityRepository
|
||
|
||
**Framework-Service** für Common Operations:
|
||
|
||
```php
|
||
namespace App\Framework\Database\Repository;
|
||
|
||
final readonly class EntityRepository
|
||
{
|
||
public function __construct(
|
||
private EntityManager $entityManager
|
||
) {}
|
||
|
||
public function find(string $entityClass, string $id): ?object
|
||
public function findAll(string $entityClass): array
|
||
public function findBy(string $entityClass, array $criteria, ...): array
|
||
public function findOneBy(string $entityClass, array $criteria): ?object
|
||
public function save(object $entity): object
|
||
public function delete(object $entity): void
|
||
public function transaction(callable $callback): mixed
|
||
|
||
// Batch Loading (N+1 Prevention)
|
||
public function findWithRelations(...): array
|
||
|
||
// Pagination
|
||
public function findPaginated(...): PaginatedResult
|
||
|
||
// Batch Operations
|
||
public function saveBatch(array $entities, int $batchSize = 100): array
|
||
public function deleteBatch(array $entities, int $batchSize = 100): void
|
||
}
|
||
```
|
||
|
||
### Domain Repository Pattern
|
||
|
||
**Composition-basiert** statt Inheritance:
|
||
|
||
```php
|
||
namespace App\Domain\User\Repositories;
|
||
|
||
use App\Framework\Database\Repository\EntityRepository;
|
||
|
||
final readonly class UserRepository
|
||
{
|
||
public function __construct(
|
||
private EntityRepository $entityRepository
|
||
) {}
|
||
|
||
public function findById(UserId $id): ?User
|
||
{
|
||
return $this->entityRepository->find(User::class, $id->value);
|
||
}
|
||
|
||
public function findByEmail(Email $email): ?User
|
||
{
|
||
return $this->entityRepository->findOneBy(
|
||
User::class,
|
||
['email' => $email->value]
|
||
);
|
||
}
|
||
|
||
public function findActiveUsers(): array
|
||
{
|
||
return $this->entityRepository->findBy(
|
||
User::class,
|
||
criteria: ['active' => true],
|
||
orderBy: ['created_at' => 'DESC']
|
||
);
|
||
}
|
||
|
||
public function save(User $user): User
|
||
{
|
||
return $this->entityRepository->save($user);
|
||
}
|
||
|
||
public function delete(User $user): void
|
||
{
|
||
$this->entityRepository->delete($user);
|
||
}
|
||
|
||
// Domain-spezifische Methode mit Batch Loading
|
||
public function findUsersWithProfiles(array $userIds): array
|
||
{
|
||
return $this->entityRepository->findWithRelations(
|
||
User::class,
|
||
criteria: ['id' => $userIds],
|
||
relations: ['profile', 'settings']
|
||
);
|
||
}
|
||
|
||
// Pagination
|
||
public function findPaginated(int $page, int $limit = 20): PaginatedResult
|
||
{
|
||
return $this->entityRepository->findPaginated(
|
||
User::class,
|
||
page: $page,
|
||
limit: $limit,
|
||
orderBy: ['created_at' => 'DESC']
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
### Repository Best Practices
|
||
|
||
**✅ Composition Over Inheritance**:
|
||
```php
|
||
// ✅ Framework Pattern - Composition
|
||
final readonly class OrderRepository
|
||
{
|
||
public function __construct(
|
||
private EntityRepository $entityRepository
|
||
) {}
|
||
}
|
||
|
||
// ❌ Avoid - Inheritance
|
||
class OrderRepository extends BaseRepository
|
||
{
|
||
// Problematisch: Tight coupling
|
||
}
|
||
```
|
||
|
||
**✅ Domain-Specific Methods**:
|
||
```php
|
||
// ✅ Sprechende Domain-Methoden
|
||
public function findOverdueOrders(): array
|
||
{
|
||
return $this->entityRepository->findBy(
|
||
Order::class,
|
||
['status' => 'pending'],
|
||
orderBy: ['due_date' => 'ASC']
|
||
);
|
||
}
|
||
|
||
// ❌ Generic Methods außerhalb Domain
|
||
public function findBy(array $criteria): array
|
||
{
|
||
// Zu generisch - nutze EntityRepository direkt
|
||
}
|
||
```
|
||
|
||
**✅ N+1 Prevention**:
|
||
```php
|
||
// ✅ Batch Loading für Relations
|
||
public function findOrdersWithItems(array $orderIds): array
|
||
{
|
||
return $this->entityRepository->findWithRelations(
|
||
Order::class,
|
||
criteria: ['id' => $orderIds],
|
||
relations: ['items', 'customer'] // Lädt in Batches
|
||
);
|
||
}
|
||
```
|
||
|
||
## Unit of Work Pattern
|
||
|
||
### Overview
|
||
|
||
Der Unit of Work Pattern verwaltet Entities und deren Änderungen für transaktionale Konsistenz.
|
||
|
||
**Key Features**:
|
||
- **Change Tracking**: Automatische Änderungserkennung
|
||
- **Transactional Consistency**: Atomic Commits/Rollbacks
|
||
- **Batch Operations**: Optimierte Bulk-Operationen
|
||
- **Entity States**: NEW, MANAGED, DIRTY, DELETED, DETACHED
|
||
|
||
### Entity Lifecycle
|
||
|
||
```php
|
||
use App\Framework\Database\UnitOfWork\UnitOfWork;
|
||
|
||
final readonly class OrderService
|
||
{
|
||
public function __construct(
|
||
private EntityManager $entityManager
|
||
) {}
|
||
|
||
public function createOrder(CreateOrderCommand $command): Order
|
||
{
|
||
$unitOfWork = $this->entityManager->unitOfWork;
|
||
|
||
// 1. Create new entity (state: NEW)
|
||
$order = Order::create($command);
|
||
|
||
// 2. Persist marks entity for INSERT (state: NEW → MANAGED)
|
||
$unitOfWork->persist($order);
|
||
|
||
// 3. Flush writes to database (state: MANAGED)
|
||
$unitOfWork->flush();
|
||
|
||
// 4. Commit transaction
|
||
$unitOfWork->commit();
|
||
|
||
return $order;
|
||
}
|
||
}
|
||
```
|
||
|
||
### Change Tracking
|
||
|
||
**Automatic Change Detection**:
|
||
|
||
```php
|
||
$unitOfWork = $this->entityManager->unitOfWork;
|
||
|
||
// Load entity (state: MANAGED)
|
||
$user = $this->entityManager->find(User::class, $userId);
|
||
|
||
// Modify entity
|
||
$user->updateProfile($newName);
|
||
|
||
// Change Tracking erkennt Änderung automatisch
|
||
$unitOfWork->flush(); // UPDATE query wird generiert
|
||
$unitOfWork->commit();
|
||
```
|
||
|
||
**Manual Change Tracking**:
|
||
|
||
```php
|
||
// Entity aus externer Quelle (state: DETACHED)
|
||
$user = unserialize($serializedUser);
|
||
|
||
// Merge in Unit of Work (state: DETACHED → MANAGED)
|
||
$managedUser = $unitOfWork->merge($user);
|
||
|
||
// Änderungen werden getrackt
|
||
$unitOfWork->flush();
|
||
```
|
||
|
||
### Transaction Management
|
||
|
||
**Explicit Transactions**:
|
||
|
||
```php
|
||
public function processOrder(Order $order): void
|
||
{
|
||
$unitOfWork = $this->entityManager->unitOfWork;
|
||
|
||
try {
|
||
$unitOfWork->beginTransaction();
|
||
|
||
// Multiple operations
|
||
$order->confirm();
|
||
$unitOfWork->persist($order);
|
||
|
||
$this->inventoryService->reserve($order->items);
|
||
$this->paymentService->charge($order->total);
|
||
|
||
// Write changes to database
|
||
$unitOfWork->flush();
|
||
|
||
// Commit transaction
|
||
$unitOfWork->commit();
|
||
} catch (\Exception $e) {
|
||
$unitOfWork->rollback();
|
||
throw $e;
|
||
}
|
||
}
|
||
```
|
||
|
||
**Implicit Transactions (Auto-Commit)**:
|
||
|
||
```php
|
||
// Auto-commit ist standardmäßig aktiviert
|
||
$unitOfWork->persist($entity); // Automatisch committed
|
||
```
|
||
|
||
**Disable Auto-Commit** für Batch Operations:
|
||
|
||
```php
|
||
$unitOfWork->setAutoCommit(false);
|
||
|
||
try {
|
||
$unitOfWork->beginTransaction();
|
||
|
||
foreach ($orders as $order) {
|
||
$unitOfWork->persist($order); // Kein Auto-Commit
|
||
}
|
||
|
||
$unitOfWork->flush(); // Batch-Write
|
||
$unitOfWork->commit(); // Atomic Commit
|
||
} catch (\Exception $e) {
|
||
$unitOfWork->rollback();
|
||
throw $e;
|
||
} finally {
|
||
$unitOfWork->setAutoCommit(true);
|
||
}
|
||
```
|
||
|
||
### Entity States
|
||
|
||
```php
|
||
use App\Framework\Database\UnitOfWork\EntityState;
|
||
|
||
// Check Entity State
|
||
$state = $unitOfWork->getEntityState($entity);
|
||
|
||
match ($state) {
|
||
EntityState::NEW => 'Entity marked for INSERT',
|
||
EntityState::MANAGED => 'Entity being tracked, no changes',
|
||
EntityState::DIRTY => 'Entity modified, pending UPDATE',
|
||
EntityState::DELETED => 'Entity marked for DELETE',
|
||
EntityState::DETACHED => 'Entity not managed by UnitOfWork',
|
||
};
|
||
|
||
// Check if entity is managed
|
||
if ($unitOfWork->contains($entity)) {
|
||
// Entity is managed (NEW, MANAGED, DIRTY, or DELETED)
|
||
}
|
||
```
|
||
|
||
### Detaching Entities
|
||
|
||
```php
|
||
// Detach entity from Unit of Work
|
||
$unitOfWork->detach($user);
|
||
|
||
// Entity state: DETACHED
|
||
// Changes to entity are no longer tracked
|
||
```
|
||
|
||
**Use Cases**:
|
||
- Long-running processes
|
||
- Serialization/Caching
|
||
- Read-only operations
|
||
- Memory management in batch processing
|
||
|
||
### Bulk Operations
|
||
|
||
```php
|
||
// Bulk Insert
|
||
$unitOfWork->bulkInsert($entities); // Optimized batch INSERT
|
||
|
||
// Bulk Update
|
||
$unitOfWork->bulkUpdate($entities); // Optimized batch UPDATE
|
||
|
||
// Bulk Delete
|
||
$unitOfWork->bulkDelete($entities); // Optimized batch DELETE
|
||
```
|
||
|
||
## Query Optimization
|
||
|
||
### N+1 Query Problem
|
||
|
||
**Erkennnung**: Jede Iteration führt zusätzliche Query aus
|
||
|
||
```php
|
||
// ❌ N+1 Problem
|
||
$orders = $this->entityManager->findAll(Order::class); // 1 Query
|
||
|
||
foreach ($orders as $order) {
|
||
echo $order->getCustomer()->name; // N Queries (1 pro Order)
|
||
}
|
||
// Total: 1 + N Queries
|
||
```
|
||
|
||
### Solution: Batch Loading
|
||
|
||
```php
|
||
// ✅ Batch Loading
|
||
$orders = $this->entityManager->findWithRelations(
|
||
Order::class,
|
||
criteria: [],
|
||
relations: ['customer', 'items'] // Preload relations
|
||
);
|
||
|
||
foreach ($orders as $order) {
|
||
echo $order->getCustomer()->name; // Kein Query - bereits geladen
|
||
}
|
||
// Total: 2-3 Queries (1 für Orders, 1-2 für Relations)
|
||
```
|
||
|
||
### Eager vs Lazy Loading
|
||
|
||
**Strategic Loading**:
|
||
|
||
```php
|
||
// Heavy Operation - Eager Loading
|
||
public function getOrderDetails(OrderId $id): OrderDetails
|
||
{
|
||
$order = $this->entityManager->findEager(Order::class, $id->value);
|
||
// Alle Relations sofort geladen
|
||
return OrderDetails::fromOrder($order);
|
||
}
|
||
|
||
// Light Operation - Lazy Loading
|
||
public function listOrders(): array
|
||
{
|
||
// Relations werden nur bei Bedarf geladen
|
||
return $this->entityManager->findAll(Order::class);
|
||
}
|
||
```
|
||
|
||
### Pagination
|
||
|
||
```php
|
||
use App\Framework\Database\Repository\EntityRepository;
|
||
|
||
public function getUsers(int $page): PaginatedResult
|
||
{
|
||
return $this->entityRepository->findPaginated(
|
||
User::class,
|
||
page: $page,
|
||
limit: 20,
|
||
orderBy: ['created_at' => 'DESC']
|
||
);
|
||
}
|
||
|
||
// PaginatedResult enthält:
|
||
// - items: array (Entities)
|
||
// - total: int (Total count)
|
||
// - page: int (Current page)
|
||
// - limit: int (Items per page)
|
||
// - totalPages: int (Calculated)
|
||
```
|
||
|
||
### Query Caching
|
||
|
||
```php
|
||
// EntityManager mit Cache
|
||
$entityManager = new EntityManager(
|
||
// ...
|
||
cacheManager: $cacheManager
|
||
);
|
||
|
||
// Find verwendet Cache automatisch
|
||
$user = $entityManager->find(User::class, $id);
|
||
// Beim zweiten Aufruf: Cache Hit
|
||
```
|
||
|
||
## Connection Pooling
|
||
|
||
### Overview
|
||
|
||
Connection Pooling verbessert Performance durch Wiederverwendung von Datenbankverbindungen.
|
||
|
||
**Features**:
|
||
- Min/Max Connection Limits
|
||
- Connection Health Monitoring
|
||
- Automatic Retry mit Exponential Backoff
|
||
- Idle Connection Cleanup
|
||
- Connection Warmup
|
||
|
||
### Configuration
|
||
|
||
```php
|
||
use App\Framework\Database\Config\PoolConfig;
|
||
|
||
$poolConfig = new PoolConfig(
|
||
minConnections: 2, // Minimum pool size
|
||
maxConnections: 10, // Maximum pool size
|
||
maxIdleTime: 300, // Idle timeout (seconds)
|
||
healthCheckInterval: 60, // Health check frequency
|
||
enableWarmup: true // Pre-create min connections
|
||
);
|
||
```
|
||
|
||
### Usage
|
||
|
||
```php
|
||
use App\Framework\Database\ConnectionPool;
|
||
|
||
$pool = new ConnectionPool($driverConfig, $poolConfig, $timer);
|
||
|
||
// Get connection from pool
|
||
$connection = $pool->getConnection();
|
||
|
||
try {
|
||
// Use connection
|
||
$result = $connection->query('SELECT * FROM users');
|
||
} finally {
|
||
// Return connection to pool (automatic with PooledConnection)
|
||
$connection->release();
|
||
}
|
||
```
|
||
|
||
### Health Monitoring
|
||
|
||
```php
|
||
// Pool automatically monitors connection health
|
||
$pool->performHealthCheck();
|
||
|
||
// Get pool statistics
|
||
$stats = $pool->getPoolStatistics();
|
||
// Returns:
|
||
[
|
||
'current_connections' => 5,
|
||
'max_connections' => 10,
|
||
'connections_in_use' => 3,
|
||
'healthy_connections' => 5,
|
||
'total_created' => 12,
|
||
'total_destroyed' => 7
|
||
]
|
||
```
|
||
|
||
### Retry Logic
|
||
|
||
```php
|
||
// Automatic retry with exponential backoff
|
||
$retryStrategy = ExponentialBackoffStrategy::create()
|
||
->withMaxAttempts(3)
|
||
->withBaseDelay(Duration::fromMilliseconds(100));
|
||
|
||
// Connection pool uses retry automatically
|
||
$connection = $pool->getConnectionWithRetry($retryStrategy);
|
||
```
|
||
|
||
### Production Configuration
|
||
|
||
```php
|
||
// Production Pool Config
|
||
$productionPoolConfig = new PoolConfig(
|
||
minConnections: 5, // Keep 5 connections ready
|
||
maxConnections: 50, // Scale up to 50 under load
|
||
maxIdleTime: 600, // 10 minutes idle timeout
|
||
healthCheckInterval: 30, // Check every 30 seconds
|
||
enableWarmup: true, // Pre-warm connections
|
||
connectionTimeout: 5000 // 5 second timeout
|
||
);
|
||
```
|
||
|
||
## Transaction Management
|
||
|
||
### Basic Transactions
|
||
|
||
```php
|
||
// EntityManager Transaction Helper
|
||
$this->entityManager->transaction(function (EntityManager $em) {
|
||
$order = Order::create($command);
|
||
$em->save($order);
|
||
|
||
$this->inventoryService->reserve($order->items);
|
||
$this->paymentService->charge($order->total);
|
||
|
||
// Automatic commit on success, rollback on exception
|
||
});
|
||
```
|
||
|
||
### Manual Transaction Control
|
||
|
||
```php
|
||
$unitOfWork = $this->entityManager->unitOfWork;
|
||
|
||
$unitOfWork->beginTransaction();
|
||
|
||
try {
|
||
// Operations
|
||
$unitOfWork->persist($entity1);
|
||
$unitOfWork->persist($entity2);
|
||
|
||
$unitOfWork->flush();
|
||
$unitOfWork->commit();
|
||
} catch (\Exception $e) {
|
||
$unitOfWork->rollback();
|
||
throw $e;
|
||
}
|
||
```
|
||
|
||
### Nested Transactions (Savepoints)
|
||
|
||
```php
|
||
$unitOfWork->beginTransaction();
|
||
|
||
try {
|
||
$order = Order::create($command);
|
||
$unitOfWork->persist($order);
|
||
|
||
// Nested transaction (savepoint)
|
||
$unitOfWork->beginTransaction();
|
||
try {
|
||
$this->inventoryService->reserve($order->items);
|
||
$unitOfWork->commit(); // Commit savepoint
|
||
} catch (\Exception $e) {
|
||
$unitOfWork->rollback(); // Rollback to savepoint
|
||
throw $e;
|
||
}
|
||
|
||
$unitOfWork->commit(); // Commit main transaction
|
||
} catch (\Exception $e) {
|
||
$unitOfWork->rollback(); // Rollback main transaction
|
||
throw $e;
|
||
}
|
||
```
|
||
|
||
### Transaction Isolation Levels
|
||
|
||
```php
|
||
use App\Framework\Database\TransactionIsolation;
|
||
|
||
$unitOfWork->beginTransaction(
|
||
isolation: TransactionIsolation::READ_COMMITTED
|
||
);
|
||
|
||
// Available isolation levels:
|
||
// - READ_UNCOMMITTED (lowest isolation)
|
||
// - READ_COMMITTED (default)
|
||
// - REPEATABLE_READ
|
||
// - SERIALIZABLE (highest isolation)
|
||
```
|
||
|
||
### Transaction Best Practices
|
||
|
||
**✅ Short Transactions**:
|
||
```php
|
||
// ✅ Keep transactions short
|
||
$this->entityManager->transaction(fn(EntityManager $em) =>
|
||
$em->save($entity)
|
||
);
|
||
```
|
||
|
||
**❌ Long Transactions**:
|
||
```php
|
||
// ❌ Avoid long-running transactions
|
||
$this->entityManager->transaction(function (EntityManager $em) {
|
||
$this->sendEmail($user); // External I/O - BAD
|
||
$this->processHeavyComputation(); // CPU-intensive - BAD
|
||
$em->save($entity);
|
||
});
|
||
```
|
||
|
||
**✅ Batch with Periodic Commits**:
|
||
```php
|
||
// ✅ Batch processing with periodic commits
|
||
$unitOfWork->setAutoCommit(false);
|
||
$batchSize = 100;
|
||
|
||
for ($i = 0; $i < count($items); $i++) {
|
||
$unitOfWork->persist($items[$i]);
|
||
|
||
if (($i + 1) % $batchSize === 0) {
|
||
$unitOfWork->flush();
|
||
$unitOfWork->commit();
|
||
$unitOfWork->beginTransaction();
|
||
}
|
||
}
|
||
|
||
$unitOfWork->flush();
|
||
$unitOfWork->commit();
|
||
$unitOfWork->setAutoCommit(true);
|
||
```
|
||
|
||
## Database Testing
|
||
|
||
### Testing mit Database VOs
|
||
|
||
```php
|
||
use App\Framework\Database\ValueObjects\{TableName, ColumnName};
|
||
|
||
it('creates table with value objects', function () {
|
||
$tableName = TableName::fromString('test_users');
|
||
|
||
$this->schema->create($tableName, function (Blueprint $table) {
|
||
$table->string(ColumnName::fromString('email'));
|
||
});
|
||
|
||
expect($this->schema->hasTable($tableName))->toBeTrue();
|
||
});
|
||
|
||
it('validates table name format', function () {
|
||
TableName::fromString('123invalid'); // Should throw
|
||
})->throws(\InvalidArgumentException::class);
|
||
```
|
||
|
||
### Test Cleanup
|
||
|
||
```php
|
||
afterEach(function () {
|
||
// Cleanup mit VOs
|
||
$this->schema->dropIfExists(TableName::fromString('test_users'));
|
||
$this->schema->dropIfExists(TableName::fromString('test_profiles'));
|
||
});
|
||
```
|
||
|
||
## Performance Considerations
|
||
|
||
### Value Object Overhead
|
||
|
||
Database VOs haben minimalen Performance-Overhead:
|
||
|
||
- **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 Strategies
|
||
|
||
```php
|
||
// VO Instances können gecached werden
|
||
final class TableNameCache
|
||
{
|
||
private static array $cache = [];
|
||
|
||
public static function get(string $name): TableName
|
||
{
|
||
return self::$cache[$name] ??= TableName::fromString($name);
|
||
}
|
||
}
|
||
|
||
// Usage
|
||
$users = TableNameCache::get('users');
|
||
$profiles = TableNameCache::get('user_profiles');
|
||
```
|
||
|
||
## Migration Pattern Recommendations
|
||
|
||
### Simple Tables (String-basiert OK)
|
||
|
||
Für einfache Tabellen ohne komplexe Constraints:
|
||
|
||
```php
|
||
$schema->create('simple_logs', function (Blueprint $table) {
|
||
$table->id();
|
||
$table->string('message');
|
||
$table->timestamps();
|
||
});
|
||
```
|
||
|
||
### Complex Tables (VOs empfohlen)
|
||
|
||
Für Tabellen mit Relationships, Constraints, Composite Indexes:
|
||
|
||
```php
|
||
use App\Framework\Database\ValueObjects\{TableName, ColumnName, IndexName, ConstraintName};
|
||
|
||
$schema->create(TableName::fromString('complex_orders'), function (Blueprint $table) {
|
||
// Type Safety für alle Identifier
|
||
$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 indexes mit explicit naming
|
||
$table->index(
|
||
ColumnName::fromString('user_id'),
|
||
ColumnName::fromString('status'),
|
||
ColumnName::fromString('created_at'),
|
||
IndexName::fromString('idx_orders_user_status_created')
|
||
);
|
||
});
|
||
```
|
||
|
||
## SQL Injection Prevention
|
||
|
||
Database VOs bieten eingebauten Schutz:
|
||
|
||
```php
|
||
// ✅ VOs validieren Input - SQL Injection unmöglich
|
||
$tableName = TableName::fromString($_GET['table']); // Wirft Exception bei Injection-Versuch
|
||
|
||
// ❌ Raw strings sind gefährlich
|
||
$query = "SELECT * FROM {$_GET['table']}"; // SQL Injection möglich!
|
||
|
||
// ✅ VOs in Queries nutzen
|
||
$query = "SELECT * FROM {$tableName}"; // Validated and safe
|
||
```
|
||
|
||
## 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 over inheritance
|
||
- ✅ **Value Objects**: Keine Primitive Obsession
|
||
- ✅ **Type Safety**: Union Types für Backwards Compatibility
|
||
- ✅ **Framework Integration**: `__toString()` für seamless integration
|