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

View File

@@ -24,6 +24,10 @@ Für allgemeine Framework-Dokumentation, die sowohl für Entwickler als auch AI-
### MCP Integration
- [MCP Integration](mcp-integration.md) - Model Context Protocol Server und Tools
### Dependency Injection & Initializers
- [Session Binding Initializer](session-binding-initializer.md) - SessionInterface lazy binding für Initializer
- [Initializer Context Filtering](initializer-context-filtering.md) - Context-Filter während Bootstrap
### Framework Personas
- [Framework Personas](framework-personas.md) - AI Personas für Framework-Entwicklung

522
docs/claude/crud-system.md Normal file
View File

@@ -0,0 +1,522 @@
# Einheitliches CRUD-System
## Übersicht
Das einheitliche CRUD-System bietet eine zentrale Abstraktion für alle Admin-CRUD-Operationen im Framework. Es basiert auf Komposition statt Vererbung und folgt den Framework-Prinzipien.
## Architektur
### Komponenten
- **CrudService**: Zentrale Service-Klasse für CRUD-Operationen
- **CrudConfig**: Value Object für CRUD-Konfiguration
- **AdminPageRenderer**: Rendering von Admin-Seiten (wird von CrudService verwendet)
- **AdminTableFactory**: Erstellung von Tabellen (wird von CrudService verwendet)
- **AdminFormFactory**: Erstellung von Formularen (wird von CrudService verwendet)
### Design-Prinzipien
- **Komposition statt Vererbung**: Keine Base-Controller-Klassen
- **Value Objects**: CrudConfig als immutable Value Object
- **Dependency Injection**: Alle Abhängigkeiten werden injiziert
- **Separation of Concerns**: Controller enthalten nur Business-Logik, Rendering wird delegiert
## Verwendung
### Grundlegende CRUD-Implementierung
```php
final readonly class MyResourceController
{
private CrudConfig $config;
public function __construct(
private CrudService $crudService,
private MyResourceRepository $repository
) {
$this->config = CrudConfig::forResource(
resource: 'my-resource',
resourceName: 'My Resource',
title: 'My Resources'
)->withColumns([
['field' => 'name', 'label' => 'Name', 'sortable' => true, 'searchable' => true],
['field' => 'created_at', 'label' => 'Created', 'sortable' => true, 'formatter' => 'date'],
]);
}
#[Route('/admin/my-resource', Method::GET)]
public function index(Request $request): ViewResult
{
$items = $this->repository->findAll();
$itemData = array_map(fn($item) => $item->toArray(), $items);
return $this->crudService->renderIndex(
config: $this->config,
items: $itemData,
request: $request
);
}
#[Route('/admin/my-resource/create', Method::GET)]
public function create(): ViewResult
{
return $this->crudService->renderCreate(
config: $this->config,
formFields: [
'name' => [
'type' => 'text',
'label' => 'Name',
'required' => true,
],
]
);
}
#[Route('/admin/my-resource', Method::POST)]
public function store(Request $request): Redirect
{
try {
$item = $this->repository->create($request->body->toArray());
return $this->crudService->redirectAfterCreate(
config: $this->config,
request: $request,
itemId: $item->id->toString()
);
} catch (\Throwable $e) {
return $this->crudService->redirectWithError(
'Failed to create: ' . $e->getMessage(),
$request->body->toArray()
);
}
}
#[Route('/admin/my-resource/{id}/edit', Method::GET)]
public function edit(string $id): ViewResult
{
$item = $this->repository->findById($id);
return $this->crudService->renderEdit(
config: $this->config,
id: $id,
formFields: [
'name' => [
'type' => 'text',
'label' => 'Name',
'required' => true,
],
],
itemData: $item->toArray()
);
}
#[Route('/admin/my-resource/{id}', Method::PUT)]
public function update(string $id, Request $request): Redirect
{
try {
$this->repository->update($id, $request->body->toArray());
return $this->crudService->redirectAfterUpdate(
config: $this->config,
request: $request,
itemId: $id
);
} catch (\Throwable $e) {
return $this->crudService->redirectWithError(
'Failed to update: ' . $e->getMessage(),
$request->body->toArray()
);
}
}
#[Route('/admin/my-resource/{id}', Method::DELETE)]
public function destroy(string $id): Redirect
{
try {
$this->repository->delete($id);
return $this->crudService->redirectAfterDelete($this->config);
} catch (\Throwable $e) {
return $this->crudService->redirectWithError(
'Failed to delete: ' . $e->getMessage()
);
}
}
}
```
## CrudConfig Konfiguration
### Grundkonfiguration
```php
$config = CrudConfig::forResource(
resource: 'users',
resourceName: 'User',
title: 'Users'
);
```
### Spalten definieren
```php
$config = $config->withColumns([
// Format 1: Mit 'field' Key
['field' => 'name', 'label' => 'Name', 'sortable' => true, 'searchable' => true],
// Format 2: Key ist der Feldname
'email' => ['label' => 'Email', 'sortable' => true, 'formatter' => 'email'],
// Mit Formatter
'created_at' => ['label' => 'Created', 'sortable' => true, 'formatter' => 'date'],
// Mit CSS-Klasse
'status' => ['label' => 'Status', 'formatter' => 'status', 'class' => 'text-center'],
]);
```
### Berechtigungen konfigurieren
```php
$config = $config->withPermissions(
canCreate: true,
canEdit: true,
canView: true,
canDelete: false // Delete deaktiviert
);
```
### Bulk-Actions
```php
$config = $config->withBulkActions([
[
'label' => 'Delete',
'action' => 'delete',
'method' => 'DELETE',
'style' => 'danger',
'confirm' => 'Delete Selected Items',
],
[
'label' => 'Publish',
'action' => 'publish',
'method' => 'POST',
'style' => 'primary',
],
]);
```
### Filter-Optionen
```php
$config = $config->withFilterOptions([
'statuses' => ['draft', 'published', 'archived'],
'categories' => $categories,
'current_filters' => $activeFilters,
]);
```
### Zusätzliche Actions
```php
$config = $config->withAdditionalActions([
[
'url' => '/admin/resource/stats',
'label' => 'Statistics',
'icon' => 'chart-bar',
],
]);
```
### Custom Table-Konfiguration
```php
$config = $config->withTableConfig([
'editUrlTemplate' => '/admin/custom-resource/{id}/edit',
'viewUrlTemplate' => '/admin/custom-resource/{id}/view',
'rowActions' => true,
]);
```
## Erweiterte Verwendung
### Custom Filter-Logik
Für komplexe Filter bleibt die Logik im Controller:
```php
public function index(Request $request): ViewResult
{
// Custom filter logic
$filters = MyFilters::fromArray($request->query->toArray());
$items = $this->repository->findWithFilters($filters);
// Data enrichment
$itemData = array_map(function ($item) {
$data = $item->toArray();
$data['custom_field'] = $this->enrichData($item);
return $data;
}, $items);
// Use CrudService with filter options
$configWithFilters = $this->config->withFilterOptions([
'filter_options' => $this->getFilterOptions(),
'current_filters' => $filters->toArray(),
]);
return $this->crudService->renderIndex(
config: $configWithFilters,
items: $itemData,
request: $request
);
}
```
### LiveComponents für spezielle Formulare
Für komplexe Formulare können LiveComponents verwendet werden:
```php
public function create(): ViewResult
{
// Use LiveComponent for complex form
$component = $this->componentRegistry->resolve(...);
return $this->pageRenderer->renderLiveForm(
resource: 'my-resource',
component: $component,
title: 'Create Resource'
);
}
public function store(Request $request): Redirect
{
// After successful creation, use CrudService redirect
return $this->crudService->redirectAfterCreate(
config: $this->config,
request: $request,
itemId: $item->id->toString()
);
}
```
### Custom Show-View
```php
public function show(string $id): ViewResult
{
$item = $this->repository->findById($id);
return $this->crudService->renderShow(
config: $this->config,
id: $id,
fields: [
['label' => 'Name', 'value' => $item->name, 'type' => 'text'],
['label' => 'Status', 'value' => $item->status, 'type' => 'badge', 'color' => 'success'],
],
metadata: [
'created_at' => $item->createdAt->toIso8601(),
'updated_at' => $item->updatedAt->toIso8601(),
],
relatedItems: [
'related_data' => $this->getRelatedData($item),
],
actions: [
['type' => 'link', 'url' => "/admin/resource/{$id}/custom", 'label' => 'Custom Action'],
]
);
}
```
## Migration bestehender Controller
### Schritt 1: Dependencies ersetzen
**Vorher:**
```php
public function __construct(
private AdminPageRenderer $pageRenderer,
private AdminTableFactory $tableFactory,
private AdminFormFactory $formFactory,
private MyRepository $repository
) {}
```
**Nachher:**
```php
public function __construct(
private CrudService $crudService,
private MyRepository $repository
) {
$this->config = CrudConfig::forResource(...);
}
```
### Schritt 2: Index-Methode migrieren
**Vorher:**
```php
public function index(): ViewResult
{
$tableConfig = AdminTableConfig::create(...);
$table = $this->tableFactory->create($tableConfig, $data);
return $this->pageRenderer->renderIndex(...);
}
```
**Nachher:**
```php
public function index(Request $request): ViewResult
{
$items = $this->repository->findAll();
$itemData = array_map(fn($item) => $item->toArray(), $items);
return $this->crudService->renderIndex(
config: $this->config,
items: $itemData,
request: $request
);
}
```
### Schritt 3: Create/Edit migrieren
**Vorher:**
```php
public function create(): ViewResult
{
$formConfig = new AdminFormConfig(...);
$form = $this->formFactory->create($formConfig);
return $this->pageRenderer->renderForm(...);
}
```
**Nachher:**
```php
public function create(): ViewResult
{
return $this->crudService->renderCreate(
config: $this->config,
formFields: [...]
);
}
```
### Schritt 4: Redirects migrieren
**Vorher:**
```php
return new Redirect('/admin/resource', status: 303, flashMessage: 'Success');
```
**Nachher:**
```php
return $this->crudService->redirectAfterCreate(
config: $this->config,
request: $request,
itemId: $item->id->toString()
);
```
## Best Practices
### 1. CrudConfig im Constructor erstellen
Erstelle die CrudConfig einmal im Constructor, nicht in jeder Methode:
```php
public function __construct(...)
{
$this->config = CrudConfig::forResource(...)
->withColumns([...])
->withBulkActions([...]);
}
```
### 2. Data-Transformation
Konvertiere Entities zu Arrays für die Tabelle:
```php
$itemData = array_map(
fn($item) => $item->toArray(),
$items
);
```
### 3. Error-Handling
Verwende immer `redirectWithError()` für Fehler:
```php
try {
// ...
} catch (\Throwable $e) {
return $this->crudService->redirectWithError(
'Failed: ' . $e->getMessage(),
$request->body->toArray() // Preserve input for form
);
}
```
### 4. Custom-Logik bleibt im Controller
Komplexe Filter, Data-Enrichment und spezielle Features bleiben im Controller. CrudService übernimmt nur das Standard-Rendering.
### 5. LiveComponents für komplexe Formulare
Für sehr komplexe Formulare (z.B. mit Drag & Drop, Live-Updates) verwende LiveComponents statt Standard-Formulare.
## Verfügbare Formatter
- `date`: Datumsformatierung
- `status`: Status-Badges
- `boolean`: Boolean-Werte mit Badges
- `currency`: Währungsformatierung
- `number`: Zahlenformatierung
- `masked`: Maskierte Werte
- `preview`: Preview-Format (z.B. für Bilder)
## URL-Templates
Standard-URLs werden automatisch generiert:
- Create: `/admin/{resource}/create`
- Edit: `/admin/{resource}/edit/{id}`
- View: `/admin/{resource}/view/{id}`
- Delete: `/admin/{resource}/delete/{id}`
Custom URLs können über `withTableConfig()` konfiguriert werden.
## Beispiele
Siehe `src/Framework/Admin/Examples/CampaignCrudController.php` für ein vollständiges Beispiel.
## Migration-Guide
1. **Einfache Controller** (wie Asset Collections): Vollständige Migration möglich
2. **Mittlere Controller** (wie Contents): Index und Redirects migrieren, LiveComponents bleiben
3. **Komplexe Controller** (wie Assets): Hybrid-Ansatz - Standard-CRUD über Service, Custom-Features bleiben
## Troubleshooting
### Problem: Spalten werden nicht angezeigt
**Lösung**: Stelle sicher, dass die Spalten-Namen in `withColumns()` mit den Keys in den Daten übereinstimmen.
### Problem: Formular-Felder werden nicht angezeigt
**Lösung**: Verwende das korrekte Format für `formFields`:
```php
'field_name' => [
'type' => 'text',
'label' => 'Label',
'required' => true,
]
```
### Problem: Redirects funktionieren nicht
**Lösung**: Stelle sicher, dass `Request $request` als Parameter übergeben wird.
## Weitere Informationen
- Siehe `src/Framework/Admin/Services/CrudService.php` für die vollständige API
- Siehe `src/Framework/Admin/ValueObjects/CrudConfig.php` für alle Konfigurationsoptionen
- Siehe migrierte Controller für praktische Beispiele

