# Database Seeding Das Seed-System ermöglicht die Initialisierung der Datenbank mit Standard-Daten, getrennt von Schema-Migrationen. Es bietet eine saubere Trennung zwischen Schema-Änderungen (Migrations) und Initial-Daten (Seeds). ## Übersicht **Migrations** sind für Schema-Änderungen (CREATE TABLE, ALTER TABLE, etc.) **Seeds** sind für Initial-Daten (Standard-Content-Types, Default-Rollen, etc.) Seeds sind idempotent - sie können mehrfach ausgeführt werden ohne Duplikate zu erstellen. Das System trackt ausgeführte Seeds in einer `seeds` Tabelle. ## Architektur Das Seed-System besteht aus folgenden Komponenten: ### Core-Komponenten - **`Seeder` Interface** - Basis-Interface für alle Seeder-Klassen - **`SeedLoader`** - Lädt Seeder-Klassen über das Discovery-System - **`SeedRunner`** - Führt Seeder aus und verwaltet Idempotenz - **`SeedRepository`** - Verwaltet die `seeds` Tabelle für Tracking - **`SeedCommand`** - Console Command für manuelle Ausführung ### Discovery-Integration Das Seed-System nutzt das Framework Discovery-System zur automatischen Erkennung von Seeder-Klassen. Alle Klassen, die das `Seeder` Interface implementieren, werden automatisch gefunden und können ausgeführt werden. ### Seeds Tabelle Die `seeds` Tabelle wird durch die Migration `CreateSeedsTable` erstellt und trackt alle ausgeführten Seeders: - `id` (VARCHAR, PRIMARY KEY) - Eindeutige ID (SHA256 Hash des Seeder-Namens) - `name` (VARCHAR, UNIQUE) - Seeder-Name - `description` (TEXT) - Beschreibung des Seeders - `executed_at` (TIMESTAMP) - Zeitpunkt der Ausführung Diese Tabelle verhindert, dass Seeders mehrfach ausgeführt werden. ## Installation ### 1. Migration ausführen Die `seeds` Tabelle wird automatisch erstellt, wenn die Migration `CreateSeedsTable` ausgeführt wird: ```bash php console.php db:migrate ``` Die Migration ist Teil des Framework-Seed-Systems und wird automatisch erkannt. ### 2. Discovery-Cache aktualisieren Nach dem Erstellen neuer Seeder-Klassen sollte der Discovery-Cache geleert werden: ```bash php console.php cache:clear ``` ## Verwendung ### Alle Seeders ausführen Führt alle gefundenen Seeder aus, die noch nicht ausgeführt wurden: ```bash php console.php db:seed ``` **Ausgabe:** ``` Running all seeders... Found 1 seeder(s): - DefaultContentTypesSeeder: Seeds default CMS content types (page, post, landing_page) [info] Running seeder 'DefaultContentTypesSeeder'... [info] Seeder 'DefaultContentTypesSeeder' completed successfully ✅ All seeders completed. ``` ### Spezifischen Seeder ausführen Führt nur einen bestimmten Seeder aus: ```bash php console.php db:seed --class=DefaultContentTypesSeeder ``` **Ausgabe:** ``` Running seeder: DefaultContentTypesSeeder [info] Running seeder 'DefaultContentTypesSeeder'... [info] Seeder 'DefaultContentTypesSeeder' completed successfully ✅ Seeder 'DefaultContentTypesSeeder' completed. ``` ### Alle Seeds neu ausführen (--fresh) Löscht die `seeds` Tabelle und führt alle Seeders neu aus: ```bash php console.php db:seed --fresh ``` **Warnung:** Die `--fresh` Option sollte nur in Development-Umgebungen verwendet werden. **Ausgabe:** ``` ⚠️ Clearing seeds table (--fresh option)... ✅ Seeds table cleared. Running all seeders... ... ``` ## Seeder erstellen ### 1. Seeder-Klasse erstellen Erstelle eine Klasse, die das `Seeder` Interface implementiert: ```php service->findBySlug('example'); // Skip if already exists } catch (\RuntimeException $e) { // Create if not exists $this->service->create( name: 'Example', slug: 'example', description: 'Example description' ); } } ``` **Beispiel 2: Mit expliziter Existenz-Prüfung** ```php public function seed(): void { if (!$this->repository->exists(ExampleId::fromString('example'))) { $this->service->create(...); } } ``` **Beispiel 3: Mit Config-Datei** ```php public function seed(): void { $configPath = $this->pathProvider->getBasePath() ->join('config', 'your-domain', 'default-data.php'); $defaultData = require $configPath->toString(); foreach ($defaultData as $slug => $data) { try { $this->service->findBySlug($slug); // Skip if already exists } catch (\RuntimeException $e) { $this->service->create( name: $data['name'], slug: $slug, description: $data['description'] ?? null ); } } } ``` ### 3. Verzeichnis-Struktur Seeder sollten im `Seeds` Verzeichnis der jeweiligen Domain liegen: ``` src/Domain/ ├── Cms/ │ └── Seeds/ │ └── DefaultContentTypesSeeder.php └── User/ └── Seeds/ └── DefaultRolesSeeder.php ``` Das Discovery-System findet automatisch alle Seeder-Klassen, die das `Seeder` Interface implementieren. ### 4. Dependency Injection Seeder können alle Framework-Services über Dependency Injection verwenden: ```php final readonly class YourSeeder implements Seeder { public function __construct( private YourService $yourService, private PathProvider $pathProvider, private Clock $clock, private Logger $logger ) {} // ... } ``` Der DI Container stellt automatisch alle benötigten Dependencies bereit. ## Standard-Content-Types Das CMS-System stellt Standard-Content-Types über den `DefaultContentTypesSeeder` bereit: - **`page`** - Standard-Seiten (z.B. "Über uns", "Impressum", "Kontakt") - **`post`** - Blog-Artikel/News - **`landing_page`** - Marketing-Landing-Pages Diese werden automatisch erstellt, wenn der Seeder ausgeführt wird. Die Definitionen können in `config/cms/default-content-types.php` angepasst werden. ### Config-Datei anpassen Die Standard-Content-Types werden in `config/cms/default-content-types.php` definiert: ```php [ 'name' => 'Page', 'description' => 'Standard pages for general content', 'isSystem' => true, ], 'post' => [ 'name' => 'Post', 'description' => 'Blog posts and news articles', 'isSystem' => true, ], 'landing_page' => [ 'name' => 'Landing Page', 'description' => 'Marketing landing pages for campaigns', 'isSystem' => true, ], ]; ``` Du kannst diese Datei anpassen, um weitere Content-Types hinzuzufügen oder bestehende zu ändern. Nach Änderungen führe `db:seed --fresh` aus, um die Seeds neu auszuführen. ## Seeds Tabelle Das Seed-System verwendet eine `seeds` Tabelle, um zu tracken, welche Seeders bereits ausgeführt wurden: ### Schema ```sql CREATE TABLE seeds ( id VARCHAR(255) PRIMARY KEY, name VARCHAR(255) UNIQUE NOT NULL, description TEXT, executed_at TIMESTAMP NOT NULL ); CREATE INDEX idx_seeds_name ON seeds(name); ``` ### Spalten - **`id`** (VARCHAR, PRIMARY KEY) - Eindeutige ID (SHA256 Hash des Seeder-Namens) - **`name`** (VARCHAR, UNIQUE) - Seeder-Name (muss eindeutig sein) - **`description`** (TEXT) - Beschreibung des Seeders - **`executed_at`** (TIMESTAMP) - Zeitpunkt der Ausführung ### Verwendung Die Tabelle wird automatisch erstellt, wenn die Migration `CreateSeedsTable` ausgeführt wird. Der `SeedRepository` verwaltet diese Tabelle automatisch: - `hasRun(string $name): bool` - Prüft, ob ein Seeder bereits ausgeführt wurde - `markAsRun(string $name, string $description): void` - Markiert einen Seeder als ausgeführt - `clearAll(): void` - Löscht alle Einträge (für `--fresh` Option) ## Best Practices ### 1. Idempotenz Seeds sollten immer idempotent sein - prüfe auf Existenz vor dem Erstellen: ```php public function seed(): void { // ✅ GUT: Prüft Existenz vor Erstellung try { $this->service->findBySlug('example'); return; // Skip if exists } catch (\RuntimeException $e) { $this->service->create(...); } // ❌ SCHLECHT: Erstellt ohne Prüfung $this->service->create(...); // Kann Duplikate erzeugen } ``` ### 2. Config-Dateien Verwende Config-Dateien für anpassbare Seed-Daten: ```php // config/your-domain/default-data.php return [ 'example' => [ 'name' => 'Example', 'description' => 'Example description', 'isSystem' => true, ], ]; ``` Vorteile: - Einfache Anpassung ohne Code-Änderung - Versionierbar in Git - Kann in verschiedenen Umgebungen unterschiedlich sein ### 3. System-Daten Markiere System-Daten mit `isSystem: true` um sie vor Löschung zu schützen: ```php $this->service->create( name: 'System Role', slug: 'admin', isSystem: true // Kann nicht gelöscht werden ); ``` ### 4. Fehlerbehandlung Seeds sollten aussagekräftige Fehlermeldungen werfen: ```php public function seed(): void { try { $configPath = $this->pathProvider->getBasePath() ->join('config', 'domain', 'data.php'); if (!file_exists($configPath->toString())) { throw new \RuntimeException( "Config file not found: {$configPath->toString()}" ); } // ... seeding logic } catch (\Throwable $e) { throw new \RuntimeException( "Failed to seed data: {$e->getMessage()}", 0, $e ); } } ``` ### 5. Dokumentation Dokumentiere, was jeder Seeder macht: ```php /** * Seeds default user roles for the application * * Creates the following roles: * - admin: Full system access * - editor: Content editing access * - viewer: Read-only access */ final readonly class DefaultRolesSeeder implements Seeder { // ... } ``` ### 6. Abhängigkeiten zwischen Seeders Wenn Seeders voneinander abhängen, stelle sicher, dass sie in der richtigen Reihenfolge ausgeführt werden: ```php // Seeder 1: Muss zuerst ausgeführt werden final readonly class DefaultRolesSeeder implements Seeder { public function getName(): string { return 'DefaultRolesSeeder'; // Wird alphabetisch zuerst ausgeführt } } // Seeder 2: Kann danach ausgeführt werden final readonly class DefaultUsersSeeder implements Seeder { public function getName(): string { return 'DefaultUsersSeeder'; // Wird nach DefaultRolesSeeder ausgeführt } } ``` **Hinweis:** Seeders werden alphabetisch nach Namen sortiert. Verwende Präfixe für die Reihenfolge, falls nötig (z.B. `01_DefaultRolesSeeder`, `02_DefaultUsersSeeder`). ## Migration vs. Seed | Migration | Seed | |-----------|------| | Schema-Änderungen | Initial-Daten | | Forward-only (optional rollback) | Idempotent (kein rollback) | | Versioniert | Getrackt in `seeds` Tabelle | | Automatisch beim `db:migrate` | Manuell mit `db:seed` | ## Beispiele ### Beispiel: Default Roles Seeder ```php 'Admin', 'slug' => 'admin', 'permissions' => ['*']], ['name' => 'Editor', 'slug' => 'editor', 'permissions' => ['content.edit']], ['name' => 'Viewer', 'slug' => 'viewer', 'permissions' => ['content.view']], ]; foreach ($roles as $roleData) { try { $this->roleService->findBySlug($roleData['slug']); // Skip if already exists } catch (\RuntimeException $e) { $this->roleService->create( name: $roleData['name'], slug: $roleData['slug'], permissions: $roleData['permissions'] ); } } } public function getName(): string { return 'DefaultRolesSeeder'; } public function getDescription(): string { return 'Seeds default user roles (admin, editor, viewer)'; } } ``` ### Beispiel: Seeder mit Config-Datei ```php pathProvider->getBasePath() ->join('config', 'settings', 'defaults.php'); if (!file_exists($configPath->toString())) { throw new \RuntimeException( "Config file not found: {$configPath->toString()}" ); } $defaultSettings = require $configPath->toString(); foreach ($defaultSettings as $key => $value) { if (!$this->settingService->exists($key)) { $this->settingService->create($key, $value); } } } public function getName(): string { return 'DefaultSettingsSeeder'; } public function getDescription(): string { return 'Seeds default application settings'; } } ``` ## Troubleshooting ### Seeder wird nicht gefunden **Problem:** Der Seeder wird nicht vom Discovery-System gefunden. **Lösung:** 1. Stelle sicher, dass die Klasse das `Seeder` Interface implementiert 2. Stelle sicher, dass die Klasse im `Seeds` Verzeichnis liegt 3. Leere den Discovery-Cache: `php console.php cache:clear` 4. Prüfe, ob das `Seeder` Interface in den `targetInterfaces` der Discovery-Konfiguration ist **Debug:** ```bash # Prüfe, ob Seeder gefunden werden php console.php db:seed # Sollte "Found X seeder(s):" anzeigen ``` ### Seeder wird mehrfach ausgeführt **Problem:** Der Seeder wird trotz Tracking mehrfach ausgeführt. **Lösung:** 1. Prüfe, ob die `seeds` Tabelle existiert: `php console.php db:status | grep seeds` 2. Prüfe, ob der Seeder-Name korrekt ist (muss eindeutig sein) 3. Prüfe die `seeds` Tabelle direkt: ```sql SELECT * FROM seeds WHERE name = 'YourSeeder'; ``` ### SQL Syntax Error (ON DUPLICATE KEY UPDATE) **Problem:** `SQLSTATE[42601]: Syntax error: syntax error at or near "DUPLICATE"` **Ursache:** Die Datenbank ist PostgreSQL, aber der Code verwendet MySQL-Syntax. **Lösung:** Verwende PostgreSQL-Syntax (`ON CONFLICT`) statt MySQL-Syntax (`ON DUPLICATE KEY UPDATE`): ```php // ❌ MySQL-Syntax (funktioniert nicht mit PostgreSQL) INSERT INTO table (...) VALUES (...) ON DUPLICATE KEY UPDATE ... // ✅ PostgreSQL-Syntax INSERT INTO table (...) VALUES (...) ON CONFLICT (id) DO UPDATE SET ... ``` ### Seeder schlägt fehl **Problem:** Der Seeder wirft eine Exception. **Lösung:** 1. Prüfe die Fehlermeldung in der Console-Ausgabe 2. Stelle sicher, dass alle Dependencies verfügbar sind 3. Prüfe, ob Config-Dateien existieren und korrekt formatiert sind 4. Verwende `--fresh` um alle Seeds neu auszuführen (nur Development) ### Migration für seeds Tabelle fehlt **Problem:** `SQLSTATE[42P01]: Undefined table: relation "seeds" does not exist` **Lösung:** 1. Führe die Migration aus: `php console.php db:migrate` 2. Prüfe, ob die Migration `CreateSeedsTable` existiert 3. Prüfe den Migrations-Status: `php console.php db:status`