fix: DockerSecretsResolver - don't normalize absolute paths like /var/www/html/...
Some checks failed
Deploy Application / deploy (push) Has been cancelled
Some checks failed
Deploy Application / deploy (push) Has been cancelled
This commit is contained in:
@@ -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
522
docs/claude/crud-system.md
Normal 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
|
||||
|
||||
|
||||
|
||||
@@ -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**:
|
||||
|
||||
158
docs/claude/initializer-context-filtering.md
Normal file
158
docs/claude/initializer-context-filtering.md
Normal 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)
|
||||
|
||||
|
||||
|
||||
|
||||
368
docs/claude/migration-helper-examples.md
Normal file
368
docs/claude/migration-helper-examples.md
Normal 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
|
||||
|
||||
@@ -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)
|
||||
|
||||
129
docs/claude/session-binding-initializer.md
Normal file
129
docs/claude/session-binding-initializer.md
Normal 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)
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user