View File

@@ -135,6 +135,36 @@ final class EventStore
- **Use Cases**: Validation beim Setzen, Lazy Loading, Cache Invalidation
- **private(set)** für kontrollierte Array-Mutation in mutable Klassen
**Clone With Syntax (PHP 8.5)**:
-**Verwenden für State-Transformationen** - Reduziert Boilerplate-Code erheblich
-**Syntax**: `clone($object, ['property' => $value])` - Funktioniert perfekt mit `readonly` Klassen
-**Best Practice**: Für einfache und mittlere Transformationen verwenden
- ⚠️ **Komplexe Array-Manipulationen**: Können explizit bleiben, wenn lesbarer
```php
// ✅ Clone With für einfache Transformationen
public function withCount(int $count): self
{
return clone($this, ['count' => $count]);
}
// ✅ Clone With für mehrere Properties
public function increment(): self
{
return clone($this, [
'count' => $this->count + 1,
'lastUpdate' => date('H:i:s')
]);
}
// ⚠️ Komplexe Transformationen können explizit bleiben
public function withTodoRemoved(string $todoId): self
{
$newTodos = array_filter($this->todos, fn($todo) => $todo['id'] !== $todoId);
return clone($this, ['todos' => array_values($newTodos)]);
}
```
## Value Objects over Primitives
**Verwende Value Objects statt Arrays oder Primitives**:

