Some checks failed
Deploy Application / deploy (push) Has been cancelled
634 lines
16 KiB
Markdown
634 lines
16 KiB
Markdown
# 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
|
|
<?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**
|
|
|
|
```php
|
|
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**
|
|
|
|
```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
|
|
<?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
|
|
|
|
```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
|
|
<?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
|
|
<?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:**
|
|
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`
|
|
|