- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
613 lines
16 KiB
Markdown
613 lines
16 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
|
||
|
||
TODO: Document EntityManager and UnitOfWork pattern
|
||
|
||
## Repository Pattern
|
||
|
||
TODO: Document repository implementation and usage
|
||
|
||
## Query Optimization
|
||
|
||
TODO: Document N+1 prevention and batch loading
|
||
|
||
## Connection Pooling
|
||
|
||
TODO: Document connection pool configuration
|
||
|
||
## Transaction Management
|
||
|
||
TODO: Document transaction patterns and best practices
|
||
|
||
## 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
|