View File

@@ -0,0 +1,158 @@
# Initializer Context Filtering
## Übersicht
Initializer können mit `ContextType`-Filtern versehen werden, um zu steuern, in welchen Execution-Contexts sie ausgeführt werden sollen. Dies ist jedoch mit Vorsicht zu verwenden, da die Discovery während der Bootstrap-Phase läuft.
## Problem: Context-Filter während Bootstrap
**Wichtig**: Die Discovery scannt und verarbeitet Initializer während der **Bootstrap-Phase**, wenn der Execution-Context noch `cli-script` ist (nicht `web`).
### Beispiel-Problem
```php
// ❌ Problem: Wird während Bootstrap übersprungen
#[Initializer(ContextType::WEB)]
public function __invoke(): SomeInterface
{
// ...
}
```
**Warum**: Die Discovery läuft während der Bootstrap-Phase im `cli-script` Context. Initializer mit `ContextType::WEB` Filter werden deshalb übersprungen (`shouldSkipInitializer()` gibt `true` zurück).
### Lösung: Kein Context-Filter für Dependency-Registrierung
Initializer, die **Dependencies registrieren** müssen, sollten **keinen** ContextType-Filter haben:
```php
// ✅ Richtig: Wird während Bootstrap gefunden und ausgeführt
#[Initializer]
public function __invoke(): SomeInterface
{
// ...
}
```
## Wann Context-Filter verwenden?
### ✅ Geeignet für Context-Filter
- **Setup-Initializer** (void-Return), die nur in bestimmten Contexts ausgeführt werden sollen
- **Initializer**, die nur für bestimmte Contexts benötigt werden und keine Dependencies für andere Initializer bereitstellen
### ❌ Nicht geeignet für Context-Filter
- **Initializer**, die Interfaces oder Services registrieren, die von anderen Initializern benötigt werden
- **Initializer**, die während der Bootstrap-Phase verfügbar sein müssen
## Beispiel: SessionBindingInitializer
```php
// ✅ Kein Context-Filter: Muss während Bootstrap verfügbar sein
#[Initializer]
public function __invoke(): void
{
// Registriert SessionInterface als lazy binding
// Wird von ActionAuthorizationCheckerInitializer benötigt
}
```
**Grund**: `SessionBindingInitializer` registriert `SessionInterface`, das von `ActionAuthorizationCheckerInitializer` benötigt wird. Wenn `SessionBindingInitializer` einen `ContextType::WEB` Filter hätte, würde er während der Bootstrap-Phase übersprungen werden, und `ActionAuthorizationCheckerInitializer` würde fehlschlagen.
## Beispiel: ActionAuthorizationCheckerInitializer
```php
// ✅ Kein Context-Filter: Muss während Bootstrap verfügbar sein
#[Initializer]
public function __invoke(): ActionAuthorizationChecker
{
// Benötigt SessionInterface (von SessionBindingInitializer)
// Muss während Bootstrap verfügbar sein, auch wenn nur für WEB verwendet
}
```
**Grund**: Obwohl `ActionAuthorizationChecker` nur für WEB-Requests verwendet wird, muss der Initializer während der Bootstrap-Phase verfügbar sein, damit die Dependency-Injection funktioniert.
## Discovery-Prozess
1. **Bootstrap-Phase**: Discovery scannt alle Initializer
2. **Context-Filter-Prüfung**: `shouldSkipInitializer()` prüft, ob Initializer für aktuellen Context erlaubt ist
3. **Überspringen**: Initializer mit nicht-passendem Context werden übersprungen
4. **Registrierung**: Verbleibende Initializer werden registriert
## Best Practices
### 1. Kein Context-Filter für Dependency-Registrierung
```php
// ✅ Richtig
#[Initializer]
public function __invoke(): SomeInterface
{
// Registriert Interface, das von anderen Initializern benötigt wird
}
```
### 2. Context-Filter nur für Setup-Initializer
```php
// ✅ Geeignet: Setup-Initializer mit Context-Filter
#[Initializer(ContextType::WEB)]
public function __invoke(): void
{
// Setup-Code, der nur für WEB-Contexts benötigt wird
// Wird nicht von anderen Initializern benötigt
}
```
### 3. Dokumentation hinzufügen
```php
/**
* No ContextType filter: This initializer must be available during bootstrap
* (even if context is cli-script) so it can be registered in the container.
* The actual session will be provided by SessionMiddleware when processing web requests.
*/
#[Initializer]
public function __invoke(): SomeInterface
{
// ...
}
```
## Troubleshooting
### Problem: Initializer wird nicht gefunden
**Symptom**: "No initializers found that return this interface"
**Ursache**: Initializer hat ContextType-Filter, der während Bootstrap nicht passt
**Lösung**: Entferne den ContextType-Filter:
```php
// Vorher
#[Initializer(ContextType::WEB)]
// Nachher
#[Initializer]
```
### Problem: Dependency-Injection-Fehler
**Symptom**: "Cannot resolve parameter 'session' for method..."
**Ursache**: Initializer, der Dependency bereitstellt, hat ContextType-Filter
**Lösung**: Entferne den ContextType-Filter vom Dependency-Provider-Initializer
## Siehe auch
- [Session Binding Initializer](session-binding-initializer.md)
- [Dependency Injection](../features/dependency-injection.md)
- [Initializer System](../features/initializer-system.md)

