16 KiB
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
SeederInterface - Basis-Interface für alle Seeder-KlassenSeedLoader- Lädt Seeder-Klassen über das Discovery-SystemSeedRunner- Führt Seeder aus und verwaltet IdempotenzSeedRepository- Verwaltet dieseedsTabelle für TrackingSeedCommand- 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-Namedescription(TEXT) - Beschreibung des Seedersexecuted_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:
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:
php console.php cache:clear
Verwendung
Alle Seeders ausführen
Führt alle gefundenen Seeder aus, die noch nicht ausgeführt wurden:
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:
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:
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
declare(strict_types=1);
namespace App\Domain\YourDomain\Seeds;
use App\Framework\Database\Seed\Seeder;
use App\Domain\YourDomain\Services\YourService;
final readonly class YourSeeder implements Seeder
{
public function __construct(
private YourService $yourService
) {}
public function seed(): void
{
// Your seeding logic here
// Should be idempotent - check if data exists before creating
}
public function getName(): string
{
return 'YourSeeder';
}
public function getDescription(): string
{
return 'Description of what this seeder does';
}
}
2. Idempotenz sicherstellen
Seeds sollten idempotent sein - sie sollten prüfen, ob Daten bereits existieren, bevor sie erstellt werden:
Beispiel 1: Mit Exception-Handling
public function seed(): void
{
try {
$this->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
public function seed(): void
{
if (!$this->repository->exists(ExampleId::fromString('example'))) {
$this->service->create(...);
}
}
Beispiel 3: Mit Config-Datei
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:
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/Newslanding_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
declare(strict_types=1);
return [
'page' => [
'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
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 Seedersexecuted_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 wurdemarkAsRun(string $name, string $description): void- Markiert einen Seeder als ausgeführtclearAll(): void- Löscht alle Einträge (für--freshOption)
Best Practices
1. Idempotenz
Seeds sollten immer idempotent sein - prüfe auf Existenz vor dem Erstellen:
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:
// 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:
$this->service->create(
name: 'System Role',
slug: 'admin',
isSystem: true // Kann nicht gelöscht werden
);
4. Fehlerbehandlung
Seeds sollten aussagekräftige Fehlermeldungen werfen:
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:
/**
* 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:
// 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
declare(strict_types=1);
namespace App\Domain\User\Seeds;
use App\Domain\User\Services\RoleService;
use App\Framework\Database\Seed\Seeder;
final readonly class DefaultRolesSeeder implements Seeder
{
public function __construct(
private RoleService $roleService
) {}
public function seed(): void
{
$roles = [
['name' => '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
declare(strict_types=1);
namespace App\Domain\Settings\Seeds;
use App\Domain\Settings\Services\SettingService;
use App\Framework\Core\PathProvider;
use App\Framework\Database\Seed\Seeder;
final readonly class DefaultSettingsSeeder implements Seeder
{
public function __construct(
private SettingService $settingService,
private PathProvider $pathProvider
) {}
public function seed(): void
{
$configPath = $this->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:
- Stelle sicher, dass die Klasse das
SeederInterface implementiert - Stelle sicher, dass die Klasse im
SeedsVerzeichnis liegt - Leere den Discovery-Cache:
php console.php cache:clear - Prüfe, ob das
SeederInterface in dentargetInterfacesder Discovery-Konfiguration ist
Debug:
# 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:
- Prüfe, ob die
seedsTabelle existiert:php console.php db:status | grep seeds - Prüfe, ob der Seeder-Name korrekt ist (muss eindeutig sein)
- Prüfe die
seedsTabelle direkt: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):
// ❌ 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:
- Prüfe die Fehlermeldung in der Console-Ausgabe
- Stelle sicher, dass alle Dependencies verfügbar sind
- Prüfe, ob Config-Dateien existieren und korrekt formatiert sind
- Verwende
--freshum 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:
- Führe die Migration aus:
php console.php db:migrate - Prüfe, ob die Migration
CreateSeedsTableexistiert - Prüfe den Migrations-Status:
php console.php db:status