fix: DockerSecretsResolver - don't normalize absolute paths like /var/www/html/...
Some checks failed
Deploy Application / deploy (push) Has been cancelled

This commit is contained in:
2025-11-24 21:28:25 +01:00
parent 4eb7134853
commit 77abc65cd7
1327 changed files with 91915 additions and 9909 deletions

633
docs/database/seeding.md Normal file
View File

@@ -0,0 +1,633 @@
# 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`