View File

@@ -0,0 +1,368 @@
# Migration Helper Examples
This document shows how to use the new `MigrationHelper` service to simplify migration code and explains the improved migration generation system.
## Overview
The `MigrationHelper` provides a simplified API for common migration operations without requiring abstract classes. It uses composition and follows framework principles.
The improved `MigrationGenerator` now:
- Automatically determines namespace from file path using `PathProvider`
- Uses `PhpNamespace` Value Object for type-safe namespace handling
- Automatically detects if migration should be `SafelyReversible` based on name patterns
- Generates cleaner code using `MigrationHelper` by default
## Basic Usage
### Before (Old Way)
```php
final readonly class CreateUsersTable implements Migration
{
public function up(ConnectionInterface $connection): void
{
$schema = new Schema($connection);
$schema->createIfNotExists('users', function (Blueprint $table) {
$table->ulid('ulid')->primary();
$table->string('name', 255);
$table->timestamps();
});
$schema->execute();
}
public function getVersion(): MigrationVersion
{
return MigrationVersion::fromTimestamp('2024_01_15_000001');
}
public function getDescription(): string
{
return 'Create Users Table';
}
}
```
### After (New Way with Helper)
```php
final readonly class CreateUsersTable implements Migration, SafelyReversible
{
public function up(ConnectionInterface $connection): void
{
$helper = new MigrationHelper($connection);
$helper->createTable('users', function (Blueprint $table) {
$table->ulid('ulid')->primary();
$table->string('name', 255);
$table->timestamps();
});
}
public function down(ConnectionInterface $connection): void
{
$helper = new MigrationHelper($connection);
$helper->dropTable('users');
}
public function getVersion(): MigrationVersion
{
return MigrationVersion::fromTimestamp('2024_01_15_000001');
}
public function getDescription(): string
{
return 'Create Users Table';
}
}
```
## Available Helper Methods
### Creating Tables
```php
$helper = new MigrationHelper($connection);
$helper->createTable('users', function (Blueprint $table) {
$table->ulid('ulid')->primary();
$table->string('name', 255);
$table->string('email', 255)->unique();
$table->timestamps();
});
```
### Dropping Tables
```php
$helper->dropTable('users');
```
### Adding Columns
```php
// Simple column
$helper->addColumn('users', 'phone', 'string', ['length' => 20]);
// Column with options
$helper->addColumn('users', 'age', 'integer', [
'nullable' => false,
'default' => 0
]);
// Decimal column
$helper->addColumn('orders', 'total', 'decimal', [
'precision' => 10,
'scale' => 2,
'nullable' => false
]);
```
### Adding Indexes
```php
// Simple index
$helper->addIndex('users', ['email']);
// Named index
$helper->addIndex('users', ['name', 'email'], 'idx_users_name_email');
// Unique index
$helper->addUniqueIndex('users', ['email'], 'uk_users_email');
```
### Dropping Columns
```php
$helper->dropColumn('users', 'phone');
```
### Renaming Tables
```php
$helper->renameTable('old_table_name', 'new_table_name');
```
## Advanced: Using Schema Directly
For complex operations, you can still use Schema directly:
```php
public function up(ConnectionInterface $connection): void
{
$helper = new MigrationHelper($connection);
$schema = $helper->schema();
// Complex operations
$schema->table('users', function (Blueprint $table) {
$table->dropColumn('old_column');
$table->renameColumn('old_name', 'new_name');
$table->addColumn('new_column', 'string', 255);
});
$schema->execute();
}
```
## Benefits
1. **Less Boilerplate**: No need to manually create Schema and call execute()
2. **Consistent API**: All helper methods follow the same pattern
3. **Type Safe**: Uses Value Objects and proper types
4. **Composable**: Can mix helper methods with direct Schema usage
5. **No Inheritance**: Uses composition, follows framework principles
## Migration Generator
The `MigrationGenerator` has been significantly improved with automatic namespace detection and smarter code generation.
### Automatic Namespace Detection
The generator uses `PathProvider` to automatically determine the correct namespace from the file path, making it work with any project structure:
```php
// When generating: php console.php make:migration CreateUsersTable User
// Path: src/Domain/User/Migrations/CreateUsersTable.php
// Namespace automatically determined: App\Domain\User\Migrations
```
**How it works:**
1. `PathProvider` reads `composer.json` to understand PSR-4 autoloading rules
2. The migration directory path is analyzed
3. `PhpNamespace` Value Object is created from the path
4. Fallback to default structure if path doesn't match PSR-4 rules
**Benefits:**
- Works with any namespace structure configured in `composer.json`
- No hardcoded namespace assumptions
- Type-safe namespace handling via `PhpNamespace` Value Object
- Automatically adapts to project structure changes
### Automatic SafelyReversible Detection
The generator analyzes migration names to determine if they should implement `SafelyReversible`:
**Safe patterns (automatically reversible):**
- `Create*` - Creating tables can be rolled back
- `Add*` - Adding columns/indexes can be removed
- `Index*` - Index operations are reversible
- `Constraint*` - Constraints can be dropped
- `Rename*` - Renaming can be reversed
**Unsafe patterns (forward-only):**
- `Drop*` - Dropping tables/columns loses data
- `Delete*` - Data deletion cannot be reversed
- `Remove*` - Removing columns loses data
- `Truncate*` - Truncating loses all data
- `Alter*Type` - Type changes may lose data
**Example:**
```bash
# Creates SafelyReversible migration
php console.php make:migration CreateUsersTable User
# Creates forward-only migration
php console.php make:migration DropOldColumns User
```
### Generated Code Examples
**Create Table Migration (SafelyReversible):**
```bash
php console.php make:migration CreateUsersTable User
```
Generates:
```php
<?php
declare(strict_types=1);
namespace App\Domain\User\Migrations;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\Migration\Migration;
use App\Framework\Database\Migration\MigrationVersion;
use App\Framework\Database\Migration\Services\MigrationHelper;
use App\Framework\Database\Migration\SafelyReversible;
final readonly class CreateUsersTable implements Migration, SafelyReversible
{
public function up(ConnectionInterface $connection): void
{
$helper = new MigrationHelper($connection);
// TODO: Implement your migration here
// Example:
// $helper->createTable('users', function($table) {
// $table->ulid('ulid')->primary();
// $table->string('name', 255);
// $table->timestamps();
// });
}
public function down(ConnectionInterface $connection): void
{
$helper = new MigrationHelper($connection);
$helper->dropTable('users'); // Auto-generated from name
}
public function getVersion(): MigrationVersion
{
return MigrationVersion::fromTimestamp('2024_01_15_120000');
}
public function getDescription(): string
{
return 'Create Users Table';
}
}
```
**Forward-Only Migration:**
```bash
php console.php make:migration DropOldColumns User
```
Generates:
```php
<?php
declare(strict_types=1);
namespace App\Domain\User\Migrations;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\Migration\Migration;
use App\Framework\Database\Migration\MigrationVersion;
use App\Framework\Database\Migration\Services\MigrationHelper;
final readonly class DropOldColumns implements Migration
{
public function up(ConnectionInterface $connection): void
{
$helper = new MigrationHelper($connection);
// TODO: Implement your migration here
// Example:
// $helper->dropColumn('users', 'old_column');
}
// No down() method - forward-only migration
public function getVersion(): MigrationVersion
{
return MigrationVersion::fromTimestamp('2024_01_15_120001');
}
public function getDescription(): string
{
return 'Drop Old Columns';
}
}
```
### Custom Namespace Structures
The generator works with any namespace structure defined in `composer.json`:
```json
{
"autoload": {
"psr-4": {
"App\\": "src/",
"Custom\\Namespace\\": "custom/path/"
}
}
}
```
If you generate a migration in `custom/path/Migrations/`, the namespace will automatically be `Custom\Namespace\Migrations`.
### MigrationMetadata Value Object
For advanced use cases, you can use `MigrationMetadata` to work with migration metadata:
```php
use App\Framework\Database\Migration\ValueObjects\MigrationMetadata;
use App\Framework\Core\ValueObjects\PhpNamespace;
$namespace = PhpNamespace::fromString('App\Domain\User\Migrations');
$metadata = MigrationMetadata::create(
version: MigrationVersion::fromTimestamp('2024_01_15_120000'),
description: 'Create Users Table',
namespace: $namespace,
domain: 'User',
author: 'John Doe'
);
// Metadata automatically extracts domain from namespace
// $metadata->domain === 'User'
```
## Best Practices
1. **Use MigrationHelper for common operations** - Reduces boilerplate
2. **Let generator detect SafelyReversible** - Don't manually add it unless needed
3. **Trust PathProvider for namespaces** - It handles PSR-4 correctly
4. **Use PhpNamespace Value Objects** - Type-safe namespace operations
5. **Keep migrations simple** - Use helper methods, fall back to Schema for complex cases

View File

@@ -272,6 +272,105 @@ final readonly class Url
---
## ✅ Clone With Syntax (Relevantes Feature)
### Clone With für State-Objekte
PHP 8.5 führt die `clone()` Funktion mit Property-Überschreibung ein, die perfekt für immutable State-Objekte geeignet ist.
**Syntax:**
```php
$newState = clone($state, ['property' => $value]);
```
**Framework Integration:**
Die `clone with` Syntax wurde erfolgreich in alle LiveComponent State-Objekte integriert:
```php
// Vorher (PHP 8.4):
public function withCount(int $count): self
{
return new self(
count: $count,
lastUpdate: $this->lastUpdate,
renderCount: $this->renderCount
);
}
// Nachher (PHP 8.5):
public function withCount(int $count): self
{
return clone($this, ['count' => $count]);
}
```
**Vorteile:**
- ✅ Reduziert Boilerplate-Code um ~30-40%
- ✅ Verbessert Lesbarkeit - klarer Intent
- ✅ Funktioniert perfekt mit `readonly` Klassen
- ✅ Type-Safety bleibt erhalten
- ✅ Automatische Property-Kopie für unveränderte Properties
**Beispiele aus dem Framework:**
```php
// Einfache Transformation
public function withLastUpdate(string $timestamp): self
{
return clone($this, ['lastUpdate' => $timestamp]);
}
// Mehrere Properties ändern
public function increment(): self
{
return clone($this, [
'count' => $this->count + 1,
'lastUpdate' => date('H:i:s')
]);
}
// Mit berechneten Werten
public function withSearchResults(string $query, array $results, float $executionTimeMs): self
{
return clone($this, [
'query' => $query,
'results' => $results,
'resultCount' => $this->countResults($results),
'executionTimeMs' => $executionTimeMs,
'timestamp' => time()
]);
}
```
**Wann explizite `new self()` besser ist:**
Für komplexe Array-Manipulationen kann die explizite Variante lesbarer bleiben:
```php
// Komplexe Array-Manipulation - explizit bleibt lesbarer
public function withTodoRemoved(string $todoId): self
{
$newTodos = array_filter(
$this->todos,
fn ($todo) => $todo['id'] !== $todoId
);
return clone($this, ['todos' => array_values($newTodos)]);
}
```
**Migration Status:**
- ✅ Phase 1: Alle einfachen Transformationen migriert (~80 Methoden)
- ✅ Phase 2: Mittlere Transformationen migriert (~50 Methoden)
- ⏭️ Phase 3: Komplexe Transformationen bleiben explizit (optional)
**Referenz:**
- [PHP 8.5 Release Notes - Clone With](https://www.php.net/releases/8.5/en.php#clone-with)
- [RFC: Clone with](https://wiki.php.net/rfc/clone_with)
---
## ❌ Nicht-Relevante Features
### Property Hooks (❌ Inkompatibel mit Framework)

View File

@@ -0,0 +1,129 @@
# Session Binding Initializer
## Übersicht
Der `SessionBindingInitializer` löst das Problem, dass `SessionInterface` während der Bootstrap-Phase noch nicht verfügbar ist, wenn Initializer ausgeführt werden, die `SessionInterface` als Dependency benötigen.
## Problem
**Timing-Problem**: `SessionInterface` wird normalerweise von `SessionMiddleware` während der Request-Verarbeitung gebunden. Initializer laufen jedoch während der Bootstrap-Phase, bevor ein Request verarbeitet wird. Dies führt zu Dependency-Injection-Fehlern, wenn Initializer `SessionInterface` als Dependency benötigen.
**Beispiel**: `ActionAuthorizationCheckerInitializer` benötigt `SessionInterface`, um `SessionBasedAuthorizationChecker` zu erstellen. Ohne `SessionBindingInitializer` würde die Dependency-Injection fehlschlagen.
## Lösung
Der `SessionBindingInitializer` registriert `SessionInterface` als **lazy binding** im Container:
```php
#[Initializer]
public function __invoke(): void
{
$sessionManager = $this->sessionManager;
$this->container->singleton(SessionInterface::class, function(Container $c) use ($sessionManager) {
// Check if Session is already bound (by Middleware)
if ($c->has(Session::class)) {
return $c->get(Session::class);
}
// Fallback: Create new session without request context
// This will be overridden by SessionMiddleware when request is available
return $sessionManager->createNewSession();
});
}
```
## Funktionsweise
1. **Lazy Binding**: `SessionInterface` wird als Closure registriert, die erst ausgeführt wird, wenn `SessionInterface` tatsächlich benötigt wird.
2. **Fallback-Mechanismus**:
- Wenn `SessionMiddleware` bereits gelaufen ist, wird die bereits gebundene `Session` verwendet.
- Andernfalls wird eine neue Session ohne Request-Context erstellt.
3. **Override durch Middleware**: `SessionMiddleware` überschreibt das lazy binding mit der tatsächlichen Session-Instanz, wenn ein Request verarbeitet wird.
## Verwendung
Initializer, die `SessionInterface` benötigen, können es einfach als Dependency injizieren:
```php
final readonly class ActionAuthorizationCheckerInitializer
{
public function __construct(
private Container $container
) {
}
#[Initializer]
public function __invoke(): ActionAuthorizationChecker
{
// SessionInterface wird automatisch vom lazy binding bereitgestellt
$session = $this->container->get(SessionInterface::class);
$checker = new SessionBasedAuthorizationChecker($session);
$this->container->singleton(ActionAuthorizationChecker::class, $checker);
return $checker;
}
}
```
## Wichtige Hinweise
### Kein ContextType-Filter
**Wichtig**: Der `SessionBindingInitializer` hat **keinen** `ContextType`-Filter:
```php
#[Initializer] // ✅ Kein ContextType::WEB Filter
public function __invoke(): void
```
**Grund**: Die Discovery läuft während der Bootstrap-Phase, wenn der Context noch `cli-script` ist. Ein `ContextType::WEB` Filter würde dazu führen, dass der Initializer übersprungen wird und `SessionInterface` nicht registriert wird.
### Discovery-Timing
Die Discovery scannt Initializer während der Bootstrap-Phase, bevor Requests verarbeitet werden. Initializer mit ContextType-Filtern werden nur ausgeführt, wenn der aktuelle Context passt. Da die Bootstrap-Phase im `cli-script` Context läuft, werden Initializer mit `ContextType::WEB` Filter übersprungen.
**Lösung**: Initializer, die für die Dependency-Registrierung benötigt werden, sollten **keinen** ContextType-Filter haben, auch wenn sie nur für WEB-Contexts verwendet werden.
## Dateien
- **Initializer**: `src/Framework/Http/Session/SessionBindingInitializer.php`
- **Verwendet von**: `src/Framework/LiveComponents/Security/ActionAuthorizationCheckerInitializer.php`
- **Middleware**: `src/Framework/Http/Session/SessionMiddleware.php`
## Troubleshooting
### Problem: "Cannot instantiate interface SessionInterface"
**Ursache**: `SessionBindingInitializer` wurde nicht gefunden oder nicht ausgeführt.
**Lösung**:
1. Prüfe, ob `SessionBindingInitializer` das `#[Initializer]` Attribut hat (ohne ContextType-Filter).
2. Leere den Discovery-Cache: `php console.php discovery:clear-cache`
3. Prüfe die Logs, ob der Initializer gefunden wurde.
### Problem: Initializer wird nicht gefunden
**Ursache**: ContextType-Filter verhindert, dass der Initializer während der Bootstrap-Phase gefunden wird.
**Lösung**: Entferne den `ContextType`-Filter vom `#[Initializer]` Attribut:
```php
// ❌ Falsch: Wird während Bootstrap übersprungen
#[Initializer(ContextType::WEB)]
// ✅ Richtig: Wird während Bootstrap gefunden
#[Initializer]
```
## Siehe auch
- [Dependency Injection](../features/dependency-injection.md)
- [Session Management](../features/session-management.md)
- [Initializer System](../features/initializer-system.md)