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,7 @@ Die Dokumentation ist in folgende Hauptbereiche gegliedert:
|
||||
#### Database
|
||||
- [Database Patterns](features/database/patterns.md)
|
||||
- [Migrations](features/database/migrations.md)
|
||||
- [Seeding](database/seeding.md) - Seed-System für Initial-Daten
|
||||
- [Neue Features](database/new-features.md)
|
||||
|
||||
#### Queue System
|
||||
@@ -48,6 +49,9 @@ Die Dokumentation ist in folgende Hauptbereiche gegliedert:
|
||||
#### Filesystem
|
||||
- [Filesystem Patterns](features/filesystem/patterns.md)
|
||||
|
||||
#### View Transitions
|
||||
- [View Transitions API](features/view-transitions.md)
|
||||
|
||||
#### Routing
|
||||
- [Routing Value Objects](features/routing/value-objects.md)
|
||||
- [Routing Guide](guides/routing.md)
|
||||
@@ -106,6 +110,7 @@ Die Dokumentation ist in folgende Hauptbereiche gegliedert:
|
||||
- [Image Upload](guides/README-image-upload.md)
|
||||
- [Static Site](guides/README-static-site.md)
|
||||
- [WebSocket](guides/README-websocket.md)
|
||||
- [Migrating to Modern Features](guides/migrating-to-modern-features.md)
|
||||
|
||||
### API-Dokumentation
|
||||
- [API-Übersicht](api/index.md)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
213
docs/components/README.md
Normal file
213
docs/components/README.md
Normal file
@@ -0,0 +1,213 @@
|
||||
# Component System
|
||||
|
||||
Das Component-System ermöglicht wiederverwendbare UI-Komponenten mit konsistenter Syntax und Design-Token-Integration.
|
||||
|
||||
## Syntax
|
||||
|
||||
Alle Components verwenden die `<x-*>` Syntax:
|
||||
|
||||
```html
|
||||
<x-button variant="primary" size="sm">Click me</x-button>
|
||||
```
|
||||
|
||||
## Verfügbare Components
|
||||
|
||||
### Button
|
||||
|
||||
Vielseitige Button-Komponente mit verschiedenen Varianten und Größen.
|
||||
|
||||
**Syntax:**
|
||||
```html
|
||||
<x-button variant="primary|secondary|danger|success|ghost"
|
||||
size="sm|md|lg"
|
||||
href="/url"
|
||||
disabled>
|
||||
Button Text
|
||||
</x-button>
|
||||
```
|
||||
|
||||
**Attribute:**
|
||||
- `variant` (optional): `primary`, `secondary`, `danger`, `success`, `ghost` (Standard: `primary`)
|
||||
- `size` (optional): `sm`, `md`, `lg` (Standard: `md`)
|
||||
- `href` (optional): Wenn gesetzt, wird ein `<a>`-Tag statt `<button>` verwendet
|
||||
- `disabled` (optional): Deaktiviert den Button
|
||||
- `type` (optional): Button-Typ (`button`, `submit`, `reset`) - nur bei `<button>` relevant
|
||||
- `full-width` (optional): Button nimmt volle Breite ein
|
||||
- `icon` (optional): Icon-HTML für Icon-Button
|
||||
|
||||
**Beispiele:**
|
||||
|
||||
```html
|
||||
<!-- Primary Button -->
|
||||
<x-button variant="primary">Save</x-button>
|
||||
|
||||
<!-- Secondary Button als Link -->
|
||||
<x-button variant="secondary" href="/admin">Back</x-button>
|
||||
|
||||
<!-- Small Danger Button -->
|
||||
<x-button variant="danger" size="sm">Delete</x-button>
|
||||
|
||||
<!-- Disabled Button -->
|
||||
<x-button variant="primary" disabled>Processing...</x-button>
|
||||
|
||||
<!-- Full Width Button -->
|
||||
<x-button variant="primary" full-width>Submit Form</x-button>
|
||||
```
|
||||
|
||||
### Card
|
||||
|
||||
Card-Komponente für strukturierte Inhaltsblöcke.
|
||||
|
||||
**Syntax:**
|
||||
```html
|
||||
<x-card variant="default|highlighted|success|warning|error"
|
||||
title="Card Title"
|
||||
subtitle="Subtitle"
|
||||
footer="Footer Content">
|
||||
Card content here
|
||||
</x-card>
|
||||
```
|
||||
|
||||
**Attribute:**
|
||||
- `variant` (optional): `default`, `highlighted`, `success`, `warning`, `error` (Standard: `default`)
|
||||
- `title` (optional): Card-Titel
|
||||
- `subtitle` (optional): Card-Untertitel
|
||||
- `footer` (optional): Footer-Text
|
||||
- `image-src` (optional): Bild-URL für Card-Header
|
||||
- `image-alt` (optional): Alt-Text für Bild
|
||||
|
||||
**Beispiele:**
|
||||
|
||||
```html
|
||||
<!-- Einfache Card -->
|
||||
<x-card>
|
||||
<p>Card content</p>
|
||||
</x-card>
|
||||
|
||||
<!-- Card mit Titel -->
|
||||
<x-card title="Welcome">
|
||||
<p>Welcome to our platform!</p>
|
||||
</x-card>
|
||||
|
||||
<!-- Card mit Variante -->
|
||||
<x-card variant="success" title="Success" subtitle="Operation completed">
|
||||
<p>Your changes have been saved.</p>
|
||||
</x-card>
|
||||
|
||||
<!-- Card mit Bild -->
|
||||
<x-card image-src="/images/hero.jpg" image-alt="Hero Image" title="Featured">
|
||||
<p>Featured content here</p>
|
||||
</x-card>
|
||||
```
|
||||
|
||||
### Badge
|
||||
|
||||
Kleine Status-Indikatoren und Labels.
|
||||
|
||||
**Syntax:**
|
||||
```html
|
||||
<x-badge variant="default|success|warning|error|info"
|
||||
size="sm|md|lg"
|
||||
pill>
|
||||
Badge Text
|
||||
</x-badge>
|
||||
```
|
||||
|
||||
**Attribute:**
|
||||
- `variant` (optional): `default`, `success`, `warning`, `error`, `info` (Standard: `default`)
|
||||
- `size` (optional): `sm`, `md`, `lg` (Standard: `md`)
|
||||
- `pill` (optional): Rundet die Badge ab (pill-Form)
|
||||
|
||||
**Beispiele:**
|
||||
|
||||
```html
|
||||
<!-- Default Badge -->
|
||||
<x-badge>New</x-badge>
|
||||
|
||||
<!-- Success Badge -->
|
||||
<x-badge variant="success">Active</x-badge>
|
||||
|
||||
<!-- Warning Badge -->
|
||||
<x-badge variant="warning">Pending</x-badge>
|
||||
|
||||
<!-- Pill Badge -->
|
||||
<x-badge variant="info" pill>Beta</x-badge>
|
||||
|
||||
<!-- Small Badge -->
|
||||
<x-badge variant="error" size="sm">Error</x-badge>
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Konsistente Verwendung
|
||||
|
||||
1. **Immer Components verwenden**: Verwende `<x-button>` statt `<button class="btn">`
|
||||
2. **Varianten statt Custom CSS**: Nutze `variant` Attribute statt eigene CSS-Klassen
|
||||
3. **Design Tokens**: Components nutzen automatisch Design Tokens für konsistente Styles
|
||||
|
||||
### Performance
|
||||
|
||||
- Components werden server-side gerendert
|
||||
- Keine JavaScript-Dependencies für statische Components
|
||||
- Caching für bessere Performance
|
||||
|
||||
### Accessibility
|
||||
|
||||
- Components generieren semantisch korrektes HTML
|
||||
- ARIA-Attribute werden automatisch gesetzt wo nötig
|
||||
- Keyboard-Navigation wird unterstützt
|
||||
|
||||
## Migration von alten Templates
|
||||
|
||||
### Von direkten CSS-Klassen
|
||||
|
||||
```html
|
||||
<!-- Vorher -->
|
||||
<button class="btn btn--primary btn--sm">Click</button>
|
||||
|
||||
<!-- Nachher -->
|
||||
<x-button variant="primary" size="sm">Click</x-button>
|
||||
```
|
||||
|
||||
### Von admin-* Klassen
|
||||
|
||||
```html
|
||||
<!-- Vorher -->
|
||||
<button class="admin-btn admin-btn--secondary">Click</button>
|
||||
|
||||
<!-- Nachher -->
|
||||
<x-button variant="secondary">Click</x-button>
|
||||
```
|
||||
|
||||
### Von <component> Syntax
|
||||
|
||||
```html
|
||||
<!-- Vorher -->
|
||||
<component name="button" variant="primary">Click</component>
|
||||
|
||||
<!-- Nachher -->
|
||||
<x-button variant="primary">Click</x-button>
|
||||
```
|
||||
|
||||
## Template Linting
|
||||
|
||||
Verwende den `design:lint-templates` Command um Templates auf Component-Verwendung zu prüfen:
|
||||
|
||||
```bash
|
||||
php console.php design:lint-templates
|
||||
```
|
||||
|
||||
Der Linter erkennt:
|
||||
- Direkte CSS-Klassen-Verwendung
|
||||
- Deprecated `admin-*` Klassen
|
||||
- Alte `<component>` Syntax
|
||||
|
||||
## Modern Web Features
|
||||
|
||||
- [Search Component](./search.md) - Semantisches `<search>` Element (Baseline 2023)
|
||||
- [Popover Component](./popover.md) - Native Popover API (Baseline 2025)
|
||||
|
||||
## Weitere Components
|
||||
|
||||
Weitere Components werden kontinuierlich hinzugefügt. Siehe die einzelnen Component-Dateien in `src/Framework/View/Components/` für Details.
|
||||
|
||||
229
docs/components/migration-guide.md
Normal file
229
docs/components/migration-guide.md
Normal file
@@ -0,0 +1,229 @@
|
||||
# Component Migration Guide
|
||||
|
||||
Anleitung zur Migration von Templates auf die neue Component-Syntax.
|
||||
|
||||
## Übersicht
|
||||
|
||||
Dieser Guide hilft dabei, bestehende Templates von direkter CSS-Klassen-Verwendung auf die neue `<x-*>` Component-Syntax zu migrieren.
|
||||
|
||||
## Schritt-für-Schritt Migration
|
||||
|
||||
### Schritt 1: Template analysieren
|
||||
|
||||
Verwende den Linter um Probleme zu finden:
|
||||
|
||||
```bash
|
||||
php console.php design:lint-templates src/Application/Admin/templates/admin-index.view.php
|
||||
```
|
||||
|
||||
### Schritt 2: Button-Migration
|
||||
|
||||
**Pattern 1: Einfacher Button**
|
||||
|
||||
```html
|
||||
<!-- Vorher -->
|
||||
<button class="btn btn--primary">Save</button>
|
||||
|
||||
<!-- Nachher -->
|
||||
<x-button variant="primary">Save</x-button>
|
||||
```
|
||||
|
||||
**Pattern 2: Button als Link**
|
||||
|
||||
```html
|
||||
<!-- Vorher -->
|
||||
<a href="/admin" class="btn btn--secondary">Back</a>
|
||||
|
||||
<!-- Nachher -->
|
||||
<x-button variant="secondary" href="/admin">Back</x-button>
|
||||
```
|
||||
|
||||
**Pattern 3: Button mit Größe**
|
||||
|
||||
```html
|
||||
<!-- Vorher -->
|
||||
<button class="btn btn--primary btn--sm">Small Button</button>
|
||||
|
||||
<!-- Nachher -->
|
||||
<x-button variant="primary" size="sm">Small Button</x-button>
|
||||
```
|
||||
|
||||
**Pattern 4: Disabled Button**
|
||||
|
||||
```html
|
||||
<!-- Vorher -->
|
||||
<button class="btn btn--primary" disabled>Processing</button>
|
||||
|
||||
<!-- Nachher -->
|
||||
<x-button variant="primary" disabled>Processing</x-button>
|
||||
```
|
||||
|
||||
**Pattern 5: Admin-Button**
|
||||
|
||||
```html
|
||||
<!-- Vorher -->
|
||||
<button class="admin-btn admin-btn--secondary">Click</button>
|
||||
|
||||
<!-- Nachher -->
|
||||
<x-button variant="secondary">Click</x-button>
|
||||
```
|
||||
|
||||
### Schritt 3: Card-Migration
|
||||
|
||||
**Pattern 1: Einfache Card**
|
||||
|
||||
```html
|
||||
<!-- Vorher -->
|
||||
<div class="card">
|
||||
<p>Content</p>
|
||||
</div>
|
||||
|
||||
<!-- Nachher -->
|
||||
<x-card>
|
||||
<p>Content</p>
|
||||
</x-card>
|
||||
```
|
||||
|
||||
**Pattern 2: Card mit Header**
|
||||
|
||||
```html
|
||||
<!-- Vorher -->
|
||||
<div class="admin-card">
|
||||
<div class="admin-card__header">
|
||||
<h2 class="admin-card__title">Title</h2>
|
||||
</div>
|
||||
<div class="admin-card__body">
|
||||
<p>Content</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nachher -->
|
||||
<x-card title="Title">
|
||||
<p>Content</p>
|
||||
</x-card>
|
||||
```
|
||||
|
||||
**Pattern 3: Card mit Variante**
|
||||
|
||||
```html
|
||||
<!-- Vorher -->
|
||||
<div class="admin-card admin-card--success">
|
||||
<p>Success message</p>
|
||||
</div>
|
||||
|
||||
<!-- Nachher -->
|
||||
<x-card variant="success">
|
||||
<p>Success message</p>
|
||||
</x-card>
|
||||
```
|
||||
|
||||
### Schritt 4: Badge-Migration
|
||||
|
||||
**Pattern 1: Einfache Badge**
|
||||
|
||||
```html
|
||||
<!-- Vorher -->
|
||||
<span class="badge badge--success">Active</span>
|
||||
|
||||
<!-- Nachher -->
|
||||
<x-badge variant="success">Active</x-badge>
|
||||
```
|
||||
|
||||
**Pattern 2: Admin-Badge**
|
||||
|
||||
```html
|
||||
<!-- Vorher -->
|
||||
<span class="admin-badge admin-badge--error">Error</span>
|
||||
|
||||
<!-- Nachher -->
|
||||
<x-badge variant="error">Error</x-badge>
|
||||
```
|
||||
|
||||
**Pattern 3: Pill Badge**
|
||||
|
||||
```html
|
||||
<!-- Vorher -->
|
||||
<span class="badge badge--info badge--pill">Beta</span>
|
||||
|
||||
<!-- Nachher -->
|
||||
<x-badge variant="info" pill>Beta</x-badge>
|
||||
```
|
||||
|
||||
## Häufige Patterns
|
||||
|
||||
### In Loops
|
||||
|
||||
```html
|
||||
<!-- Vorher -->
|
||||
<for items="{{$actions}}" as="action">
|
||||
<a href="{{$action['url']}}" class="btn btn--primary">
|
||||
{{$action['label']}}
|
||||
</a>
|
||||
</for>
|
||||
|
||||
<!-- Nachher -->
|
||||
<for items="{{$actions}}" as="action">
|
||||
<x-button variant="primary" href="{{$action['url']}}">
|
||||
{{$action['label']}}
|
||||
</x-button>
|
||||
</for>
|
||||
```
|
||||
|
||||
### Mit Bedingungen
|
||||
|
||||
```html
|
||||
<!-- Vorher -->
|
||||
<button class="btn btn--primary" if="{{can_edit}}">Edit</button>
|
||||
|
||||
<!-- Nachher -->
|
||||
<x-button variant="primary" if="{{can_edit}}">Edit</x-button>
|
||||
```
|
||||
|
||||
### Mit zusätzlichen Attributen
|
||||
|
||||
```html
|
||||
<!-- Vorher -->
|
||||
<button class="btn btn--secondary" data-toggle="modal" aria-label="Open">
|
||||
Open Modal
|
||||
</button>
|
||||
|
||||
<!-- Nachher -->
|
||||
<x-button variant="secondary" data-toggle="modal" aria-label="Open">
|
||||
Open Modal
|
||||
</x-button>
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Problem: Component wird nicht gerendert
|
||||
|
||||
**Lösung:** Stelle sicher, dass die Component registriert ist:
|
||||
- Component-Klasse existiert in `src/Framework/View/Components/`
|
||||
- Component hat `#[ComponentName('...')]` Attribut
|
||||
- Component implementiert `StaticComponent` Interface
|
||||
|
||||
### Problem: Attribute werden nicht übernommen
|
||||
|
||||
**Lösung:**
|
||||
- Prüfe ob das Attribut in der Component-Klasse unterstützt wird
|
||||
- Verwende `class` Attribut für zusätzliche CSS-Klassen falls nötig
|
||||
|
||||
### Problem: Alte Syntax funktioniert noch
|
||||
|
||||
**Lösung:**
|
||||
- Die alte `<component name="...">` Syntax wird noch unterstützt, sollte aber migriert werden
|
||||
- Verwende den Linter um alte Syntax zu finden
|
||||
|
||||
## Migration-Checkliste
|
||||
|
||||
- [ ] Template mit Linter analysiert
|
||||
- [ ] Alle Buttons migriert
|
||||
- [ ] Alle Cards migriert
|
||||
- [ ] Alle Badges migriert
|
||||
- [ ] Template getestet
|
||||
- [ ] Keine Linter-Fehler mehr
|
||||
|
||||
## Automatische Migration (Zukünftig)
|
||||
|
||||
Ein `--fix` Flag für automatische Migration ist geplant, aber noch nicht implementiert. Bitte migriere Templates manuell nach diesem Guide.
|
||||
|
||||
174
docs/components/popover.md
Normal file
174
docs/components/popover.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# Popover Component
|
||||
|
||||
Wiederverwendbare Popover-Komponente mit nativer Popover API.
|
||||
|
||||
## Baseline Status
|
||||
|
||||
**Baseline 2025**: Popover API ist Baseline Newly available.
|
||||
|
||||
## Verwendung
|
||||
|
||||
### Basis-Verwendung
|
||||
|
||||
```html
|
||||
<button popovertarget="my-popover">Open Popover</button>
|
||||
|
||||
<x-popover id="my-popover" type="auto">
|
||||
<p>Popover content</p>
|
||||
</x-popover>
|
||||
```
|
||||
|
||||
### Mit Positionierung
|
||||
|
||||
```html
|
||||
<button popovertarget="user-menu-popover">User Menu</button>
|
||||
|
||||
<x-popover id="user-menu-popover" type="auto" position="bottom-end">
|
||||
<ul role="menu">
|
||||
<li><a href="/profile">Profile</a></li>
|
||||
<li><a href="/settings">Settings</a></li>
|
||||
</ul>
|
||||
</x-popover>
|
||||
```
|
||||
|
||||
## Attribute
|
||||
|
||||
| Attribut | Typ | Standard | Beschreibung |
|
||||
|----------|-----|----------|-------------|
|
||||
| `id` | string | `auto-generated` | Popover ID (erforderlich für popovertarget) |
|
||||
| `type` | string | `auto` | Popover-Typ: `auto` oder `manual` |
|
||||
| `popover` | string | `auto` | Alias für `type` |
|
||||
| `position` | string | `null` | Position: `bottom-start`, `bottom-end`, `top-start`, `top-end` |
|
||||
| `anchor` | string | `null` | CSS Anchor Element ID (für Anchor Positioning) |
|
||||
| `aria-label` | string | `null` | ARIA label für accessibility |
|
||||
| `aria-labelledby` | string | `null` | ARIA labelledby für accessibility |
|
||||
|
||||
## Popover-Typen
|
||||
|
||||
### Auto Popover (`type="auto"`)
|
||||
|
||||
- Automatisches Dismissal beim Klick außerhalb
|
||||
- Automatisches Dismissal beim ESC-Key
|
||||
- Automatisches Dismissal beim Klick auf einen anderen Popover-Trigger
|
||||
- Automatisches Focus-Management
|
||||
|
||||
### Manual Popover (`type="manual"`)
|
||||
|
||||
- Manuelles Management erforderlich
|
||||
- Kein automatisches Dismissal
|
||||
- Für komplexe Interaktionen geeignet
|
||||
|
||||
## Positionierung
|
||||
|
||||
Die Komponente unterstützt verschiedene Positionierungen:
|
||||
|
||||
- `bottom-start` - Unten links
|
||||
- `bottom-end` - Unten rechts
|
||||
- `top-start` - Oben links
|
||||
- `top-end` - Oben rechts
|
||||
|
||||
## Trigger-Button
|
||||
|
||||
Der Trigger-Button benötigt das `popovertarget` Attribut:
|
||||
|
||||
```html
|
||||
<button popovertarget="my-popover" aria-haspopup="true" aria-expanded="false">
|
||||
Open Popover
|
||||
</button>
|
||||
```
|
||||
|
||||
Das `aria-expanded` Attribut wird automatisch aktualisiert, wenn der Popover geöffnet/geschlossen wird.
|
||||
|
||||
## Accessibility
|
||||
|
||||
Die Komponente ist vollständig accessibility-konform:
|
||||
|
||||
- Native Browser-Support für Keyboard-Navigation
|
||||
- Automatisches Focus-Management
|
||||
- ESC-Key Support (native)
|
||||
- ARIA-Attribute unterstützt
|
||||
- Screen Reader Support
|
||||
|
||||
## JavaScript Integration
|
||||
|
||||
Die Popover API funktioniert ohne JavaScript, aber für erweiterte Features kann JavaScript verwendet werden:
|
||||
|
||||
```javascript
|
||||
const popover = document.getElementById('my-popover');
|
||||
const trigger = document.querySelector('[popovertarget="my-popover"]');
|
||||
|
||||
// Popover öffnen
|
||||
popover.showPopover();
|
||||
|
||||
// Popover schließen
|
||||
popover.hidePopover();
|
||||
|
||||
// Popover togglen
|
||||
popover.togglePopover();
|
||||
|
||||
// Event Listener
|
||||
popover.addEventListener('toggle', (e) => {
|
||||
const isOpen = popover.matches(':popover-open');
|
||||
trigger.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
||||
});
|
||||
```
|
||||
|
||||
## View Transitions
|
||||
|
||||
Die Komponente unterstützt View Transitions für animierte Übergänge:
|
||||
|
||||
```css
|
||||
[popover].admin-popover {
|
||||
view-transition-name: admin-popover;
|
||||
}
|
||||
```
|
||||
|
||||
## CSS-Klassen
|
||||
|
||||
Die Komponente verwendet folgende CSS-Klassen:
|
||||
|
||||
- `.admin-popover` - Basis-Klasse
|
||||
- `.admin-popover--auto` - Auto Popover Variante
|
||||
- `.admin-popover--manual` - Manual Popover Variante
|
||||
- `.admin-popover--bottom-start` - Position bottom-start
|
||||
- `.admin-popover--bottom-end` - Position bottom-end
|
||||
- `.admin-popover--top-start` - Position top-start
|
||||
- `.admin-popover--top-end` - Position top-end
|
||||
|
||||
## Browser-Support
|
||||
|
||||
- Chrome 125+
|
||||
- Firefox 129+
|
||||
- Safari 18+
|
||||
- Edge 125+
|
||||
|
||||
## Migration
|
||||
|
||||
### Von Custom Dropdown migrieren
|
||||
|
||||
**Vorher:**
|
||||
```html
|
||||
<div class="dropdown" data-dropdown>
|
||||
<button data-dropdown-trigger="menu">Menu</button>
|
||||
<ul class="dropdown-menu" data-dropdown-content="menu">
|
||||
<li><a href="/item1">Item 1</a></li>
|
||||
<li><a href="/item2">Item 2</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Nachher:**
|
||||
```html
|
||||
<button popovertarget="menu-popover">Menu</button>
|
||||
|
||||
<x-popover id="menu-popover" type="auto" position="bottom-start">
|
||||
<ul role="menu">
|
||||
<li><a href="/item1">Item 1</a></li>
|
||||
<li><a href="/item2">Item 2</a></li>
|
||||
</ul>
|
||||
</x-popover>
|
||||
```
|
||||
|
||||
**JavaScript entfernen:**
|
||||
Das Custom Dropdown-Management JavaScript kann entfernt werden, da die Popover API native Funktionalität bietet.
|
||||
|
||||
150
docs/components/search.md
Normal file
150
docs/components/search.md
Normal file
@@ -0,0 +1,150 @@
|
||||
# Search Component
|
||||
|
||||
Wiederverwendbare Search-Komponente mit semantischem `<search>` Element.
|
||||
|
||||
## Baseline Status
|
||||
|
||||
**Baseline 2023**: `<search>` Element ist Baseline Newly available.
|
||||
|
||||
## Verwendung
|
||||
|
||||
### Basis-Verwendung
|
||||
|
||||
```html
|
||||
<x-search
|
||||
action="/search"
|
||||
method="get"
|
||||
placeholder="Search..."
|
||||
/>
|
||||
```
|
||||
|
||||
### Mit Varianten
|
||||
|
||||
```html
|
||||
<!-- Header Variante -->
|
||||
<x-search
|
||||
action="/admin/search"
|
||||
method="get"
|
||||
placeholder="Search..."
|
||||
variant="header"
|
||||
/>
|
||||
|
||||
<!-- Sidebar Variante -->
|
||||
<x-search
|
||||
action="/search"
|
||||
method="get"
|
||||
placeholder="Search..."
|
||||
variant="sidebar"
|
||||
/>
|
||||
|
||||
<!-- Standalone Variante -->
|
||||
<x-search
|
||||
action="/search"
|
||||
method="get"
|
||||
placeholder="Search..."
|
||||
variant="standalone"
|
||||
/>
|
||||
```
|
||||
|
||||
## Attribute
|
||||
|
||||
| Attribut | Typ | Standard | Beschreibung |
|
||||
|----------|-----|----------|-------------|
|
||||
| `action` | string | `/search` | Form action URL |
|
||||
| `method` | string | `get` | HTTP method (get/post) |
|
||||
| `name` | string | `q` | Input name attribute |
|
||||
| `placeholder` | string | `Search...` | Placeholder text |
|
||||
| `value` | string | `null` | Initial input value |
|
||||
| `variant` | string | `standalone` | Variant: `header`, `sidebar`, `standalone` |
|
||||
| `autocomplete` | boolean | `true` | Enable/disable autocomplete |
|
||||
| `aria-label` | string | `null` | ARIA label for accessibility |
|
||||
| `aria-describedby` | string | `null` | ARIA describedby for accessibility |
|
||||
| `id` | string | `auto-generated` | Input ID |
|
||||
|
||||
## Varianten
|
||||
|
||||
### Header Variante
|
||||
Für die Verwendung im Header-Bereich. Automatisch angepasste Breite und Positionierung.
|
||||
|
||||
### Sidebar Variante
|
||||
Für die Verwendung in der Sidebar. Optimiert für schmale Bereiche.
|
||||
|
||||
### Standalone Variante
|
||||
Für eigenständige Search-Formulare. Maximale Breite 600px, zentriert.
|
||||
|
||||
## Accessibility
|
||||
|
||||
Die Komponente ist vollständig accessibility-konform:
|
||||
|
||||
- Semantisches `<search>` Element
|
||||
- Visuell verstecktes Label für Screen Reader
|
||||
- ARIA-Attribute unterstützt
|
||||
- Keyboard-Navigation unterstützt
|
||||
- Focus-Management
|
||||
|
||||
## Beispiele
|
||||
|
||||
### Mit Autocomplete deaktiviert
|
||||
|
||||
```html
|
||||
<x-search
|
||||
action="/admin/search"
|
||||
method="get"
|
||||
placeholder="Search..."
|
||||
autocomplete="false"
|
||||
/>
|
||||
```
|
||||
|
||||
### Mit ARIA-Label
|
||||
|
||||
```html
|
||||
<x-search
|
||||
action="/search"
|
||||
method="get"
|
||||
placeholder="Search..."
|
||||
aria-label="Search the website"
|
||||
/>
|
||||
```
|
||||
|
||||
## CSS-Klassen
|
||||
|
||||
Die Komponente verwendet folgende CSS-Klassen:
|
||||
|
||||
- `.admin-search` - Basis-Klasse
|
||||
- `.admin-search--header` - Header Variante
|
||||
- `.admin-search--sidebar` - Sidebar Variante
|
||||
- `.admin-search--standalone` - Standalone Variante
|
||||
- `.admin-search__input` - Input-Element
|
||||
- `.admin-search__submit` - Submit-Button
|
||||
- `.admin-search__icon` - Search-Icon
|
||||
|
||||
## Browser-Support
|
||||
|
||||
- Chrome 125+
|
||||
- Firefox 129+
|
||||
- Safari 18+
|
||||
- Edge 125+
|
||||
|
||||
## Migration
|
||||
|
||||
### Von `<form role="search">` migrieren
|
||||
|
||||
**Vorher:**
|
||||
```html
|
||||
<form class="admin-search" role="search" action="/search" method="get">
|
||||
<input type="search" name="q" placeholder="Search..." />
|
||||
</form>
|
||||
```
|
||||
|
||||
**Nachher:**
|
||||
```html
|
||||
<x-search
|
||||
action="/search"
|
||||
method="get"
|
||||
name="q"
|
||||
placeholder="Search..."
|
||||
/>
|
||||
```
|
||||
|
||||
Die Komponente generiert automatisch das semantische `<search>` Element mit korrektem HTML-Struktur.
|
||||
|
||||
633
docs/database/seeding.md
Normal file
633
docs/database/seeding.md
Normal 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`
|
||||
|
||||
226
docs/debugging/additional-refactoring-suggestions.md
Normal file
226
docs/debugging/additional-refactoring-suggestions.md
Normal file
@@ -0,0 +1,226 @@
|
||||
# Weitere Refactoring-Vorschläge für Discovery Cache
|
||||
|
||||
## Implementiert: Vorschlag 2 - Cache-Invalidierung bei Datei-Änderungen
|
||||
|
||||
✅ **Implementiert:**
|
||||
- `StalenessChecker::areFilesModifiedSince()` - Prüft einzelne Dateien statt nur Verzeichnisse
|
||||
- `DiscoveryCacheManager::invalidateIfFilesChanged()` - Invalidiert Cache bei Datei-Änderungen
|
||||
- `DiscoveryCacheManager::extractCriticalFilesFromRegistry()` - Extrahiert kritische Dateien aus Registry
|
||||
- Integration in `DiscoveryServiceBootstrapper` für automatische Prüfung kritischer Komponenten-Dateien
|
||||
|
||||
## Weitere Vorschläge
|
||||
|
||||
### Vorschlag 6: Automatische Datei-Erkennung für kritische Komponenten
|
||||
|
||||
**Problem:** Aktuell müssen kritische Dateien manuell in `DiscoveryServiceBootstrapper` aufgelistet werden.
|
||||
|
||||
**Lösung:** Automatische Erkennung aller Komponenten-Dateien aus der Registry:
|
||||
|
||||
```php
|
||||
// In DiscoveryCacheManager.php
|
||||
public function invalidateIfComponentFilesChanged(DiscoveryContext $context): bool
|
||||
{
|
||||
$result = $this->getStandardCache($context);
|
||||
|
||||
if (!$result->found || !$result->entry) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$criticalFiles = $this->extractCriticalFilesFromRegistry($result->entry->registry);
|
||||
|
||||
if (empty($criticalFiles)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->invalidateIfFilesChanged($context, $criticalFiles);
|
||||
}
|
||||
```
|
||||
|
||||
**Vorteil:** Keine manuelle Wartung der Datei-Liste nötig.
|
||||
|
||||
### Vorschlag 7: Watch-Mode für Development
|
||||
|
||||
**Problem:** In Development müssen Entwickler manuell den Cache löschen.
|
||||
|
||||
**Lösung:** Watch-Mode, der automatisch auf Datei-Änderungen reagiert:
|
||||
|
||||
```php
|
||||
// In DiscoveryServiceBootstrapper.php
|
||||
public function enableWatchMode(array $watchPaths = []): void
|
||||
{
|
||||
$this->watchMode = true;
|
||||
$this->watchPaths = $watchPaths;
|
||||
|
||||
// Register file watcher
|
||||
$this->fileWatcher = new FileWatcher($watchPaths, function($changedFiles) {
|
||||
$this->invalidateCacheForFiles($changedFiles);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Vorteil:** Automatische Cache-Invalidierung während Development.
|
||||
|
||||
### Vorschlag 8: Cache-Warming mit File-Hash-Verification
|
||||
|
||||
**Problem:** Cache könnte veraltet sein, auch wenn Datei-Modifikationszeiten gleich sind.
|
||||
|
||||
**Lösung:** File-Hash-Verification für kritische Dateien:
|
||||
|
||||
```php
|
||||
// In StalenessChecker.php
|
||||
public function verifyFileHashes(array $filePaths, array $expectedHashes): bool
|
||||
{
|
||||
foreach ($filePaths as $filePath) {
|
||||
if (!isset($expectedHashes[$filePath])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$currentHash = md5_file($filePath);
|
||||
if ($currentHash !== $expectedHashes[$filePath]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
**Vorteil:** Präzisere Erkennung von Datei-Änderungen, auch bei gleichen Modifikationszeiten.
|
||||
|
||||
### Vorschlag 9: Incremental Cache Updates
|
||||
|
||||
**Problem:** Bei jeder Datei-Änderung wird der gesamte Cache neu erstellt.
|
||||
|
||||
**Lösung:** Incremental Updates - nur geänderte Komponenten werden neu geladen:
|
||||
|
||||
```php
|
||||
// In DiscoveryCacheManager.php
|
||||
public function updateCacheIncrementally(DiscoveryContext $context, array $changedFiles): bool
|
||||
{
|
||||
$result = $this->getStandardCache($context);
|
||||
|
||||
if (!$result->found || !$result->entry) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$registry = $result->entry->registry;
|
||||
|
||||
// Remove entries for changed files
|
||||
foreach ($changedFiles as $file) {
|
||||
$registry = $this->removeEntriesForFile($registry, $file);
|
||||
}
|
||||
|
||||
// Re-discover only changed files
|
||||
$newEntries = $this->discoverFiles($changedFiles);
|
||||
|
||||
// Merge new entries into registry
|
||||
$updatedRegistry = $this->mergeRegistry($registry, $newEntries);
|
||||
|
||||
// Store updated registry
|
||||
return $this->store($context, $updatedRegistry);
|
||||
}
|
||||
```
|
||||
|
||||
**Vorteil:** Deutlich schneller bei kleinen Änderungen.
|
||||
|
||||
### Vorschlag 10: Cache-Tags für selektive Invalidierung
|
||||
|
||||
**Problem:** Bei Änderungen müssen alle Discovery-Caches gelöscht werden.
|
||||
|
||||
**Lösung:** Cache-Tags für selektive Invalidierung:
|
||||
|
||||
```php
|
||||
// In DiscoveryCacheManager.php
|
||||
public function invalidateByTag(string $tag): bool
|
||||
{
|
||||
// Invalidate only caches with specific tag
|
||||
// e.g., 'component:popover', 'route:api', etc.
|
||||
$keys = $this->findCacheKeysByTag($tag);
|
||||
|
||||
foreach ($keys as $key) {
|
||||
$this->cache->forget($key);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
**Vorteil:** Selektive Invalidierung nur betroffener Caches.
|
||||
|
||||
### Vorschlag 11: Cache-Metadaten für Debugging
|
||||
|
||||
**Problem:** Schwer zu debuggen, welche Dateien im Cache enthalten sind.
|
||||
|
||||
**Lösung:** Cache-Metadaten mit Datei-Liste:
|
||||
|
||||
```php
|
||||
// In CacheEntry.php
|
||||
public function __construct(
|
||||
public readonly DiscoveryRegistry|array $registry,
|
||||
public readonly Timestamp $createdAt,
|
||||
public readonly string $version = '',
|
||||
public readonly CacheLevel $cacheLevel = CacheLevel::NORMAL,
|
||||
public readonly CacheTier $cacheTier = CacheTier::HOT,
|
||||
public readonly array $sourceFiles = [], // NEW: List of source files
|
||||
public readonly array $fileHashes = [] // NEW: File hashes for verification
|
||||
) {
|
||||
}
|
||||
```
|
||||
|
||||
**Vorteil:** Besseres Debugging und Verifikation.
|
||||
|
||||
### Vorschlag 12: Background Cache Refresh
|
||||
|
||||
**Problem:** Cache-Refresh blockiert Request.
|
||||
|
||||
**Lösung:** Background Refresh mit stale-while-revalidate Pattern:
|
||||
|
||||
```php
|
||||
// In DiscoveryCacheManager.php
|
||||
public function getWithBackgroundRefresh(DiscoveryContext $context): ?DiscoveryRegistry
|
||||
{
|
||||
$result = $this->getStandardCache($context);
|
||||
|
||||
if ($result->found && $result->isUsable()) {
|
||||
return $result->entry->registry;
|
||||
}
|
||||
|
||||
// Return stale cache if available
|
||||
if ($result->entry !== null) {
|
||||
$this->triggerBackgroundRefresh($context);
|
||||
return $result->entry->registry;
|
||||
}
|
||||
|
||||
// No cache available, do synchronous refresh
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
**Vorteil:** Keine Blockierung von Requests während Cache-Refresh.
|
||||
|
||||
## Empfohlene Implementierungsreihenfolge
|
||||
|
||||
1. ✅ **Vorschlag 2** (Cache-Invalidierung bei Datei-Änderungen) - **IMPLEMENTIERT**
|
||||
2. **Vorschlag 6** (Automatische Datei-Erkennung) - Sofortige Verbesserung
|
||||
3. **Vorschlag 11** (Cache-Metadaten) - Für besseres Debugging
|
||||
4. **Vorschlag 7** (Watch-Mode) - Für Development-Quality-of-Life
|
||||
5. **Vorschlag 9** (Incremental Updates) - Für Performance
|
||||
6. **Vorschlag 10** (Cache-Tags) - Für Skalierbarkeit
|
||||
7. **Vorschlag 12** (Background Refresh) - Für Production-Performance
|
||||
8. **Vorschlag 8** (File-Hash-Verification) - Optional, für höchste Präzision
|
||||
|
||||
## Priorisierung
|
||||
|
||||
**Hoch (sofort):**
|
||||
- Vorschlag 6: Automatische Datei-Erkennung
|
||||
- Vorschlag 11: Cache-Metadaten
|
||||
|
||||
**Mittel (nächste Iteration):**
|
||||
- Vorschlag 7: Watch-Mode
|
||||
- Vorschlag 9: Incremental Updates
|
||||
|
||||
**Niedrig (langfristig):**
|
||||
- Vorschlag 10: Cache-Tags
|
||||
- Vorschlag 12: Background Refresh
|
||||
- Vorschlag 8: File-Hash-Verification
|
||||
|
||||
216
docs/debugging/discovery-cache-refactoring.md
Normal file
216
docs/debugging/discovery-cache-refactoring.md
Normal file
@@ -0,0 +1,216 @@
|
||||
# Discovery Cache Refactoring - Analyse und Vorschläge
|
||||
|
||||
## Problem-Analyse
|
||||
|
||||
### Identifiziertes Problem
|
||||
|
||||
Die Debug-Logs zeigen:
|
||||
```
|
||||
[DEBUG ComponentRegistry] LiveComponent: class=App\Application\LiveComponents\Popover\PopoverComponent, name=popover, arguments={"name":"popover"}
|
||||
```
|
||||
|
||||
**Befund:**
|
||||
1. ✅ Die Datei enthält korrekt `popover-live` (bestätigt durch direkte Reflection)
|
||||
2. ❌ Der Discovery-Cache enthält noch `{"name":"popover"}`
|
||||
3. ❌ Die Discovery wird nicht neu durchgeführt - es erscheint "Cached registry loaded", aber kein "Performing fresh discovery"
|
||||
4. ❌ Der Cache wird sofort nach dem Löschen neu erstellt, bevor die Discovery-Registry die Attribute aus den Dateien liest
|
||||
|
||||
### Root Cause
|
||||
|
||||
Der Discovery-Cache wird geladen, bevor die Discovery-Registry die Attribute aus den Dateien liest. Die Arguments werden aus dem Cache geladen, nicht direkt aus den Dateien.
|
||||
|
||||
**Cache-Load-Reihenfolge:**
|
||||
1. `DiscoveryCacheManager::get()` wird aufgerufen
|
||||
2. Cache wird geladen (enthält noch alte Arguments)
|
||||
3. `ComponentRegistry::buildNameMap()` verwendet die gecachten Arguments
|
||||
4. Kollision tritt auf, weil beide Komponenten als `popover` registriert werden
|
||||
|
||||
## Refactoring-Vorschläge
|
||||
|
||||
### Vorschlag 1: Staleness-Prüfung verbessern
|
||||
|
||||
**Problem:** Die Staleness-Prüfung prüft nicht auf die spezifischen Dateien, die geändert wurden.
|
||||
|
||||
**Lösung:** Erweitere `StalenessChecker` um eine Methode, die prüft, ob bestimmte Dateien geändert wurden:
|
||||
|
||||
```php
|
||||
// In StalenessChecker.php
|
||||
public function areFilesModifiedSince(array $filePaths, Timestamp $since): bool
|
||||
{
|
||||
foreach ($filePaths as $filePath) {
|
||||
if ($this->fileSystemService->exists($filePath)) {
|
||||
$modifiedTime = $this->fileSystemService->getModifiedTime($filePath);
|
||||
if ($modifiedTime->toInt() > $since->toInt()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
**Vorteil:** Präzise Prüfung auf geänderte Dateien statt allgemeine Staleness-Prüfung.
|
||||
|
||||
### Vorschlag 2: Cache-Invalidierung bei Datei-Änderungen
|
||||
|
||||
**Problem:** Der Cache wird nicht automatisch invalidated, wenn Dateien geändert werden.
|
||||
|
||||
**Lösung:** Füge einen File-Watcher hinzu, der Datei-Änderungen erkennt und den Cache automatisch invalidated:
|
||||
|
||||
```php
|
||||
// In DiscoveryCacheManager.php
|
||||
public function invalidateIfFilesChanged(array $filePaths): bool
|
||||
{
|
||||
$key = $this->buildCacheKey($context);
|
||||
$entry = $this->getStandardCache($context);
|
||||
|
||||
if (!$entry->found || !$entry->entry) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$cacheCreatedAt = $entry->entry->createdAt;
|
||||
|
||||
foreach ($filePaths as $filePath) {
|
||||
if ($this->fileSystemService->exists($filePath)) {
|
||||
$modifiedTime = $this->fileSystemService->getModifiedTime($filePath);
|
||||
if ($modifiedTime->toInt() > $cacheCreatedAt->toInt()) {
|
||||
$this->invalidate($context);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
**Vorteil:** Automatische Cache-Invalidierung bei Datei-Änderungen.
|
||||
|
||||
### Vorschlag 3: Arguments direkt aus Dateien lesen (Bypass Cache)
|
||||
|
||||
**Problem:** Die Arguments werden aus dem Cache geladen, nicht direkt aus den Dateien.
|
||||
|
||||
**Lösung:** Füge eine Option hinzu, die Arguments direkt aus den Dateien liest, wenn der Cache als stale erkannt wird:
|
||||
|
||||
```php
|
||||
// In ComponentRegistry.php, buildNameMap()
|
||||
private function buildNameMap(): ComponentNameMap
|
||||
{
|
||||
$mappings = [];
|
||||
$liveComponentClassNames = [];
|
||||
|
||||
// Get all LiveComponent attributes from DiscoveryRegistry
|
||||
$liveComponents = $this->discoveryRegistry->attributes()->get(LiveComponent::class);
|
||||
|
||||
foreach ($liveComponents as $discoveredAttribute) {
|
||||
/** @var LiveComponent|null $attribute */
|
||||
$attribute = $discoveredAttribute->createAttributeInstance();
|
||||
|
||||
if ($attribute && ! empty($attribute->name)) {
|
||||
$className = $discoveredAttribute->className->toString();
|
||||
|
||||
// DEBUG: Log für Popover-Komponenten
|
||||
if (str_contains($className, 'Popover')) {
|
||||
// Prüfe ob Datei geändert wurde seit Cache-Erstellung
|
||||
$filePath = $discoveredAttribute->filePath?->toString();
|
||||
if ($filePath && $this->isFileModifiedSinceCache($filePath)) {
|
||||
// Lese Arguments direkt aus Datei
|
||||
$attribute = $this->readAttributeFromFile($className, LiveComponent::class);
|
||||
}
|
||||
|
||||
error_log(sprintf(
|
||||
"[DEBUG ComponentRegistry] LiveComponent: class=%s, name=%s, arguments=%s, source=%s",
|
||||
$className,
|
||||
$attribute->name,
|
||||
json_encode($discoveredAttribute->arguments),
|
||||
$filePath ?? 'unknown'
|
||||
));
|
||||
}
|
||||
|
||||
$mappings[] = new ComponentMapping($attribute->name, $className);
|
||||
$liveComponentClassNames[] = $className;
|
||||
}
|
||||
}
|
||||
|
||||
// ... rest of method
|
||||
}
|
||||
```
|
||||
|
||||
**Vorteil:** Umgeht den Cache, wenn Dateien geändert wurden.
|
||||
|
||||
### Vorschlag 4: Cache-Versionierung
|
||||
|
||||
**Problem:** Der Cache hat keine Versionierung, die bei Änderungen der Cache-Struktur hilft.
|
||||
|
||||
**Lösung:** Füge eine Cache-Version hinzu, die bei Änderungen der Cache-Struktur erhöht wird:
|
||||
|
||||
```php
|
||||
// In CacheEntry.php
|
||||
private const int CACHE_VERSION = 2; // Erhöhe bei Cache-Struktur-Änderungen
|
||||
|
||||
public function __construct(
|
||||
public readonly DiscoveryRegistry|array $registry,
|
||||
public readonly Timestamp $createdAt,
|
||||
public readonly string $version = '',
|
||||
public readonly CacheLevel $cacheLevel = CacheLevel::NORMAL,
|
||||
public readonly CacheTier $cacheTier = CacheTier::HOT,
|
||||
public readonly int $cacheVersion = self::CACHE_VERSION
|
||||
) {
|
||||
}
|
||||
|
||||
// In DiscoveryCacheManager.php
|
||||
private function isCacheVersionValid(CacheEntry $entry): bool
|
||||
{
|
||||
return $entry->cacheVersion === CacheEntry::CACHE_VERSION;
|
||||
}
|
||||
```
|
||||
|
||||
**Vorteil:** Automatische Cache-Invalidierung bei Cache-Struktur-Änderungen.
|
||||
|
||||
### Vorschlag 5: Discovery-Registry Refresh-Mechanismus
|
||||
|
||||
**Problem:** Die Discovery-Registry wird nicht automatisch aktualisiert, wenn Dateien geändert werden.
|
||||
|
||||
**Lösung:** Füge einen Refresh-Mechanismus hinzu, der die Discovery-Registry automatisch aktualisiert:
|
||||
|
||||
```php
|
||||
// In DiscoveryServiceBootstrapper.php
|
||||
public function refreshIfNeeded(): void
|
||||
{
|
||||
$discoveryCacheManager = $this->container->get(DiscoveryCacheManager::class);
|
||||
$context = $this->createDiscoveryContext();
|
||||
|
||||
$cachedRegistry = $discoveryCacheManager->get($context);
|
||||
|
||||
if ($cachedRegistry === null) {
|
||||
// Cache ist leer, führe Discovery durch
|
||||
$this->bootstrap();
|
||||
return;
|
||||
}
|
||||
|
||||
// Prüfe ob Registry stale ist
|
||||
if ($this->isRegistryStale($cachedRegistry, $context)) {
|
||||
// Registry ist stale, führe Discovery durch
|
||||
$this->bootstrap();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Vorteil:** Automatische Aktualisierung der Discovery-Registry bei Änderungen.
|
||||
|
||||
## Empfohlene Implementierungsreihenfolge
|
||||
|
||||
1. **Vorschlag 1** (Staleness-Prüfung verbessern) - Sofortige Verbesserung
|
||||
2. **Vorschlag 2** (Cache-Invalidierung bei Datei-Änderungen) - Langfristige Lösung
|
||||
3. **Vorschlag 3** (Arguments direkt aus Dateien lesen) - Workaround für aktuelles Problem
|
||||
4. **Vorschlag 4** (Cache-Versionierung) - Präventive Maßnahme
|
||||
5. **Vorschlag 5** (Discovery-Registry Refresh-Mechanismus) - Automatisierung
|
||||
|
||||
## Sofortige Lösung (Workaround)
|
||||
|
||||
Für das aktuelle Problem sollte **Vorschlag 3** implementiert werden, da er das Problem direkt behebt, ohne die gesamte Cache-Architektur zu ändern.
|
||||
|
||||
## Langfristige Lösung
|
||||
|
||||
**Vorschlag 2** sollte langfristig implementiert werden, da er das Problem an der Wurzel löst und verhindert, dass ähnliche Probleme in Zukunft auftreten.
|
||||
|
||||
328
docs/features/attribute-execution/guide.md
Normal file
328
docs/features/attribute-execution/guide.md
Normal file
@@ -0,0 +1,328 @@
|
||||
# Attribute Execution System Guide
|
||||
|
||||
Das Attribute Execution System ermöglicht die Ausführung von Attributen zur Laufzeit mit drei verschiedenen Patterns. Alle Patterns sind cachebar, testbar und unterstützen Dependency Injection.
|
||||
|
||||
**Wichtig**: Attribute werden primär auf **Command/Query-Handler-Methoden** verwendet, nicht auf Controllern, da die meisten Operationen über den CommandBus laufen.
|
||||
|
||||
## Übersicht
|
||||
|
||||
Das System besteht aus drei Hauptkomponenten:
|
||||
|
||||
1. **CallbackMetadata**: Speichert Metadaten über Callbacks (cachebar)
|
||||
2. **CallbackExecutor**: Führt Callbacks basierend auf Metadata aus
|
||||
3. **AttributeRunner**: Führt ausführbare Attribute aus
|
||||
|
||||
## Die drei Patterns
|
||||
|
||||
### Pattern A: Handler-Klassen
|
||||
|
||||
Handler-Klassen sind dedizierte Klassen die eine `check()`, `handle()` oder `__invoke()` Methode implementieren.
|
||||
|
||||
**Vorteile:**
|
||||
- 100% cachebar (nur Strings/Arrays)
|
||||
- Isoliert testbar
|
||||
- Framework-kompatibel (nutzt Container)
|
||||
|
||||
**Beispiel:**
|
||||
|
||||
```php
|
||||
// Handler-Klasse
|
||||
final readonly class PermissionGuard
|
||||
{
|
||||
public function __construct(
|
||||
private readonly array $permissions
|
||||
) {}
|
||||
|
||||
public function check(AttributeExecutionContext $context): bool
|
||||
{
|
||||
$user = $context->container->get(UserService::class)->getCurrentUser();
|
||||
return $user->hasPermissions(...$this->permissions);
|
||||
}
|
||||
}
|
||||
|
||||
// Verwendung
|
||||
#[Guard(PermissionGuard::class, ['edit_post', 'delete_post'])]
|
||||
class PostController {}
|
||||
```
|
||||
|
||||
### Pattern B: First-Class Callables
|
||||
|
||||
First-Class Callables nutzen statische Methoden mit PHP 8.1+ Syntax.
|
||||
|
||||
**Vorteile:**
|
||||
- Modern und lesbar
|
||||
- Cachebar (class + method als Strings)
|
||||
- Flexibel mit DI
|
||||
|
||||
**Beispiel:**
|
||||
|
||||
```php
|
||||
// Policy-Klasse mit statischen Methoden
|
||||
final readonly class UserPolicies
|
||||
{
|
||||
public static function isAdmin(AttributeExecutionContext $context): bool
|
||||
{
|
||||
$user = $context->container->get(UserService::class)->getCurrentUser();
|
||||
return $user->isAdmin();
|
||||
}
|
||||
|
||||
public static function hasRole(
|
||||
string $role,
|
||||
AttributeExecutionContext $context
|
||||
): bool {
|
||||
$user = $context->container->get(UserService::class)->getCurrentUser();
|
||||
return $user->hasRole($role);
|
||||
}
|
||||
}
|
||||
|
||||
// Verwendung
|
||||
#[Guard(UserPolicies::isAdmin(...))]
|
||||
class AdminController {}
|
||||
|
||||
#[Guard(UserPolicies::hasRole('editor', ...))]
|
||||
class EditorController {}
|
||||
```
|
||||
|
||||
### Pattern C: Closure-Factories
|
||||
|
||||
Closure-Factories erstellen parametrisierte Closures zur Laufzeit.
|
||||
|
||||
**Vorteile:**
|
||||
- Parametrisierbar
|
||||
- Cachebar (nur Factory-Metadaten, nicht die Closure)
|
||||
- Flexibel für komplexe Logik
|
||||
|
||||
**Beispiel:**
|
||||
|
||||
```php
|
||||
// Factory-Klasse
|
||||
final readonly class Policies
|
||||
{
|
||||
public static function requirePermission(string $permission): Closure
|
||||
{
|
||||
return static function (AttributeExecutionContext $context) use ($permission): bool {
|
||||
$user = $context->container->get(UserService::class)->getCurrentUser();
|
||||
return $user->hasPermission($permission);
|
||||
};
|
||||
}
|
||||
|
||||
public static function requireAnyPermission(string ...$permissions): Closure
|
||||
{
|
||||
return static function (AttributeExecutionContext $context) use ($permissions): bool {
|
||||
$user = $context->container->get(UserService::class)->getCurrentUser();
|
||||
foreach ($permissions as $perm) {
|
||||
if ($user->hasPermission($perm)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Verwendung
|
||||
#[Guard(Policies::requirePermission('edit_post'))]
|
||||
class PostController {}
|
||||
|
||||
#[Guard(Policies::requireAnyPermission('edit_post', 'delete_post'))]
|
||||
class PostAdminController {}
|
||||
```
|
||||
|
||||
## Verfügbare Attribute
|
||||
|
||||
### BeforeExecute
|
||||
|
||||
Führt Logik VOR Handler-Ausführung aus.
|
||||
|
||||
```php
|
||||
final readonly class UpdateUserHandler
|
||||
{
|
||||
#[BeforeExecute(Validators::validateInput(...))]
|
||||
#[BeforeExecute(RateLimiters::perUser(10, 60))]
|
||||
#[CommandHandler]
|
||||
public function handle(UpdateUserCommand $command): void
|
||||
{
|
||||
// Handler-Logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### AfterExecute
|
||||
|
||||
Führt Logik NACH erfolgreicher Handler-Ausführung aus.
|
||||
|
||||
```php
|
||||
final readonly class CreateOrderHandler
|
||||
{
|
||||
#[AfterExecute(Notifiers::sendConfirmation(...))]
|
||||
#[AfterExecute(AuditLoggers::logOrderCreated(...))]
|
||||
#[CommandHandler]
|
||||
public function handle(CreateOrderCommand $command): Order
|
||||
{
|
||||
// Handler-Logic
|
||||
return $order;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### OnError
|
||||
|
||||
Führt Logik bei Fehlern aus.
|
||||
|
||||
```php
|
||||
final readonly class CallExternalApiHandler
|
||||
{
|
||||
#[OnError(ErrorHandlers::logAndNotify(...))]
|
||||
#[OnError(RetryHandlers::scheduleRetry(...))]
|
||||
#[CommandHandler]
|
||||
public function handle(CallApiCommand $command): Response
|
||||
{
|
||||
// Handler-Logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Guard
|
||||
|
||||
Schützt Methoden/Klassen mit Guards.
|
||||
|
||||
```php
|
||||
#[Guard(PermissionGuard::class, ['edit_post'])]
|
||||
#[Guard(UserPolicies::isAdmin(...))]
|
||||
#[Guard(Policies::requirePermission('edit_post'))]
|
||||
```
|
||||
|
||||
### Validate
|
||||
|
||||
Validiert Property- oder Parameter-Werte.
|
||||
|
||||
```php
|
||||
final readonly class User
|
||||
{
|
||||
#[Validate(EmailValidator::class)]
|
||||
public string $email;
|
||||
|
||||
#[Validate(Validators::minLength(5))]
|
||||
public string $username;
|
||||
}
|
||||
```
|
||||
|
||||
### OnBoot
|
||||
|
||||
Führt Boot-Logik beim Laden einer Klasse aus.
|
||||
|
||||
```php
|
||||
#[OnBoot(ServiceRegistrar::registerServices(...))]
|
||||
final readonly class MyService {}
|
||||
```
|
||||
|
||||
## Verwendung in Command/Query Handlers
|
||||
|
||||
### Command Handler mit Attributen
|
||||
|
||||
```php
|
||||
final readonly class UpdateUserHandler
|
||||
{
|
||||
#[BeforeExecute(Validators::validateInput(...))]
|
||||
#[BeforeExecute(RateLimiters::perUser(10, 60))]
|
||||
#[AfterExecute(Notifiers::sendEmail(...))]
|
||||
#[OnError(ErrorHandlers::logAndNotify(...))]
|
||||
#[CommandHandler]
|
||||
public function handle(UpdateUserCommand $command): void
|
||||
{
|
||||
// Business-Logic hier
|
||||
// Attribute werden automatisch ausgeführt:
|
||||
// 1. BeforeExecute Attribute (vor dieser Methode)
|
||||
// 2. Handler-Ausführung
|
||||
// 3. AfterExecute Attribute (nach erfolgreicher Ausführung)
|
||||
// 4. OnError Attribute (bei Exceptions)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Query Handler mit Attributen
|
||||
|
||||
```php
|
||||
final readonly class GetUserQueryHandler
|
||||
{
|
||||
#[BeforeExecute(CacheValidators::checkCache(...))]
|
||||
#[AfterExecute(CacheStrategies::storeResult(...))]
|
||||
#[QueryHandler]
|
||||
public function handle(GetUserQuery $query): User
|
||||
{
|
||||
// Query-Logic
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Verwendung des AttributeRunners (für manuelle Ausführung)
|
||||
|
||||
### Alle Attribute eines Typs ausführen
|
||||
|
||||
```php
|
||||
$runner = $container->get(AttributeRunner::class);
|
||||
$results = $runner->executeAttributes(Guard::class);
|
||||
```
|
||||
|
||||
### Attribute für eine Klasse ausführen
|
||||
|
||||
```php
|
||||
$className = ClassName::create('MyController');
|
||||
$results = $runner->executeForClass($className, Guard::class);
|
||||
```
|
||||
|
||||
### Attribute für eine Methode ausführen
|
||||
|
||||
```php
|
||||
$className = ClassName::create('MyController');
|
||||
$methodName = MethodName::create('edit');
|
||||
$results = $runner->executeForMethod($className, $methodName, Guard::class);
|
||||
```
|
||||
|
||||
## Cache-Kompatibilität
|
||||
|
||||
Alle drei Patterns sind vollständig cachebar:
|
||||
|
||||
- **Handler**: Nur Klassenname und Argumente werden gecacht
|
||||
- **First-Class Callables**: Nur Klassenname und Methodenname werden gecacht
|
||||
- **Closure-Factories**: Nur Factory-Klasse, Methode und Argumente werden gecacht
|
||||
|
||||
Die Closures selbst werden nicht gecacht, sondern zur Laufzeit aus den Factories erstellt.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Verwende Attribute auf Command/Query-Handlern**, nicht auf Controllern
|
||||
2. **Präferiere Pattern A (Handler)** für komplexe Logik die isoliert getestet werden soll
|
||||
3. **Präferiere Pattern B (First-Class Callables)** für einfache Policy-Checks
|
||||
4. **Präferiere Pattern C (Closure-Factories)** für parametrisierte Policies
|
||||
5. **Vermeide direkte Closures** in Attributen - nutze stattdessen Factories
|
||||
6. **Nutze Dependency Injection** über `AttributeExecutionContext` statt globale Services
|
||||
7. **BeforeExecute** für Validierung, Rate Limiting, etc.
|
||||
8. **AfterExecute** für Notifications, Audit Logs, Cache Updates
|
||||
9. **OnError** für Error Handling, Retry Logic, Logging
|
||||
|
||||
## Performance
|
||||
|
||||
- Callback-Metadata wird gecacht (nicht Closures)
|
||||
- Closures nur zur Laufzeit erstellt
|
||||
- Keine Reflection zur Laufzeit für bekannte Patterns
|
||||
- Handler werden über Container erstellt (unterstützt Lazy Loading)
|
||||
|
||||
## Testing
|
||||
|
||||
Alle Patterns sind testbar:
|
||||
|
||||
```php
|
||||
// Handler testen
|
||||
$handler = new PermissionGuard(['edit_post']);
|
||||
$result = $handler->check($context);
|
||||
|
||||
// Static Method testen
|
||||
$result = UserPolicies::isAdmin($context);
|
||||
|
||||
// Factory testen
|
||||
$closure = Policies::requirePermission('edit_post');
|
||||
$result = $closure($context);
|
||||
```
|
||||
|
||||
283
docs/features/view-transitions.md
Normal file
283
docs/features/view-transitions.md
Normal file
@@ -0,0 +1,283 @@
|
||||
# View Transitions API
|
||||
|
||||
View Transitions API für nahtlose Übergänge zwischen Seiten und Zuständen.
|
||||
|
||||
## Baseline Status
|
||||
|
||||
**Baseline 2023**: View Transitions API ist Baseline Newly available.
|
||||
|
||||
## Übersicht
|
||||
|
||||
Die View Transitions API ermöglicht nahtlose Übergänge zwischen verschiedenen Seitenzuständen ohne zusätzliche JavaScript-Bibliotheken.
|
||||
|
||||
## Verwendung
|
||||
|
||||
### Automatische Navigation Transitions
|
||||
|
||||
Die View Transitions API wird automatisch für Navigation verwendet:
|
||||
|
||||
```css
|
||||
@view-transition {
|
||||
navigation: auto;
|
||||
}
|
||||
```
|
||||
|
||||
### Manuelle View Transitions
|
||||
|
||||
```javascript
|
||||
import { transitionNavigation, transitionContentUpdate } from './modules/admin/view-transitions.js';
|
||||
|
||||
// Navigation Transition
|
||||
transitionNavigation(() => {
|
||||
window.location.href = '/new-page';
|
||||
});
|
||||
|
||||
// Content Update Transition
|
||||
transitionContentUpdate(() => {
|
||||
document.getElementById('content').innerHTML = newContent;
|
||||
});
|
||||
```
|
||||
|
||||
## Benannte Bereiche
|
||||
|
||||
### Admin-Bereiche
|
||||
|
||||
Die folgenden Bereiche haben benannte View Transitions:
|
||||
|
||||
- `admin-sidebar` - Sidebar-Navigation
|
||||
- `admin-content` - Hauptinhalt
|
||||
- `admin-header` - Header-Bereich
|
||||
- `admin-popover` - Popover-Elemente
|
||||
|
||||
### CSS-Definition
|
||||
|
||||
```css
|
||||
.admin-sidebar {
|
||||
view-transition-name: admin-sidebar;
|
||||
}
|
||||
|
||||
.admin-content {
|
||||
view-transition-name: admin-content;
|
||||
}
|
||||
|
||||
.admin-header {
|
||||
view-transition-name: admin-header;
|
||||
}
|
||||
|
||||
[popover].admin-popover {
|
||||
view-transition-name: admin-popover;
|
||||
}
|
||||
```
|
||||
|
||||
## JavaScript Helper
|
||||
|
||||
### Verfügbare Funktionen
|
||||
|
||||
#### `supportsViewTransitions()`
|
||||
|
||||
Prüft, ob View Transitions unterstützt werden:
|
||||
|
||||
```javascript
|
||||
import { supportsViewTransitions } from './modules/admin/view-transitions.js';
|
||||
|
||||
if (supportsViewTransitions()) {
|
||||
// View Transitions verfügbar
|
||||
}
|
||||
```
|
||||
|
||||
#### `transitionNavigation(callback)`
|
||||
|
||||
Startet eine View Transition für Navigation:
|
||||
|
||||
```javascript
|
||||
import { transitionNavigation } from './modules/admin/view-transitions.js';
|
||||
|
||||
transitionNavigation(() => {
|
||||
window.location.href = '/new-page';
|
||||
});
|
||||
```
|
||||
|
||||
#### `transitionContentUpdate(callback)`
|
||||
|
||||
Startet eine View Transition für Content-Updates:
|
||||
|
||||
```javascript
|
||||
import { transitionContentUpdate } from './modules/admin/view-transitions.js';
|
||||
|
||||
transitionContentUpdate(() => {
|
||||
document.getElementById('content').innerHTML = newContent;
|
||||
});
|
||||
```
|
||||
|
||||
#### `transitionPopover(popoverElement, callback)`
|
||||
|
||||
Startet eine View Transition für Popover:
|
||||
|
||||
```javascript
|
||||
import { transitionPopover } from './modules/admin/view-transitions.js';
|
||||
|
||||
const popover = document.getElementById('my-popover');
|
||||
transitionPopover(popover, () => {
|
||||
popover.showPopover();
|
||||
});
|
||||
```
|
||||
|
||||
#### `initViewTransitions()`
|
||||
|
||||
Initialisiert alle View Transitions automatisch:
|
||||
|
||||
```javascript
|
||||
import { initViewTransitions } from './modules/admin/view-transitions.js';
|
||||
|
||||
initViewTransitions();
|
||||
```
|
||||
|
||||
## CSS-Animationen
|
||||
|
||||
### Standard-Animationen
|
||||
|
||||
```css
|
||||
::view-transition-old(admin-sidebar),
|
||||
::view-transition-new(admin-sidebar) {
|
||||
animation: fade 0.15s ease-out;
|
||||
}
|
||||
|
||||
::view-transition-old(admin-content),
|
||||
::view-transition-new(admin-content) {
|
||||
animation: fade 0.15s ease-out;
|
||||
}
|
||||
|
||||
::view-transition-old(admin-header),
|
||||
::view-transition-new(admin-header) {
|
||||
animation: slide-down 0.2s ease-out;
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Animationen
|
||||
|
||||
```css
|
||||
::view-transition-old(custom-area),
|
||||
::view-transition-new(custom-area) {
|
||||
animation: custom-animation 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes custom-animation {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Benannte Bereiche verwenden
|
||||
|
||||
Verwende `view-transition-name` für präzise Animationen:
|
||||
|
||||
```css
|
||||
.important-content {
|
||||
view-transition-name: important-content;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Performance beachten
|
||||
|
||||
View Transitions sollten schnell sein (0.15s - 0.3s):
|
||||
|
||||
```css
|
||||
::view-transition-old(content),
|
||||
::view-transition-new(content) {
|
||||
animation-duration: 0.15s;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Accessibility respektieren
|
||||
|
||||
Respektiere `prefers-reduced-motion`:
|
||||
|
||||
```css
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
::view-transition-old(*),
|
||||
::view-transition-new(*) {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Fallback für nicht unterstützende Browser
|
||||
|
||||
```javascript
|
||||
if (supportsViewTransitions()) {
|
||||
transitionNavigation(() => {
|
||||
// Navigation
|
||||
});
|
||||
} else {
|
||||
// Fallback ohne Transition
|
||||
window.location.href = '/new-page';
|
||||
}
|
||||
```
|
||||
|
||||
## Admin-spezifische Patterns
|
||||
|
||||
### Navigation Links
|
||||
|
||||
Navigation Links verwenden automatisch View Transitions:
|
||||
|
||||
```html
|
||||
<a href="/admin/users" class="admin-nav__link">Users</a>
|
||||
```
|
||||
|
||||
### Content Updates
|
||||
|
||||
Content-Updates können mit View Transitions animiert werden:
|
||||
|
||||
```javascript
|
||||
transitionContentUpdate(() => {
|
||||
document.querySelector('.admin-content').innerHTML = newContent;
|
||||
});
|
||||
```
|
||||
|
||||
### Popover Transitions
|
||||
|
||||
Popover verwenden automatisch View Transitions beim Öffnen/Schließen.
|
||||
|
||||
## Browser-Support
|
||||
|
||||
- Chrome 111+
|
||||
- Firefox 129+
|
||||
- Safari 18+
|
||||
- Edge 111+
|
||||
|
||||
## Migration
|
||||
|
||||
### Von Custom Transitions migrieren
|
||||
|
||||
**Vorher:**
|
||||
```javascript
|
||||
// Custom Transition
|
||||
element.style.transition = 'opacity 0.3s';
|
||||
element.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
updateContent();
|
||||
element.style.opacity = '1';
|
||||
}, 300);
|
||||
```
|
||||
|
||||
**Nachher:**
|
||||
```javascript
|
||||
// View Transitions API
|
||||
transitionContentUpdate(() => {
|
||||
updateContent();
|
||||
});
|
||||
```
|
||||
|
||||
## Weitere Ressourcen
|
||||
|
||||
- [MDN: View Transitions API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API)
|
||||
- [web.dev: View Transitions](https://web.dev/view-transitions/)
|
||||
|
||||
365
docs/framework/csrf-protection.md
Normal file
365
docs/framework/csrf-protection.md
Normal file
@@ -0,0 +1,365 @@
|
||||
# CSRF Protection System
|
||||
|
||||
## Übersicht
|
||||
|
||||
Das CSRF (Cross-Site Request Forgery) Protection System bietet robusten Schutz gegen CSRF-Angriffe durch:
|
||||
|
||||
- **Atomare Session-Updates**: Verhindert Race Conditions bei parallelen Requests
|
||||
- **Vereinfachte Token-Generierung**: Immer neue Token, keine Wiederverwendung
|
||||
- **Robuste HTML-Verarbeitung**: DOM-basierte Token-Ersetzung mit Regex-Fallback
|
||||
- **Einheitliche API**: Konsistente Endpunkte für PHP und JavaScript
|
||||
|
||||
## Architektur
|
||||
|
||||
### Komponenten
|
||||
|
||||
1. **CsrfProtection**: Verwaltet Token-Generierung und Validierung
|
||||
2. **SessionManager**: Bietet atomare Session-Updates mit Locking
|
||||
3. **FormDataResponseProcessor**: Ersetzt Token-Platzhalter in HTML
|
||||
4. **CsrfMiddleware**: Validiert Token bei state-changing Requests
|
||||
5. **API Controllers**: Bereitstellen Token-Endpunkte für JavaScript
|
||||
|
||||
### Session-Locking
|
||||
|
||||
Das System verwendet Session-Locking, um Race Conditions zu verhindern:
|
||||
|
||||
- **FileSessionStorage**: Datei-basiertes Locking mit `flock()`
|
||||
- **RedisSessionStorage**: Redis `SET NX EX` für atomares Locking
|
||||
- **InMemorySessionStorage**: In-Memory-Locking für Tests
|
||||
|
||||
### Optimistic Locking
|
||||
|
||||
SessionData verwendet Versionsnummern für Optimistic Locking:
|
||||
|
||||
- Jede Änderung inkrementiert die Version
|
||||
- Bei Konflikten wird automatisch retried
|
||||
- Verhindert Datenverlust bei parallelen Updates
|
||||
|
||||
## Token-Generierung
|
||||
|
||||
### Vereinfachte Strategie
|
||||
|
||||
**Wichtig**: Das System generiert **immer neue Token** - keine Wiederverwendung.
|
||||
|
||||
```php
|
||||
// Generiert immer einen neuen Token
|
||||
$token = $session->csrf->generateToken($formId);
|
||||
```
|
||||
|
||||
### Token-Verwaltung
|
||||
|
||||
- **Maximal 3 Token pro Form-ID**: Älteste werden automatisch entfernt
|
||||
- **Token-Lifetime**: 2 Stunden (7200 Sekunden)
|
||||
- **Re-Submit Window**: 30 Sekunden für erneute Submissions
|
||||
- **Automatisches Cleanup**: Abgelaufene Token werden entfernt
|
||||
|
||||
### Token-Format
|
||||
|
||||
- **Länge**: 64 Zeichen
|
||||
- **Format**: Hexadezimal (0-9, a-f)
|
||||
- **Beispiel**: `f677a410facd19e4e004e41e24fa1c8abbe2379a91abcf8642de23a6988ba8b`
|
||||
|
||||
## HTML-Verarbeitung
|
||||
|
||||
### DOM-basierte Ersetzung
|
||||
|
||||
Der `FormDataResponseProcessor` verwendet DOM-basierte Verarbeitung für robuste Token-Ersetzung:
|
||||
|
||||
```php
|
||||
// Automatische Ersetzung von Platzhaltern
|
||||
$html = $processor->process($html, $session);
|
||||
```
|
||||
|
||||
### Platzhalter-Format
|
||||
|
||||
```html
|
||||
<input type="hidden" name="_token" value="___TOKEN_FORMID___">
|
||||
```
|
||||
|
||||
### Fallback-Mechanismus
|
||||
|
||||
Bei DOM-Parsing-Fehlern fällt das System automatisch auf Regex-basierte Ersetzung zurück.
|
||||
|
||||
## API-Endpunkte
|
||||
|
||||
### Token-Generierung
|
||||
|
||||
**GET/POST `/api/csrf/token`**
|
||||
|
||||
Generiert einen neuen CSRF-Token für eine Form.
|
||||
|
||||
**Parameter:**
|
||||
- `action` (optional): Form-Action URL (Standard: `/`)
|
||||
- `method` (optional): HTTP-Methode (Standard: `post`)
|
||||
- `form_id` (optional): Explizite Form-ID
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"form_id": "form_abc123def456",
|
||||
"token": "64-character-hex-token",
|
||||
"expires_in": 7200,
|
||||
"headers": {
|
||||
"X-CSRF-Form-ID": "form_abc123def456",
|
||||
"X-CSRF-Token": "64-character-hex-token"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Token-Refresh
|
||||
|
||||
**GET `/api/csrf/refresh`**
|
||||
|
||||
Generiert einen neuen Token für eine bestehende Form-ID.
|
||||
|
||||
**Parameter:**
|
||||
- `form_id` (erforderlich): Form-ID
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"form_id": "form_abc123def456",
|
||||
"token": "64-character-hex-token",
|
||||
"expires_in": 7200,
|
||||
"headers": {
|
||||
"X-CSRF-Form-ID": "form_abc123def456",
|
||||
"X-CSRF-Token": "64-character-hex-token"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Token-Informationen
|
||||
|
||||
**GET `/api/csrf/info`**
|
||||
|
||||
Gibt Informationen über aktive Token zurück.
|
||||
|
||||
**Parameter:**
|
||||
- `form_id` (erforderlich): Form-ID
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"form_id": "form_abc123def456",
|
||||
"active_tokens": 2,
|
||||
"max_tokens_per_form": 3,
|
||||
"token_lifetime_seconds": 7200,
|
||||
"resubmit_window_seconds": 30
|
||||
}
|
||||
```
|
||||
|
||||
## JavaScript-Integration
|
||||
|
||||
### CsrfManager
|
||||
|
||||
Der `CsrfManager` verwaltet Token automatisch:
|
||||
|
||||
```javascript
|
||||
import { CsrfManager } from './modules/security/CsrfManager.js';
|
||||
|
||||
const csrfManager = CsrfManager.create({
|
||||
endpoint: '/api/csrf/token',
|
||||
autoRefresh: true,
|
||||
refreshInterval: 30 * 60 * 1000 // 30 Minuten
|
||||
});
|
||||
|
||||
// Token abrufen
|
||||
const token = csrfManager.getToken();
|
||||
|
||||
// Token-Header für Requests
|
||||
const headers = csrfManager.getTokenHeader();
|
||||
```
|
||||
|
||||
### Manuelle Token-Anfrage
|
||||
|
||||
```javascript
|
||||
// Token für spezifische Form generieren
|
||||
const response = await fetch('/api/csrf/token?action=/submit&method=post');
|
||||
const data = await response.json();
|
||||
|
||||
const token = data.token;
|
||||
const formId = data.form_id;
|
||||
```
|
||||
|
||||
### AJAX-Requests
|
||||
|
||||
```javascript
|
||||
// Token in Request-Body
|
||||
fetch('/api/submit', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Form-ID': formId,
|
||||
'X-CSRF-Token': token
|
||||
},
|
||||
body: JSON.stringify({
|
||||
_form_id: formId,
|
||||
_token: token,
|
||||
// ... weitere Daten
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
## Validierung
|
||||
|
||||
### Middleware
|
||||
|
||||
Die `CsrfMiddleware` validiert automatisch Token für:
|
||||
|
||||
- POST Requests
|
||||
- PUT Requests
|
||||
- DELETE Requests
|
||||
- PATCH Requests
|
||||
|
||||
GET Requests werden nicht validiert.
|
||||
|
||||
### Validierungsprozess
|
||||
|
||||
1. **Token-Extraktion**: Aus Request-Body oder Headers
|
||||
2. **Form-ID-Extraktion**: Aus Request-Body oder Headers
|
||||
3. **Session-Lookup**: Token in Session suchen
|
||||
4. **Validierung**: Token-Match und Expiration prüfen
|
||||
5. **Markierung**: Token als verwendet markieren
|
||||
|
||||
### Fehlerbehandlung
|
||||
|
||||
Bei Validierungsfehlern wird eine `CsrfValidationFailedException` geworfen mit:
|
||||
|
||||
- Fehlgrund (missing token, invalid token, expired, etc.)
|
||||
- Session-ID
|
||||
- Anzahl verfügbarer Token
|
||||
- Liste verfügbarer Form-IDs
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Form-IDs
|
||||
|
||||
- Verwende konsistente Form-IDs für dieselben Formulare
|
||||
- Form-IDs werden automatisch aus Action und Method generiert
|
||||
- Explizite Form-IDs können über `form_id` Parameter gesetzt werden
|
||||
|
||||
### Token-Refresh
|
||||
|
||||
- Token sollten vor Ablauf erneuert werden
|
||||
- Automatischer Refresh alle 30 Minuten empfohlen
|
||||
- Bei langen Formularen manuell vor Submit refreshen
|
||||
|
||||
### Fehlerbehandlung
|
||||
|
||||
```php
|
||||
try {
|
||||
// Form-Verarbeitung
|
||||
} catch (CsrfValidationFailedException $e) {
|
||||
// Token ungültig - Benutzer sollte Seite neu laden
|
||||
return redirect()->back()->with('error', 'Session expired. Please try again.');
|
||||
}
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```php
|
||||
// Token generieren
|
||||
$token = $session->csrf->generateToken('test-form');
|
||||
|
||||
// Token validieren
|
||||
$isValid = $session->csrf->validateToken('test-form', $token);
|
||||
|
||||
// Mit Debug-Informationen
|
||||
$result = $session->csrf->validateTokenWithDebug('test-form', $token);
|
||||
if (!$result['valid']) {
|
||||
$reason = $result['debug']['reason'];
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Token wird nicht akzeptiert
|
||||
|
||||
1. **Prüfe Token-Länge**: Muss genau 64 Zeichen sein
|
||||
2. **Prüfe Form-ID**: Muss mit generierter Form-ID übereinstimmen
|
||||
3. **Prüfe Session**: Token muss in Session vorhanden sein
|
||||
4. **Prüfe Expiration**: Token darf nicht abgelaufen sein
|
||||
|
||||
### Race Conditions
|
||||
|
||||
Das System verwendet atomare Session-Updates, um Race Conditions zu verhindern. Bei Problemen:
|
||||
|
||||
1. Prüfe Session-Storage-Konfiguration
|
||||
2. Prüfe Locking-Mechanismus (File/Redis)
|
||||
3. Prüfe Logs für Version-Conflicts
|
||||
|
||||
### HTML-Verarbeitung
|
||||
|
||||
Bei Problemen mit Token-Ersetzung:
|
||||
|
||||
1. Prüfe Platzhalter-Format: `___TOKEN_FORMID___`
|
||||
2. Prüfe HTML-Struktur (DOM-Parsing kann bei malformed HTML fehlschlagen)
|
||||
3. Prüfe Logs für Fallback auf Regex
|
||||
|
||||
## Migration
|
||||
|
||||
### Von altem System
|
||||
|
||||
Das neue System ist vollständig kompatibel mit dem alten System:
|
||||
|
||||
- Alte Token werden weiterhin akzeptiert
|
||||
- Alte API-Endpunkte funktionieren weiterhin
|
||||
- Keine Breaking Changes
|
||||
|
||||
### Neue Features nutzen
|
||||
|
||||
1. Verwende `/api/csrf/token` statt `/api/csrf-token`
|
||||
2. Nutze atomare Session-Updates für kritische Operationen
|
||||
3. Verwende DOM-basierte HTML-Verarbeitung (automatisch)
|
||||
|
||||
## Performance
|
||||
|
||||
### Optimierungen
|
||||
|
||||
- **Token-Cleanup**: Automatisch nach 5 Minuten
|
||||
- **Maximal 3 Token**: Begrenzt Session-Größe
|
||||
- **Locking-Timeout**: 5 Sekunden Standard-Timeout
|
||||
- **Optimistic Locking**: Reduziert Lock-Contention
|
||||
|
||||
### Monitoring
|
||||
|
||||
- Token-Anzahl pro Form-ID überwachen
|
||||
- Session-Lock-Contention überwachen
|
||||
- Token-Validierungs-Fehlerrate überwachen
|
||||
|
||||
## Sicherheit
|
||||
|
||||
### Schutz-Mechanismen
|
||||
|
||||
1. **Token-Rotation**: Neue Token nach jeder Generierung
|
||||
2. **Expiration**: Token laufen nach 2 Stunden ab
|
||||
3. **Single-Use**: Token können innerhalb von 30 Sekunden wiederverwendet werden
|
||||
4. **Session-Binding**: Token sind an Session gebunden
|
||||
|
||||
### Angriffs-Vektoren
|
||||
|
||||
Das System schützt gegen:
|
||||
|
||||
- **CSRF-Angriffe**: Token-Validierung verhindert unautorisierte Requests
|
||||
- **Token-Replay**: Expiration und Single-Use verhindern Replay
|
||||
- **Session-Hijacking**: Session-Binding verhindert Token-Diebstahl
|
||||
|
||||
## Referenz
|
||||
|
||||
### Klassen
|
||||
|
||||
- `App\Framework\Http\Session\CsrfProtection`
|
||||
- `App\Framework\Http\Session\SessionManager`
|
||||
- `App\Framework\View\Response\FormDataResponseProcessor`
|
||||
- `App\Framework\Http\Middlewares\CsrfMiddleware`
|
||||
- `App\Application\Api\CsrfController`
|
||||
- `App\Application\Controller\CsrfController`
|
||||
|
||||
### Konstanten
|
||||
|
||||
- `CsrfProtection::TOKEN_LIFETIME` = 7200 (2 Stunden)
|
||||
- `CsrfProtection::MAX_TOKENS_PER_FORM` = 3
|
||||
- `CsrfProtection::RE_SUBMIT_WINDOW` = 30 (Sekunden)
|
||||
|
||||
|
||||
257
docs/framework/data-attributes-reference.md
Normal file
257
docs/framework/data-attributes-reference.md
Normal file
@@ -0,0 +1,257 @@
|
||||
# Data Attributes Reference
|
||||
|
||||
Complete reference for all `data-*` HTML attributes used in the framework, organized by feature area.
|
||||
|
||||
## Overview
|
||||
|
||||
All data attributes are centrally managed through PHP Enums and JavaScript Constants to ensure:
|
||||
- **Type Safety**: IDE autocomplete and compile-time checks
|
||||
- **Consistency**: Unified naming conventions
|
||||
- **Refactoring**: Easy renaming across the entire codebase
|
||||
- **Documentation**: Central overview of all attributes
|
||||
|
||||
## Structure
|
||||
|
||||
Attributes are organized into specialized Enums/Constants by feature area:
|
||||
|
||||
- **LiveComponent Core** (`LiveComponentCoreAttribute` / `LiveComponentCoreAttributes`)
|
||||
- **LiveComponent Features** (`LiveComponentFeatureAttribute` / `LiveComponentFeatureAttributes`)
|
||||
- **LiveComponent Lazy Loading** (`LiveComponentLazyAttribute` / `LiveComponentLazyAttributes`)
|
||||
- **Admin Tables** (`AdminTableAttribute` / `AdminTableAttributes`)
|
||||
- **Bulk Operations** (`BulkOperationAttribute` / `BulkOperationAttributes`)
|
||||
- **Action Handler** (`ActionHandlerAttribute` / `ActionHandlerAttributes`)
|
||||
- **Form Data** (`FormDataAttribute` / `FormDataAttributes`)
|
||||
- **State Management** (`StateDataAttribute` / `StateDataAttributes`)
|
||||
- **UI Enhancements** (`UIDataAttribute` / `UIDataAttributes`)
|
||||
- **Module System** (`ModuleDataAttribute` / `ModuleDataAttributes`)
|
||||
|
||||
## Usage
|
||||
|
||||
### PHP Backend
|
||||
|
||||
```php
|
||||
use App\Framework\View\ValueObjects\LiveComponentCoreAttribute;
|
||||
use App\Framework\View\Dom\ElementNode;
|
||||
|
||||
// Get attribute name
|
||||
$attr = LiveComponentCoreAttribute::LIVE_COMPONENT->value();
|
||||
// Returns: "data-live-component"
|
||||
|
||||
// Convert to CSS selector
|
||||
$selector = LiveComponentCoreAttribute::LIVE_COMPONENT->toSelector();
|
||||
// Returns: "[data-live-component]"
|
||||
|
||||
// Convert to JavaScript dataset key
|
||||
$datasetKey = LiveComponentCoreAttribute::LIVE_COMPONENT->toDatasetKey();
|
||||
// Returns: "liveComponent"
|
||||
|
||||
// Use directly in methods (no ->value() needed!)
|
||||
$element = new ElementNode('div');
|
||||
$element->setAttribute(LiveComponentCoreAttribute::LIVE_COMPONENT, 'my-component');
|
||||
$element->hasAttribute(LiveComponentCoreAttribute::LIVE_COMPONENT); // true
|
||||
$value = $element->getAttribute(LiveComponentCoreAttribute::LIVE_COMPONENT); // "my-component"
|
||||
|
||||
// For array keys, ->value() is still required (PHP limitation)
|
||||
$attributes = [
|
||||
LiveComponentCoreAttribute::LIVE_COMPONENT->value() => 'my-component',
|
||||
LiveComponentCoreAttribute::STATE->value() => '{}',
|
||||
];
|
||||
```
|
||||
|
||||
### JavaScript Frontend
|
||||
|
||||
```javascript
|
||||
import { LiveComponentCoreAttributes, toDatasetKey } from '/assets/js/core/DataAttributes.js';
|
||||
|
||||
// Get attribute name
|
||||
const attr = LiveComponentCoreAttributes.LIVE_COMPONENT;
|
||||
// Returns: "data-live-component"
|
||||
|
||||
// Use in getAttribute
|
||||
element.getAttribute(LiveComponentCoreAttributes.LIVE_COMPONENT);
|
||||
|
||||
// Use in dataset (with helper)
|
||||
element.dataset[toDatasetKey(LiveComponentCoreAttributes.LIVE_COMPONENT)];
|
||||
|
||||
// Use in querySelector
|
||||
document.querySelector(`[${LiveComponentCoreAttributes.LIVE_COMPONENT}]`);
|
||||
```
|
||||
|
||||
## LiveComponent Core Attributes
|
||||
|
||||
Core data-* attributes for component identification, state, security, and real-time communication.
|
||||
|
||||
| Attribute | Enum Case | Description |
|
||||
|-----------|-----------|-------------|
|
||||
| `data-live-component` | `LIVE_COMPONENT` | Component root element identifier |
|
||||
| `data-component-id` | `COMPONENT_ID` | Unique component instance ID |
|
||||
| `data-live-id` | `LIVE_ID` | Alternative component ID |
|
||||
| `data-state` | `STATE` | Component state (JSON) |
|
||||
| `data-live-state` | `LIVE_STATE` | Alternative state attribute |
|
||||
| `data-live-content` | `LIVE_CONTENT` | Component content container |
|
||||
| `data-live-action` | `LIVE_ACTION` | Action name to trigger |
|
||||
| `data-csrf-token` | `CSRF_TOKEN` | Component-specific CSRF token |
|
||||
| `data-sse-channel` | `SSE_CHANNEL` | Server-Sent Events channel |
|
||||
| `data-poll-interval` | `POLL_INTERVAL` | Polling interval in milliseconds |
|
||||
| `data-live-upload` | `LIVE_UPLOAD` | File upload handler |
|
||||
| `data-live-dropzone` | `LIVE_DROPZONE` | File dropzone handler |
|
||||
| `data-live-polite` | `LIVE_POLITE` | Accessibility politeness level |
|
||||
|
||||
## LiveComponent Feature Attributes
|
||||
|
||||
Advanced feature attributes (data-lc-*) for data binding, fragments, URL management, transitions, and triggers.
|
||||
|
||||
| Attribute | Enum Case | Description |
|
||||
|-----------|-----------|-------------|
|
||||
| `data-lc-model` | `LC_MODEL` | Two-way data binding |
|
||||
| `data-lc-fragment` | `LC_FRAGMENT` | Fragment identifier |
|
||||
| `data-lc-fragments` | `LC_FRAGMENTS` | Comma-separated fragment list |
|
||||
| `data-lc-key` | `LC_KEY` | Element key for DOM matching |
|
||||
| `data-lc-boost` | `LC_BOOST` | Progressive enhancement |
|
||||
| `data-lc-push-url` | `LC_PUSH_URL` | Push URL to browser history |
|
||||
| `data-lc-replace-url` | `LC_REPLACE_URL` | Replace URL without history entry |
|
||||
| `data-lc-target` | `LC_TARGET` | Target element selector |
|
||||
| `data-lc-swap` | `LC_SWAP` | DOM swap strategy |
|
||||
| `data-lc-swap-transition` | `LC_SWAP_TRANSITION` | Transition animation |
|
||||
| `data-lc-keep-focus` | `LC_KEEP_FOCUS` | Preserve focus after update |
|
||||
| `data-lc-scroll` | `LC_SCROLL` | Enable scrolling |
|
||||
| `data-lc-scroll-target` | `LC_SCROLL_TARGET` | Scroll target selector |
|
||||
| `data-lc-scroll-behavior` | `LC_SCROLL_BEHAVIOR` | Scroll behavior (smooth/instant) |
|
||||
| `data-lc-indicator` | `LC_INDICATOR` | Loading indicator selector |
|
||||
| `data-lc-trigger-delay` | `LC_TRIGGER_DELAY` | Trigger delay in milliseconds |
|
||||
| `data-lc-trigger-throttle` | `LC_TRIGGER_THROTTLE` | Trigger throttle in milliseconds |
|
||||
| `data-lc-trigger-once` | `LC_TRIGGER_ONCE` | Trigger only once |
|
||||
| `data-lc-trigger-changed` | `LC_TRIGGER_CHANGED` | Trigger only on value change |
|
||||
| `data-lc-trigger-from` | `LC_TRIGGER_FROM` | Trigger from another element |
|
||||
| `data-lc-trigger-load` | `LC_TRIGGER_LOAD` | Trigger on page load |
|
||||
|
||||
## LiveComponent Lazy Loading Attributes
|
||||
|
||||
Attributes for lazy loading and island rendering of LiveComponents.
|
||||
|
||||
| Attribute | Enum Case | Description |
|
||||
|-----------|-----------|-------------|
|
||||
| `data-live-component-lazy` | `LIVE_COMPONENT_LAZY` | Lazy-loaded component ID |
|
||||
| `data-live-component-island` | `LIVE_COMPONENT_ISLAND` | Island component ID |
|
||||
| `data-island-component` | `ISLAND_COMPONENT` | Mark as island component |
|
||||
| `data-lazy-priority` | `LAZY_PRIORITY` | Loading priority (high/normal/low) |
|
||||
| `data-lazy-threshold` | `LAZY_THRESHOLD` | Intersection observer threshold |
|
||||
| `data-lazy-placeholder` | `LAZY_PLACEHOLDER` | Placeholder text |
|
||||
|
||||
## Admin Table Attributes
|
||||
|
||||
Data attributes for admin table functionality including sorting, pagination, searching, and column configuration.
|
||||
|
||||
| Attribute | Enum Case | Description |
|
||||
|-----------|-----------|-------------|
|
||||
| `data-resource` | `RESOURCE` | Resource name |
|
||||
| `data-api-endpoint` | `API_ENDPOINT` | API endpoint URL |
|
||||
| `data-sortable` | `SORTABLE` | Enable sorting |
|
||||
| `data-searchable` | `SEARCHABLE` | Enable searching |
|
||||
| `data-paginated` | `PAGINATED` | Enable pagination |
|
||||
| `data-per-page` | `PER_PAGE` | Items per page |
|
||||
| `data-column` | `COLUMN` | Column identifier |
|
||||
| `data-sort-dir` | `SORT_DIR` | Sort direction (asc/desc) |
|
||||
| `data-page` | `PAGE` | Page number |
|
||||
| `data-table-search` | `TABLE_SEARCH` | Search container selector |
|
||||
| `data-table-pagination` | `TABLE_PAGINATION` | Pagination container selector |
|
||||
|
||||
## Bulk Operation Attributes
|
||||
|
||||
Data attributes for bulk operations on admin tables.
|
||||
|
||||
| Attribute | Enum Case | Description |
|
||||
|-----------|-----------|-------------|
|
||||
| `data-bulk-operations` | `BULK_OPERATIONS` | Enable bulk operations |
|
||||
| `data-bulk-toolbar` | `BULK_TOOLBAR` | Toolbar container selector |
|
||||
| `data-bulk-count` | `BULK_COUNT` | Selected count element selector |
|
||||
| `data-bulk-buttons` | `BULK_BUTTONS` | Action buttons container selector |
|
||||
| `data-bulk-initialized` | `BULK_INITIALIZED` | Initialization flag |
|
||||
| `data-bulk-select-all` | `BULK_SELECT_ALL` | Select all checkbox |
|
||||
| `data-bulk-item-id` | `BULK_ITEM_ID` | Item ID for bulk selection |
|
||||
| `data-bulk-action` | `BULK_ACTION` | Bulk action name |
|
||||
| `data-bulk-method` | `BULK_METHOD` | HTTP method for bulk action |
|
||||
| `data-bulk-confirm` | `BULK_CONFIRM` | Confirmation message |
|
||||
|
||||
## Action Handler Attributes
|
||||
|
||||
Data attributes for the ActionHandler system.
|
||||
|
||||
| Attribute | Enum Case | Description |
|
||||
|-----------|-----------|-------------|
|
||||
| `data-action` | `ACTION` | Action name |
|
||||
| `data-action-handler` | `ACTION_HANDLER` | Handler name |
|
||||
| `data-action-url` | `ACTION_URL` | Direct action URL |
|
||||
| `data-action-method` | `ACTION_METHOD` | HTTP method |
|
||||
| `data-action-type` | `ACTION_TYPE` | Action type (e.g., "window") |
|
||||
| `data-action-handler-container` | `ACTION_HANDLER_CONTAINER` | Container selector |
|
||||
| `data-action-confirm` | `ACTION_CONFIRM` | Confirmation message |
|
||||
| `data-action-loading-text` | `ACTION_LOADING_TEXT` | Loading text |
|
||||
| `data-action-success-toast` | `ACTION_SUCCESS_TOAST` | Success message |
|
||||
| `data-action-error-toast` | `ACTION_ERROR_TOAST` | Error message |
|
||||
|
||||
**Note**: `data-action-param-*` is a pattern (not an enum case) for dynamic parameters.
|
||||
|
||||
## Form Data Attributes
|
||||
|
||||
Data attributes for form handling and validation.
|
||||
|
||||
| Attribute | Enum Case | Description |
|
||||
|-----------|-----------|-------------|
|
||||
| `data-field` | `FIELD` | Form field identifier |
|
||||
| `data-selected-if` | `SELECTED_IF` | Conditional selection |
|
||||
| `data-checked-if` | `CHECKED_IF` | Conditional check |
|
||||
|
||||
**Note**: `data-param-*` is a pattern (not an enum case) for dynamic parameters.
|
||||
|
||||
## State Management Attributes
|
||||
|
||||
Data attributes for client-side state management and data binding.
|
||||
|
||||
| Attribute | Enum Case | Description |
|
||||
|-----------|-----------|-------------|
|
||||
| `data-bind` | `BIND` | Bind to state key |
|
||||
| `data-bind-attr` | `BIND_ATTR` | Bind attribute to state |
|
||||
| `data-bind-attr-name` | `BIND_ATTR_NAME` | Attribute name for binding |
|
||||
| `data-bind-class` | `BIND_CLASS` | Bind class to state |
|
||||
| `data-bind-input` | `BIND_INPUT` | Two-way input binding |
|
||||
| `data-persistent` | `PERSISTENT` | Persist state in localStorage |
|
||||
|
||||
## UI Enhancement Attributes
|
||||
|
||||
Data attributes for UI enhancements including loading states, optimistic updates, confirmations, modals, themes, and other UI features.
|
||||
|
||||
| Attribute | Enum Case | Description |
|
||||
|-----------|-----------|-------------|
|
||||
| `data-loading` | `LOADING` | Enable loading state |
|
||||
| `data-loading-text` | `LOADING_TEXT` | Loading text |
|
||||
| `data-original-text` | `ORIGINAL_TEXT` | Original text (for restoration) |
|
||||
| `data-optimistic` | `OPTIMISTIC` | Enable optimistic updates |
|
||||
| `data-rollback` | `ROLLBACK` | Rollback flag |
|
||||
| `data-confirm-ok` | `CONFIRM_OK` | Confirm OK button |
|
||||
| `data-confirm-cancel` | `CONFIRM_CANCEL` | Confirm cancel button |
|
||||
| `data-close-modal` | `CLOSE_MODAL` | Close modal button |
|
||||
| `data-tag` | `TAG` | Tag identifier |
|
||||
| `data-theme` | `THEME` | Theme name |
|
||||
| `data-theme-icon` | `THEME_ICON` | Theme icon selector |
|
||||
| `data-mobile-menu-open` | `MOBILE_MENU_OPEN` | Mobile menu state |
|
||||
| `data-section-id` | `SECTION_ID` | Section identifier |
|
||||
| `data-tab` | `TAB` | Tab identifier |
|
||||
| `data-view-mode` | `VIEW_MODE` | View mode |
|
||||
| `data-asset-id` | `ASSET_ID` | Asset identifier |
|
||||
|
||||
## Module System Attributes
|
||||
|
||||
Data attributes for the JavaScript module initialization system.
|
||||
|
||||
| Attribute | Enum Case | Description |
|
||||
|-----------|-----------|-------------|
|
||||
| `data-module` | `MODULE` | Module name |
|
||||
| `data-options` | `OPTIONS` | Module options (JSON) |
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [LiveComponents API Reference](../livecomponents/api-reference.md)
|
||||
- [Action Handler Guide](../javascript/action-handler-guide.md)
|
||||
- [Framework Architecture](../README.md)
|
||||
|
||||
262
docs/guides/migrating-to-modern-features.md
Normal file
262
docs/guides/migrating-to-modern-features.md
Normal file
@@ -0,0 +1,262 @@
|
||||
# Migration Guide: Modern Web Features
|
||||
|
||||
Schritt-für-Schritt Migration zu modernen Baseline-Features.
|
||||
|
||||
## Übersicht
|
||||
|
||||
Dieser Guide beschreibt die Migration von Legacy-Implementierungen zu modernen Baseline-Features:
|
||||
|
||||
1. `<form role="search">` → `<search>` Element
|
||||
2. Custom Dropdowns → Popover API
|
||||
3. Custom Modals → Popover API (wo anwendbar)
|
||||
4. Custom Transitions → View Transitions API
|
||||
|
||||
## 1. Search Element Migration
|
||||
|
||||
### Vorher: Form mit role="search"
|
||||
|
||||
```html
|
||||
<form class="admin-search" role="search" action="/search" method="get">
|
||||
<label for="search-input" class="admin-visually-hidden">Search</label>
|
||||
<input
|
||||
type="search"
|
||||
id="search-input"
|
||||
name="q"
|
||||
class="admin-search__input"
|
||||
placeholder="Search..."
|
||||
/>
|
||||
<button type="submit" class="admin-search__submit">Search</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
### Nachher: Search Component
|
||||
|
||||
```html
|
||||
<x-search
|
||||
action="/search"
|
||||
method="get"
|
||||
name="q"
|
||||
placeholder="Search..."
|
||||
variant="header"
|
||||
/>
|
||||
```
|
||||
|
||||
### CSS-Anpassungen
|
||||
|
||||
**Vorher:**
|
||||
```css
|
||||
.admin-search {
|
||||
/* Styles */
|
||||
}
|
||||
```
|
||||
|
||||
**Nachher:**
|
||||
```css
|
||||
search.admin-search {
|
||||
/* Styles bleiben gleich, aber Selektor ändert sich */
|
||||
}
|
||||
```
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- CSS-Selektoren müssen von `.admin-search` zu `search.admin-search` geändert werden
|
||||
- JavaScript-Selektoren müssen angepasst werden
|
||||
|
||||
## 2. Popover API Migration
|
||||
|
||||
### Vorher: Custom Dropdown
|
||||
|
||||
```html
|
||||
<div class="dropdown" data-dropdown>
|
||||
<button class="dropdown-trigger" data-dropdown-trigger="menu">
|
||||
Menu
|
||||
</button>
|
||||
<ul class="dropdown-menu" data-dropdown-content="menu">
|
||||
<li><a href="/item1">Item 1</a></li>
|
||||
<li><a href="/item2">Item 2</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
```
|
||||
|
||||
```javascript
|
||||
// Custom JavaScript
|
||||
document.querySelectorAll('[data-dropdown-trigger]').forEach(trigger => {
|
||||
trigger.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const dropdown = trigger.closest('[data-dropdown]');
|
||||
const isOpen = dropdown.dataset.open === 'true';
|
||||
dropdown.dataset.open = !isOpen;
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
```css
|
||||
.dropdown-menu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dropdown[data-open="true"] .dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
```
|
||||
|
||||
### Nachher: Popover API
|
||||
|
||||
```html
|
||||
<button popovertarget="menu-popover" aria-haspopup="true" aria-expanded="false">
|
||||
Menu
|
||||
</button>
|
||||
|
||||
<x-popover id="menu-popover" type="auto" position="bottom-start">
|
||||
<ul role="menu">
|
||||
<li><a href="/item1">Item 1</a></li>
|
||||
<li><a href="/item2">Item 2</a></li>
|
||||
</ul>
|
||||
</x-popover>
|
||||
```
|
||||
|
||||
```javascript
|
||||
// Minimales JavaScript nur für aria-expanded
|
||||
document.querySelectorAll('[popovertarget]').forEach(trigger => {
|
||||
const popoverId = trigger.getAttribute('popovertarget');
|
||||
const popover = document.getElementById(popoverId);
|
||||
|
||||
if (popover) {
|
||||
popover.addEventListener('toggle', (e) => {
|
||||
const isOpen = popover.matches(':popover-open');
|
||||
trigger.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
```css
|
||||
[popover] {
|
||||
/* Popover Styles */
|
||||
}
|
||||
|
||||
[popover]:not(:popover-open) {
|
||||
display: none;
|
||||
}
|
||||
```
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- `data-dropdown` Attribute werden entfernt
|
||||
- Custom JavaScript für Dropdown-Management kann entfernt werden
|
||||
- CSS-Selektoren ändern sich von `[data-open="true"]` zu `:popover-open`
|
||||
|
||||
## 3. View Transitions Migration
|
||||
|
||||
### Vorher: Custom Transitions
|
||||
|
||||
```javascript
|
||||
// Custom Transition mit setTimeout
|
||||
function navigateTo(url) {
|
||||
const content = document.querySelector('.content');
|
||||
content.style.transition = 'opacity 0.3s';
|
||||
content.style.opacity = '0';
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.href = url;
|
||||
}, 300);
|
||||
}
|
||||
```
|
||||
|
||||
```css
|
||||
.content {
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
```
|
||||
|
||||
### Nachher: View Transitions API
|
||||
|
||||
```javascript
|
||||
// View Transitions API
|
||||
import { transitionNavigation } from './modules/admin/view-transitions.js';
|
||||
|
||||
function navigateTo(url) {
|
||||
transitionNavigation(() => {
|
||||
window.location.href = url;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
```css
|
||||
.content {
|
||||
view-transition-name: content;
|
||||
}
|
||||
|
||||
::view-transition-old(content),
|
||||
::view-transition-new(content) {
|
||||
animation: fade 0.15s ease-out;
|
||||
}
|
||||
```
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Custom Transition-Logik kann entfernt werden
|
||||
- CSS-Transitions werden durch View Transitions ersetzt
|
||||
|
||||
## Schritt-für-Schritt Migration
|
||||
|
||||
### Schritt 1: Search Element
|
||||
|
||||
1. Ersetze alle `<form role="search">` durch `<x-search>` Komponente
|
||||
2. Aktualisiere CSS-Selektoren von `.admin-search` zu `search.admin-search`
|
||||
3. Teste alle Search-Formulare
|
||||
|
||||
### Schritt 2: Popover API
|
||||
|
||||
1. Identifiziere alle Custom Dropdowns
|
||||
2. Ersetze durch `<x-popover>` Komponente
|
||||
3. Entferne Custom Dropdown-Management JavaScript
|
||||
4. Aktualisiere CSS-Selektoren
|
||||
5. Teste alle Dropdowns
|
||||
|
||||
### Schritt 3: View Transitions
|
||||
|
||||
1. Identifiziere alle Custom Transitions
|
||||
2. Ersetze durch View Transitions API
|
||||
3. Definiere `view-transition-name` für wichtige Bereiche
|
||||
4. Teste alle Transitions
|
||||
|
||||
## Fallback-Strategien
|
||||
|
||||
### View Transitions Fallback
|
||||
|
||||
```javascript
|
||||
import { supportsViewTransitions, transitionNavigation } from './modules/admin/view-transitions.js';
|
||||
|
||||
function navigate(url) {
|
||||
if (supportsViewTransitions()) {
|
||||
transitionNavigation(() => {
|
||||
window.location.href = url;
|
||||
});
|
||||
} else {
|
||||
// Fallback ohne Transition
|
||||
window.location.href = url;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Popover Fallback
|
||||
|
||||
Die Popover API hat keinen direkten Fallback. Für nicht unterstützende Browser sollte ein Polyfill verwendet werden oder die Custom Dropdown-Implementierung beibehalten werden.
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Alle Search-Formulare funktionieren
|
||||
- [ ] Alle Dropdowns öffnen/schließen korrekt
|
||||
- [ ] View Transitions funktionieren
|
||||
- [ ] Keyboard-Navigation funktioniert
|
||||
- [ ] Screen Reader Support funktioniert
|
||||
- [ ] Mobile Responsiveness funktioniert
|
||||
- [ ] Browser-Kompatibilität geprüft
|
||||
|
||||
## Weitere Ressourcen
|
||||
|
||||
- [Search Component Dokumentation](./components/search.md)
|
||||
- [Popover Component Dokumentation](./components/popover.md)
|
||||
- [View Transitions Dokumentation](./features/view-transitions.md)
|
||||
|
||||
523
docs/javascript/action-handler-guide.md
Normal file
523
docs/javascript/action-handler-guide.md
Normal file
@@ -0,0 +1,523 @@
|
||||
# ActionHandler Guide
|
||||
|
||||
## Übersicht
|
||||
|
||||
Das `ActionHandler` Modul bietet ein wiederverwendbares Pattern für Event-Delegation-basiertes Action-Handling mit automatischem CSRF-Token-Handling, Loading States, Bestätigungen und Toast-Integration.
|
||||
|
||||
## Konzepte
|
||||
|
||||
### Event Delegation
|
||||
|
||||
Statt jedem Button einen eigenen Event Listener zuzuweisen, verwendet ActionHandler Event Delegation: Ein einziger Event Listener auf dem Container-Element behandelt alle Actions.
|
||||
|
||||
**Vorteile:**
|
||||
- Bessere Performance (weniger Event Listener)
|
||||
- Funktioniert mit dynamisch hinzugefügten Elementen
|
||||
- Einfacheres Code-Management
|
||||
|
||||
### Handler-Registry
|
||||
|
||||
Handler sind vorkonfigurierte Action-Patterns, die wiederverwendet werden können. Sie definieren:
|
||||
- URL-Templates
|
||||
- Bestätigungs-Nachrichten
|
||||
- Loading-Texte
|
||||
- Success/Error-Messages
|
||||
|
||||
### URL-Templates
|
||||
|
||||
URL-Templates verwenden Platzhalter, die zur Laufzeit ersetzt werden:
|
||||
- `{action}` - Der Action-Name
|
||||
- `{id}` - ID aus `data-action-param-id`
|
||||
- `{param:name}` - Parameter aus `data-action-param-{name}`
|
||||
- `{data:name}` - Wert aus `data-{name}` Attribut
|
||||
|
||||
## Basis-Verwendung
|
||||
|
||||
### 1. ActionHandler initialisieren
|
||||
|
||||
```javascript
|
||||
import { ActionHandler } from '/assets/js/modules/common/ActionHandler.js';
|
||||
import { dockerContainerHandler } from '/assets/js/modules/common/ActionHandlers.js';
|
||||
|
||||
const handler = new ActionHandler('.my-container', {
|
||||
csrfTokenSelector: '[data-live-component]',
|
||||
toastHandler: showToast,
|
||||
refreshHandler: refreshComponent,
|
||||
autoRefresh: true
|
||||
});
|
||||
|
||||
// Handler registrieren
|
||||
handler.registerHandler('docker-container', dockerContainerHandler);
|
||||
```
|
||||
|
||||
### 2. HTML mit data-* Attributen
|
||||
|
||||
```html
|
||||
<div class="my-container">
|
||||
<button
|
||||
data-action="start"
|
||||
data-action-handler="docker-container"
|
||||
data-action-param-id="container-123"
|
||||
>
|
||||
Start Container
|
||||
</button>
|
||||
|
||||
<button
|
||||
data-action="stop"
|
||||
data-action-handler="docker-container"
|
||||
data-action-param-id="container-123"
|
||||
>
|
||||
Stop Container
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
## URL-Templates
|
||||
|
||||
### Einfaches Template
|
||||
|
||||
```javascript
|
||||
// Handler-Konfiguration
|
||||
const myHandler = {
|
||||
urlTemplate: '/api/users/{id}/{action}',
|
||||
method: 'POST'
|
||||
};
|
||||
|
||||
// HTML
|
||||
<button
|
||||
data-action="delete"
|
||||
data-action-handler="my-handler"
|
||||
data-action-param-id="123"
|
||||
>
|
||||
Delete User
|
||||
</button>
|
||||
|
||||
// Wird zu: /api/users/123/delete
|
||||
```
|
||||
|
||||
### Template mit mehreren Parametern
|
||||
|
||||
```javascript
|
||||
// Handler-Konfiguration
|
||||
const myHandler = {
|
||||
urlTemplate: '/api/{entity}/{id}/{action}',
|
||||
method: 'POST'
|
||||
};
|
||||
|
||||
// HTML
|
||||
<button
|
||||
data-action="update"
|
||||
data-action-handler="my-handler"
|
||||
data-action-param-entity="users"
|
||||
data-action-param-id="123"
|
||||
>
|
||||
Update User
|
||||
</button>
|
||||
|
||||
// Wird zu: /api/users/123/update
|
||||
```
|
||||
|
||||
### Template mit {data:*} Platzhaltern
|
||||
|
||||
```javascript
|
||||
// Handler-Konfiguration
|
||||
const myHandler = {
|
||||
urlTemplate: '/api/{data:entity}/{data:id}/{action}',
|
||||
method: 'POST'
|
||||
};
|
||||
|
||||
// HTML
|
||||
<button
|
||||
data-action="delete"
|
||||
data-action-handler="my-handler"
|
||||
data-entity="users"
|
||||
data-id="123"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
|
||||
// Wird zu: /api/users/123/delete
|
||||
```
|
||||
|
||||
## CSRF-Token Handling
|
||||
|
||||
ActionHandler extrahiert CSRF-Token automatisch aus verschiedenen Quellen (in Reihenfolge):
|
||||
|
||||
1. `data-csrf-token` Attribut des Action-Elements
|
||||
2. `data-csrf-token` Attribut des nächstgelegenen `[data-live-component]` Elements
|
||||
3. `data-csrf-token` Attribut des Containers
|
||||
4. Meta-Tag `<meta name="csrf-token">`
|
||||
5. Fallback: Leerer String
|
||||
|
||||
Das Token wird automatisch hinzugefügt:
|
||||
- Header: `X-CSRF-Token`
|
||||
- Body: `_token` und `_form_id`
|
||||
|
||||
## Bestätigungen
|
||||
|
||||
### Handler-basierte Bestätigungen
|
||||
|
||||
```javascript
|
||||
const myHandler = {
|
||||
urlTemplate: '/api/users/{id}/{action}',
|
||||
confirmations: {
|
||||
delete: 'Are you sure you want to delete this user?',
|
||||
update: 'Are you sure you want to update this user?'
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Element-basierte Bestätigungen
|
||||
|
||||
```html
|
||||
<button
|
||||
data-action="delete"
|
||||
data-action-confirm="Are you sure?"
|
||||
data-action-handler="my-handler"
|
||||
data-action-param-id="123"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
```
|
||||
|
||||
## Loading States
|
||||
|
||||
### Handler-basierte Loading-Texte
|
||||
|
||||
```javascript
|
||||
const myHandler = {
|
||||
loadingTexts: {
|
||||
start: 'Starting...',
|
||||
stop: 'Stopping...',
|
||||
delete: 'Deleting...'
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Element-basierte Loading-Texte
|
||||
|
||||
```html
|
||||
<button
|
||||
data-action="delete"
|
||||
data-action-loading-text="Deleting user..."
|
||||
data-action-handler="my-handler"
|
||||
data-action-param-id="123"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
```
|
||||
|
||||
## Toast-Integration
|
||||
|
||||
### Handler-basierte Toast-Messages
|
||||
|
||||
```javascript
|
||||
const myHandler = {
|
||||
successMessages: {
|
||||
delete: 'User deleted successfully',
|
||||
update: 'User updated successfully'
|
||||
},
|
||||
errorMessages: {
|
||||
delete: 'Failed to delete user',
|
||||
update: 'Failed to update user'
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Element-basierte Toast-Messages
|
||||
|
||||
```html
|
||||
<button
|
||||
data-action="delete"
|
||||
data-action-success-toast="User deleted!"
|
||||
data-action-error-toast="Failed to delete user"
|
||||
data-action-handler="my-handler"
|
||||
data-action-param-id="123"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
```
|
||||
|
||||
## Custom Handler erstellen
|
||||
|
||||
### 1. Handler-Konfiguration erstellen
|
||||
|
||||
```javascript
|
||||
// resources/js/modules/common/ActionHandlers.js
|
||||
export const myCustomHandler = {
|
||||
urlTemplate: '/api/{entity}/{id}/{action}',
|
||||
method: 'POST',
|
||||
confirmations: {
|
||||
delete: 'Are you sure?'
|
||||
},
|
||||
loadingTexts: {
|
||||
delete: 'Deleting...',
|
||||
update: 'Updating...'
|
||||
},
|
||||
successMessages: {
|
||||
delete: 'Deleted successfully',
|
||||
update: 'Updated successfully'
|
||||
},
|
||||
errorMessages: {
|
||||
delete: 'Failed to delete',
|
||||
update: 'Failed to update'
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 2. Handler registrieren
|
||||
|
||||
```javascript
|
||||
import { ActionHandler } from '/assets/js/modules/common/ActionHandler.js';
|
||||
import { myCustomHandler } from '/assets/js/modules/common/ActionHandlers.js';
|
||||
|
||||
const handler = new ActionHandler('.my-container');
|
||||
handler.registerHandler('my-handler', myCustomHandler);
|
||||
```
|
||||
|
||||
### 3. Handler verwenden
|
||||
|
||||
```html
|
||||
<button
|
||||
data-action="delete"
|
||||
data-action-handler="my-handler"
|
||||
data-action-param-entity="users"
|
||||
data-action-param-id="123"
|
||||
>
|
||||
Delete User
|
||||
</button>
|
||||
```
|
||||
|
||||
## Spezielle Action-Typen
|
||||
|
||||
### Window-Actions (öffnen in neuem Tab)
|
||||
|
||||
```html
|
||||
<button
|
||||
data-action="logs"
|
||||
data-action-type="window"
|
||||
data-action-url="/api/containers/{id}/logs"
|
||||
data-action-param-id="123"
|
||||
>
|
||||
View Logs
|
||||
</button>
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Container-Selektor verwenden
|
||||
|
||||
Verwende einen spezifischen Container-Selektor, um Konflikte zu vermeiden:
|
||||
|
||||
```javascript
|
||||
// Gut
|
||||
const handler = new ActionHandler('.docker-containers-component');
|
||||
|
||||
// Schlecht
|
||||
const handler = new ActionHandler('body');
|
||||
```
|
||||
|
||||
### 2. Handler wiederverwenden
|
||||
|
||||
Erstelle wiederverwendbare Handler für ähnliche Actions:
|
||||
|
||||
```javascript
|
||||
// Gut: Wiederverwendbarer Handler
|
||||
const userHandler = {
|
||||
urlTemplate: '/api/users/{id}/{action}',
|
||||
// ...
|
||||
};
|
||||
|
||||
// Schlecht: Inline-Konfiguration für jede Action
|
||||
```
|
||||
|
||||
### 3. Konsistente Namenskonventionen
|
||||
|
||||
Verwende konsistente Action-Namen:
|
||||
|
||||
```javascript
|
||||
// Gut: RESTful Actions
|
||||
start, stop, restart, delete, update, create
|
||||
|
||||
// Schlecht: Inkonsistente Namen
|
||||
startContainer, stopContainer, deleteUser, updateUserData
|
||||
```
|
||||
|
||||
### 4. Error Handling
|
||||
|
||||
ActionHandler behandelt Errors automatisch, aber du kannst Custom Error-Handling hinzufügen:
|
||||
|
||||
```javascript
|
||||
const handler = new ActionHandler('.container', {
|
||||
toastHandler: (message, type) => {
|
||||
if (type === 'error') {
|
||||
// Custom Error-Logging
|
||||
console.error('[ActionHandler]', message);
|
||||
}
|
||||
// Standard Toast
|
||||
showToast(message, type);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Integration in bestehende Komponenten
|
||||
|
||||
### Docker Container Actions
|
||||
|
||||
```html
|
||||
<button
|
||||
data-action="start"
|
||||
data-action-handler="docker-container"
|
||||
data-action-param-id="{{containerId}}"
|
||||
>
|
||||
Start
|
||||
</button>
|
||||
```
|
||||
|
||||
### Bulk Operations
|
||||
|
||||
```html
|
||||
<button
|
||||
data-action="delete"
|
||||
data-action-handler="bulk-operations"
|
||||
data-action-param-entity="users"
|
||||
>
|
||||
Delete Selected
|
||||
</button>
|
||||
```
|
||||
|
||||
## API-Referenz
|
||||
|
||||
### ActionHandler Konstruktor
|
||||
|
||||
```javascript
|
||||
new ActionHandler(containerSelector, options)
|
||||
```
|
||||
|
||||
**Parameter:**
|
||||
- `containerSelector` (string): CSS-Selektor für Container-Element
|
||||
- `options` (object): Konfigurationsoptionen
|
||||
- `csrfTokenSelector` (string): Selektor für CSRF-Token Element
|
||||
- `toastHandler` (function): Funktion für Toast-Anzeige
|
||||
- `confirmationHandler` (function): Funktion für Bestätigungen
|
||||
- `loadingTexts` (object): Mapping von Action-Namen zu Loading-Texten
|
||||
- `autoRefresh` (boolean): Automatisches Refresh nach erfolgreicher Action
|
||||
- `refreshHandler` (function): Funktion für Refresh
|
||||
|
||||
### registerHandler
|
||||
|
||||
```javascript
|
||||
handler.registerHandler(name, config)
|
||||
```
|
||||
|
||||
**Parameter:**
|
||||
- `name` (string): Handler-Name
|
||||
- `config` (object): Handler-Konfiguration
|
||||
- `urlTemplate` (string): URL-Template mit Platzhaltern
|
||||
- `method` (string): HTTP-Methode (default: POST)
|
||||
- `confirmations` (object): Bestätigungs-Nachrichten pro Action
|
||||
- `loadingTexts` (object): Loading-Texte pro Action
|
||||
- `successMessages` (object): Success-Messages pro Action
|
||||
- `errorMessages` (object): Error-Messages pro Action
|
||||
|
||||
### HTML-Attribute
|
||||
|
||||
- `data-action`: Action-Name (erforderlich)
|
||||
- `data-action-handler`: Handler-Name (optional)
|
||||
- `data-action-url`: Direkte URL (optional, überschreibt Template)
|
||||
- `data-action-method`: HTTP-Methode (optional, default: POST)
|
||||
- `data-action-confirm`: Bestätigungs-Nachricht (optional)
|
||||
- `data-action-loading-text`: Loading-Text (optional)
|
||||
- `data-action-success-toast`: Success-Message (optional)
|
||||
- `data-action-error-toast`: Error-Message (optional)
|
||||
- `data-action-type`: Action-Typ (optional, z.B. "window")
|
||||
- `data-action-param-*`: Parameter für URL-Template (optional)
|
||||
|
||||
## Beispiele
|
||||
|
||||
### Beispiel 1: Einfache Action
|
||||
|
||||
```html
|
||||
<div class="user-actions">
|
||||
<button
|
||||
data-action="delete"
|
||||
data-action-handler="user-handler"
|
||||
data-action-param-id="123"
|
||||
>
|
||||
Delete User
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
```javascript
|
||||
import { ActionHandler } from '/assets/js/modules/common/ActionHandler.js';
|
||||
|
||||
const handler = new ActionHandler('.user-actions');
|
||||
handler.registerHandler('user-handler', {
|
||||
urlTemplate: '/api/users/{id}/{action}',
|
||||
confirmations: {
|
||||
delete: 'Are you sure?'
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Beispiel 2: Action mit Custom URL
|
||||
|
||||
```html
|
||||
<button
|
||||
data-action="custom"
|
||||
data-action-url="/api/custom-endpoint"
|
||||
data-action-method="PUT"
|
||||
>
|
||||
Custom Action
|
||||
</button>
|
||||
```
|
||||
|
||||
### Beispiel 3: Action mit mehreren Parametern
|
||||
|
||||
```html
|
||||
<button
|
||||
data-action="transfer"
|
||||
data-action-handler="transfer-handler"
|
||||
data-action-param-from="account-1"
|
||||
data-action-param-to="account-2"
|
||||
data-action-param-amount="100"
|
||||
>
|
||||
Transfer
|
||||
</button>
|
||||
```
|
||||
|
||||
```javascript
|
||||
handler.registerHandler('transfer-handler', {
|
||||
urlTemplate: '/api/transfer/{param:from}/{param:to}/{param:amount}',
|
||||
method: 'POST'
|
||||
});
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Actions werden nicht ausgeführt
|
||||
|
||||
1. Prüfe, ob Container-Selektor korrekt ist
|
||||
2. Prüfe, ob `data-action` Attribut vorhanden ist
|
||||
3. Prüfe Browser-Konsole für Warnungen
|
||||
4. Prüfe, ob Handler registriert ist
|
||||
|
||||
### CSRF-Token-Fehler
|
||||
|
||||
1. Prüfe, ob CSRF-Token-Element vorhanden ist
|
||||
2. Prüfe, ob `csrfTokenSelector` korrekt ist
|
||||
3. Prüfe, ob Token im HTML vorhanden ist
|
||||
|
||||
### URL-Template wird nicht ersetzt
|
||||
|
||||
1. Prüfe, ob Platzhalter korrekt geschrieben sind (`{id}`, nicht `{ id }`)
|
||||
2. Prüfe, ob Parameter-Attribute vorhanden sind (`data-action-param-id`)
|
||||
3. Prüfe Browser-Konsole für Warnungen
|
||||
|
||||
### Toast wird nicht angezeigt
|
||||
|
||||
1. Prüfe, ob `toastHandler` konfiguriert ist
|
||||
2. Prüfe, ob `LiveComponentUIHelper` verfügbar ist
|
||||
3. Prüfe Browser-Konsole für Fehler
|
||||
|
||||
@@ -772,10 +772,15 @@ interface Component {
|
||||
|
||||
### Data Attributes
|
||||
|
||||
> **Note**: All data attributes are centrally managed through PHP Enums and JavaScript Constants. See [Data Attributes Reference](../../framework/data-attributes-reference.md) for complete documentation.
|
||||
|
||||
#### `data-component-id`
|
||||
|
||||
Identifies component root element. **Required** on component container.
|
||||
|
||||
**PHP Enum**: `LiveComponentCoreAttribute::COMPONENT_ID`
|
||||
**JavaScript Constant**: `LiveComponentCoreAttributes.COMPONENT_ID`
|
||||
|
||||
```html
|
||||
<div data-component-id="{component_id}" data-component-name="SearchComponent">
|
||||
<!-- Component content -->
|
||||
@@ -788,6 +793,9 @@ Identifies component root element. **Required** on component container.
|
||||
|
||||
Two-way data binding for inputs.
|
||||
|
||||
**PHP Enum**: `LiveComponentFeatureAttribute::LC_MODEL`
|
||||
**JavaScript Constant**: `LiveComponentFeatureAttributes.LC_MODEL`
|
||||
|
||||
```html
|
||||
<input
|
||||
type="text"
|
||||
@@ -817,6 +825,9 @@ Two-way data binding for inputs.
|
||||
|
||||
Trigger action on click.
|
||||
|
||||
**PHP Enum**: `LiveComponentCoreAttribute::LIVE_ACTION`
|
||||
**JavaScript Constant**: `LiveComponentCoreAttributes.LIVE_ACTION`
|
||||
|
||||
```html
|
||||
<button data-lc-action="search">Search</button>
|
||||
|
||||
|
||||
877
docs/livecomponents/end-to-end-guide.md
Normal file
877
docs/livecomponents/end-to-end-guide.md
Normal file
@@ -0,0 +1,877 @@
|
||||
# LiveComponents End-to-End Guide
|
||||
|
||||
**Complete guide to building LiveComponents from scratch to production.**
|
||||
|
||||
This guide walks you through creating, testing, and deploying LiveComponents with all features.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Quick Start (5 Minutes)](#quick-start-5-minutes)
|
||||
2. [Component Basics](#component-basics)
|
||||
3. [State Management](#state-management)
|
||||
4. [Actions & Events](#actions--events)
|
||||
5. [Advanced Features](#advanced-features)
|
||||
6. [Performance Optimization](#performance-optimization)
|
||||
7. [Real-World Examples](#real-world-examples)
|
||||
|
||||
---
|
||||
|
||||
## Quick Start (5 Minutes)
|
||||
|
||||
### Step 1: Create Component Class
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\LiveComponents\Counter;
|
||||
|
||||
use App\Framework\LiveComponents\Attributes\Action;
|
||||
use App\Framework\LiveComponents\Attributes\LiveComponent;
|
||||
use App\Framework\LiveComponents\Contracts\LiveComponentContract;
|
||||
use App\Framework\LiveComponents\ValueObjects\ComponentId;
|
||||
use App\Framework\LiveComponents\ValueObjects\ComponentRenderData;
|
||||
|
||||
#[LiveComponent('counter')]
|
||||
final readonly class CounterComponent implements LiveComponentContract
|
||||
{
|
||||
public function __construct(
|
||||
public ComponentId $id,
|
||||
public CounterState $state
|
||||
) {
|
||||
}
|
||||
|
||||
public function getRenderData(): ComponentRenderData
|
||||
{
|
||||
return new ComponentRenderData(
|
||||
templatePath: 'livecomponent-counter',
|
||||
data: [
|
||||
'componentId' => $this->id->toString(),
|
||||
'count' => $this->state->count,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[Action]
|
||||
public function increment(): CounterState
|
||||
{
|
||||
return $this->state->increment();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Create State Value Object
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\LiveComponents\Counter;
|
||||
|
||||
use App\Framework\LiveComponents\ValueObjects\ComponentState;
|
||||
|
||||
final readonly class CounterState extends ComponentState
|
||||
{
|
||||
public function __construct(
|
||||
public int $count = 0
|
||||
) {
|
||||
}
|
||||
|
||||
public function increment(): self
|
||||
{
|
||||
return new self(count: $this->count + 1);
|
||||
}
|
||||
|
||||
public static function empty(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self(
|
||||
count: $data['count'] ?? 0
|
||||
);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'count' => $this->count,
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Create Template
|
||||
|
||||
**File**: `resources/templates/livecomponent-counter.view.php`
|
||||
|
||||
```html
|
||||
<div data-component-id="{componentId}">
|
||||
<h2>Counter: {count}</h2>
|
||||
<button data-live-action="increment">Increment</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Step 4: Use in Template
|
||||
|
||||
```html
|
||||
<x-counter id="demo" />
|
||||
```
|
||||
|
||||
**That's it!** Your component is ready to use.
|
||||
|
||||
---
|
||||
|
||||
## Component Basics
|
||||
|
||||
### Component Structure
|
||||
|
||||
Every LiveComponent consists of:
|
||||
|
||||
1. **Component Class** - Implements `LiveComponentContract`
|
||||
2. **State Value Object** - Extends `ComponentState`
|
||||
3. **Template** - HTML template with placeholders
|
||||
4. **Actions** - Methods marked with `#[Action]`
|
||||
|
||||
### Component ID
|
||||
|
||||
Components are identified by a `ComponentId` consisting of:
|
||||
- **Name**: Component name from `#[LiveComponent]` attribute
|
||||
- **Instance ID**: Unique identifier (e.g., 'demo', 'user-123')
|
||||
|
||||
```php
|
||||
ComponentId::create('counter', 'demo') // counter:demo
|
||||
ComponentId::create('user-profile', '123') // user-profile:123
|
||||
```
|
||||
|
||||
### RenderData
|
||||
|
||||
`getRenderData()` returns template path and data:
|
||||
|
||||
```php
|
||||
public function getRenderData(): ComponentRenderData
|
||||
{
|
||||
return new ComponentRenderData(
|
||||
templatePath: 'livecomponent-counter',
|
||||
data: [
|
||||
'componentId' => $this->id->toString(),
|
||||
'count' => $this->state->count,
|
||||
'lastUpdate' => $this->state->lastUpdate,
|
||||
]
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## State Management
|
||||
|
||||
### State Value Objects
|
||||
|
||||
State is managed through immutable Value Objects:
|
||||
|
||||
```php
|
||||
final readonly class CounterState extends ComponentState
|
||||
{
|
||||
public function __construct(
|
||||
public int $count = 0,
|
||||
public ?string $lastUpdate = null
|
||||
) {
|
||||
}
|
||||
|
||||
// Factory methods
|
||||
public static function empty(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self(
|
||||
count: $data['count'] ?? 0,
|
||||
lastUpdate: $data['lastUpdate'] ?? null
|
||||
);
|
||||
}
|
||||
|
||||
// Transformation methods (return new instance)
|
||||
public function increment(): self
|
||||
{
|
||||
return new self(
|
||||
count: $this->count + 1,
|
||||
lastUpdate: date('H:i:s')
|
||||
);
|
||||
}
|
||||
|
||||
public function reset(): self
|
||||
{
|
||||
return new self(
|
||||
count: 0,
|
||||
lastUpdate: date('H:i:s')
|
||||
);
|
||||
}
|
||||
|
||||
// Serialization
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'count' => $this->count,
|
||||
'lastUpdate' => $this->lastUpdate,
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### State Updates
|
||||
|
||||
Actions return new State instances:
|
||||
|
||||
```php
|
||||
#[Action]
|
||||
public function increment(): CounterState
|
||||
{
|
||||
return $this->state->increment();
|
||||
}
|
||||
|
||||
#[Action]
|
||||
public function addAmount(int $amount): CounterState
|
||||
{
|
||||
return $this->state->addAmount($amount);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Actions & Events
|
||||
|
||||
### Actions
|
||||
|
||||
Actions are public methods marked with `#[Action]`:
|
||||
|
||||
```php
|
||||
#[Action]
|
||||
public function increment(): CounterState
|
||||
{
|
||||
return $this->state->increment();
|
||||
}
|
||||
|
||||
#[Action(rateLimit: 10, idempotencyTTL: 60)]
|
||||
public function expensiveOperation(): CounterState
|
||||
{
|
||||
// Rate limited to 10 calls per minute
|
||||
// Idempotent for 60 seconds
|
||||
return $this->state->performExpensiveOperation();
|
||||
}
|
||||
```
|
||||
|
||||
### Action Parameters
|
||||
|
||||
Actions can accept parameters:
|
||||
|
||||
```php
|
||||
#[Action]
|
||||
public function addAmount(int $amount): CounterState
|
||||
{
|
||||
return $this->state->addAmount($amount);
|
||||
}
|
||||
```
|
||||
|
||||
**Client-side**:
|
||||
```html
|
||||
<button data-live-action="addAmount" data-amount="5">
|
||||
Add 5
|
||||
</button>
|
||||
```
|
||||
|
||||
### Events
|
||||
|
||||
Dispatch events from actions:
|
||||
|
||||
```php
|
||||
#[Action]
|
||||
public function increment(?ComponentEventDispatcher $events = null): CounterState
|
||||
{
|
||||
$newState = $this->state->increment();
|
||||
|
||||
$events?->dispatch('counter:changed', EventPayload::fromArray([
|
||||
'old_value' => $this->state->count,
|
||||
'new_value' => $newState->count,
|
||||
]));
|
||||
|
||||
return $newState;
|
||||
}
|
||||
```
|
||||
|
||||
**Client-side**:
|
||||
```javascript
|
||||
document.addEventListener('livecomponent:event', (e) => {
|
||||
if (e.detail.name === 'counter:changed') {
|
||||
console.log('Counter changed:', e.detail.data);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Caching
|
||||
|
||||
Implement `Cacheable` interface:
|
||||
|
||||
```php
|
||||
#[LiveComponent('user-stats')]
|
||||
final readonly class UserStatsComponent implements LiveComponentContract, Cacheable
|
||||
{
|
||||
public function getCacheKey(): string
|
||||
{
|
||||
return 'user-stats:' . $this->state->userId;
|
||||
}
|
||||
|
||||
public function getCacheTTL(): Duration
|
||||
{
|
||||
return Duration::fromMinutes(5);
|
||||
}
|
||||
|
||||
public function shouldCache(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getCacheTags(): array
|
||||
{
|
||||
return ['user-stats', 'user:' . $this->state->userId];
|
||||
}
|
||||
|
||||
public function getVaryBy(): ?VaryBy
|
||||
{
|
||||
return VaryBy::userId();
|
||||
}
|
||||
|
||||
public function getStaleWhileRevalidate(): ?Duration
|
||||
{
|
||||
return Duration::fromHours(1);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Polling
|
||||
|
||||
Implement `Pollable` interface:
|
||||
|
||||
```php
|
||||
#[LiveComponent('live-feed')]
|
||||
final readonly class LiveFeedComponent implements LiveComponentContract, Pollable
|
||||
{
|
||||
public function poll(): LiveComponentState
|
||||
{
|
||||
// Fetch latest data
|
||||
$newItems = $this->feedService->getLatest();
|
||||
|
||||
return $this->state->withItems($newItems);
|
||||
}
|
||||
|
||||
public function getPollInterval(): int
|
||||
{
|
||||
return 5000; // Poll every 5 seconds
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### File Uploads
|
||||
|
||||
Implement `SupportsFileUpload` interface:
|
||||
|
||||
```php
|
||||
#[LiveComponent('image-uploader')]
|
||||
final readonly class ImageUploaderComponent implements LiveComponentContract, SupportsFileUpload
|
||||
{
|
||||
public function handleUpload(
|
||||
UploadedFile $file,
|
||||
?ComponentEventDispatcher $events = null
|
||||
): LiveComponentState {
|
||||
// Process upload
|
||||
$path = $this->storage->store($file);
|
||||
|
||||
$events?->dispatch('file:uploaded', EventPayload::fromArray([
|
||||
'path' => $path,
|
||||
'size' => $file->getSize(),
|
||||
]));
|
||||
|
||||
return $this->state->withUploadedFile($path);
|
||||
}
|
||||
|
||||
public function validateUpload(UploadedFile $file): array
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
if (!in_array($file->getMimeType(), ['image/jpeg', 'image/png'])) {
|
||||
$errors[] = 'Only JPEG and PNG images are allowed';
|
||||
}
|
||||
|
||||
if ($file->getSize() > 5 * 1024 * 1024) {
|
||||
$errors[] = 'File size must be less than 5MB';
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
public function getAllowedMimeTypes(): array
|
||||
{
|
||||
return ['image/jpeg', 'image/png'];
|
||||
}
|
||||
|
||||
public function getMaxFileSize(): int
|
||||
{
|
||||
return 5 * 1024 * 1024; // 5MB
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Fragments
|
||||
|
||||
Use fragments for partial updates:
|
||||
|
||||
**Template**:
|
||||
```html
|
||||
<div data-lc-fragment="counter-display">
|
||||
<h2>Count: {count}</h2>
|
||||
</div>
|
||||
|
||||
<div data-lc-fragment="actions">
|
||||
<button data-live-action="increment" data-lc-fragments="counter-display">
|
||||
Increment
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Client-side**: Only `counter-display` fragment is updated, not the entire component.
|
||||
|
||||
### Slots
|
||||
|
||||
Implement `SupportsSlots` for flexible component composition:
|
||||
|
||||
```php
|
||||
#[LiveComponent('card')]
|
||||
final readonly class CardComponent implements LiveComponentContract, SupportsSlots
|
||||
{
|
||||
public function getSlotDefinitions(): array
|
||||
{
|
||||
return [
|
||||
SlotDefinition::named('header'),
|
||||
SlotDefinition::default('<p>Default content</p>'),
|
||||
SlotDefinition::named('footer'),
|
||||
];
|
||||
}
|
||||
|
||||
public function getSlotContext(string $slotName): SlotContext
|
||||
{
|
||||
return SlotContext::create([
|
||||
'card_title' => $this->state->title,
|
||||
]);
|
||||
}
|
||||
|
||||
public function processSlotContent(SlotContent $content): SlotContent
|
||||
{
|
||||
return $content; // No processing needed
|
||||
}
|
||||
|
||||
public function validateSlots(array $providedSlots): array
|
||||
{
|
||||
return []; // No validation errors
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```html
|
||||
<x-card id="demo" title="My Card">
|
||||
<slot name="header">
|
||||
<h1>Custom Header</h1>
|
||||
</slot>
|
||||
|
||||
<slot>
|
||||
<p>Main content</p>
|
||||
</slot>
|
||||
</x-card>
|
||||
```
|
||||
|
||||
### Islands
|
||||
|
||||
Use `#[Island]` for isolated rendering:
|
||||
|
||||
```php
|
||||
#[LiveComponent('heavy-widget')]
|
||||
#[Island(isolated: true, lazy: true, placeholder: 'Loading widget...')]
|
||||
final readonly class HeavyWidgetComponent implements LiveComponentContract
|
||||
{
|
||||
// Component implementation
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Caching Strategies
|
||||
|
||||
**Global Cache**:
|
||||
```php
|
||||
public function getVaryBy(): ?VaryBy
|
||||
{
|
||||
return VaryBy::none(); // Same for all users
|
||||
}
|
||||
```
|
||||
|
||||
**Per-User Cache**:
|
||||
```php
|
||||
public function getVaryBy(): ?VaryBy
|
||||
{
|
||||
return VaryBy::userId(); // Different cache per user
|
||||
}
|
||||
```
|
||||
|
||||
**Per-User Per-Locale Cache**:
|
||||
```php
|
||||
public function getVaryBy(): ?VaryBy
|
||||
{
|
||||
return VaryBy::userAndLocale(); // Different cache per user and locale
|
||||
}
|
||||
```
|
||||
|
||||
**With Feature Flags**:
|
||||
```php
|
||||
public function getVaryBy(): ?VaryBy
|
||||
{
|
||||
return VaryBy::userId()
|
||||
->withFeatureFlags(['new-ui', 'dark-mode']);
|
||||
}
|
||||
```
|
||||
|
||||
### Stale-While-Revalidate
|
||||
|
||||
Serve stale content while refreshing:
|
||||
|
||||
```php
|
||||
public function getCacheTTL(): Duration
|
||||
{
|
||||
return Duration::fromMinutes(5); // Fresh for 5 minutes
|
||||
}
|
||||
|
||||
public function getStaleWhileRevalidate(): ?Duration
|
||||
{
|
||||
return Duration::fromHours(1); // Serve stale for 1 hour while refreshing
|
||||
}
|
||||
```
|
||||
|
||||
### Request Batching
|
||||
|
||||
Batch multiple operations:
|
||||
|
||||
```javascript
|
||||
// Client-side
|
||||
const response = await LiveComponent.executeBatch([
|
||||
{
|
||||
componentId: 'counter:demo',
|
||||
method: 'increment',
|
||||
params: { amount: 5 },
|
||||
fragments: ['counter-display']
|
||||
},
|
||||
{
|
||||
componentId: 'stats:user-123',
|
||||
method: 'refresh'
|
||||
}
|
||||
]);
|
||||
```
|
||||
|
||||
### Lazy Loading
|
||||
|
||||
Load components when entering viewport:
|
||||
|
||||
```php
|
||||
// In template
|
||||
{{ lazy_component('notification-center:user-123', [
|
||||
'priority' => 'high',
|
||||
'threshold' => '0.1',
|
||||
'placeholder' => 'Loading notifications...'
|
||||
]) }}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Real-World Examples
|
||||
|
||||
### Example 1: User Profile Component
|
||||
|
||||
```php
|
||||
#[LiveComponent('user-profile')]
|
||||
#[Island(isolated: true, lazy: true)]
|
||||
final readonly class UserProfileComponent implements LiveComponentContract, Cacheable
|
||||
{
|
||||
public function __construct(
|
||||
public ComponentId $id,
|
||||
public UserProfileState $state
|
||||
) {
|
||||
}
|
||||
|
||||
public function getRenderData(): ComponentRenderData
|
||||
{
|
||||
return new ComponentRenderData(
|
||||
templatePath: 'livecomponent-user-profile',
|
||||
data: [
|
||||
'componentId' => $this->id->toString(),
|
||||
'user' => $this->state->user,
|
||||
'stats' => $this->state->stats,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[Action]
|
||||
public function updateProfile(array $data, ?ComponentEventDispatcher $events = null): UserProfileState
|
||||
{
|
||||
$updatedUser = $this->userService->update($this->state->user->id, $data);
|
||||
|
||||
$events?->dispatch('profile:updated', EventPayload::fromArray([
|
||||
'user_id' => $updatedUser->id,
|
||||
]));
|
||||
|
||||
return $this->state->withUser($updatedUser);
|
||||
}
|
||||
|
||||
// Cacheable implementation
|
||||
public function getCacheKey(): string
|
||||
{
|
||||
return 'user-profile:' . $this->state->user->id;
|
||||
}
|
||||
|
||||
public function getCacheTTL(): Duration
|
||||
{
|
||||
return Duration::fromMinutes(10);
|
||||
}
|
||||
|
||||
public function shouldCache(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getCacheTags(): array
|
||||
{
|
||||
return ['user-profile', 'user:' . $this->state->user->id];
|
||||
}
|
||||
|
||||
public function getVaryBy(): ?VaryBy
|
||||
{
|
||||
return VaryBy::userId();
|
||||
}
|
||||
|
||||
public function getStaleWhileRevalidate(): ?Duration
|
||||
{
|
||||
return null; // No SWR for user profiles
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example 2: Search Component with Debouncing
|
||||
|
||||
```php
|
||||
#[LiveComponent('search')]
|
||||
final readonly class SearchComponent implements LiveComponentContract
|
||||
{
|
||||
#[Action(rateLimit: 20)] // Allow 20 searches per minute
|
||||
public function search(string $query, ?ComponentEventDispatcher $events = null): SearchState
|
||||
{
|
||||
if (strlen($query) < 3) {
|
||||
return $this->state->withResults([]);
|
||||
}
|
||||
|
||||
$results = $this->searchService->search($query);
|
||||
|
||||
$events?->dispatch('search:completed', EventPayload::fromArray([
|
||||
'query' => $query,
|
||||
'result_count' => count($results),
|
||||
]));
|
||||
|
||||
return $this->state->withQuery($query)->withResults($results);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Client-side debouncing**:
|
||||
```javascript
|
||||
let searchTimeout;
|
||||
const searchInput = document.querySelector('[data-live-action="search"]');
|
||||
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
LiveComponent.executeAction('search:demo', 'search', {
|
||||
query: e.target.value
|
||||
});
|
||||
}, 300); // 300ms debounce
|
||||
});
|
||||
```
|
||||
|
||||
### Example 3: Dashboard with Multiple Components
|
||||
|
||||
```php
|
||||
// Dashboard page template
|
||||
<div class="dashboard">
|
||||
<!-- Heavy widget - lazy loaded -->
|
||||
<x-analytics-dashboard id="main" />
|
||||
|
||||
<!-- User stats - cached -->
|
||||
<x-user-stats id="current-user" />
|
||||
|
||||
<!-- Live feed - polled -->
|
||||
<x-live-feed id="feed" />
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Using LiveComponentTestCase
|
||||
|
||||
```php
|
||||
class CounterComponentTest extends LiveComponentTestCase
|
||||
{
|
||||
public function test_increments_counter(): void
|
||||
{
|
||||
$this->mount('counter:test', ['count' => 5])
|
||||
->call('increment')
|
||||
->seeStateKey('count', 6)
|
||||
->seeHtmlHas('Count: 6');
|
||||
}
|
||||
|
||||
public function test_resets_counter(): void
|
||||
{
|
||||
$this->mount('counter:test', ['count' => 10])
|
||||
->call('reset')
|
||||
->seeStateKey('count', 0)
|
||||
->seeHtmlHas('Count: 0');
|
||||
}
|
||||
|
||||
public function test_dispatches_event_on_increment(): void
|
||||
{
|
||||
$this->mount('counter:test', ['count' => 5])
|
||||
->call('increment')
|
||||
->seeEventDispatched('counter:changed', [
|
||||
'old_value' => 5,
|
||||
'new_value' => 6,
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Snapshot Testing
|
||||
|
||||
```php
|
||||
public function test_renders_counter_correctly(): void
|
||||
{
|
||||
$component = mountComponent('counter:test', ['count' => 5]);
|
||||
|
||||
expect($component['html'])->toMatchSnapshot('counter-initial-state');
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Immutable State
|
||||
|
||||
Always return new State instances:
|
||||
|
||||
```php
|
||||
// ✅ CORRECT
|
||||
public function increment(): CounterState
|
||||
{
|
||||
return new CounterState(count: $this->state->count + 1);
|
||||
}
|
||||
|
||||
// ❌ WRONG (won't work with readonly)
|
||||
public function increment(): void
|
||||
{
|
||||
$this->state->count++; // Error: Cannot modify readonly property
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Type-Safe State
|
||||
|
||||
Use Value Objects instead of arrays:
|
||||
|
||||
```php
|
||||
// ✅ CORRECT
|
||||
public function __construct(
|
||||
public CounterState $state
|
||||
) {}
|
||||
|
||||
// ❌ WRONG
|
||||
public function __construct(
|
||||
public array $state
|
||||
) {}
|
||||
```
|
||||
|
||||
### 3. Action Validation
|
||||
|
||||
Validate inputs in actions:
|
||||
|
||||
```php
|
||||
#[Action]
|
||||
public function addAmount(int $amount): CounterState
|
||||
{
|
||||
if ($amount < 0) {
|
||||
throw new \InvalidArgumentException('Amount must be positive');
|
||||
}
|
||||
|
||||
return $this->state->addAmount($amount);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Error Handling
|
||||
|
||||
Handle errors gracefully:
|
||||
|
||||
```php
|
||||
#[Action]
|
||||
public function performOperation(): CounterState
|
||||
{
|
||||
try {
|
||||
return $this->state->performOperation();
|
||||
} catch (\Exception $e) {
|
||||
// Log error
|
||||
error_log("Operation failed: " . $e->getMessage());
|
||||
|
||||
// Return unchanged state or error state
|
||||
return $this->state->withError($e->getMessage());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Performance
|
||||
|
||||
- Use caching for expensive operations
|
||||
- Use fragments for partial updates
|
||||
- Use lazy loading for below-the-fold content
|
||||
- Use Islands for heavy components
|
||||
- Batch multiple operations
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Security Guide](security-guide-complete.md) - CSRF, Rate Limiting, Authorization
|
||||
- [Performance Guide](performance-guide-complete.md) - Caching, Batching, Optimization
|
||||
- [Upload Guide](upload-guide-complete.md) - File Uploads, Validation, Chunking
|
||||
- [API Reference](api-reference-complete.md) - Complete API documentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,521 @@
|
||||
# Implementation Plan: Testing Infrastructure, Documentation & Middleware Support
|
||||
|
||||
## Übersicht
|
||||
|
||||
Dieser Plan umfasst drei große Bereiche:
|
||||
1. **Test Infrastructure** - Verbesserte Test-Harness und Testing-Tools
|
||||
2. **Documentation** - Vollständige Dokumentation für alle LiveComponents Features
|
||||
3. **Middleware Support** - Component-Level Middleware für flexible Request/Response Transformation
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Test Infrastructure Verbesserung
|
||||
|
||||
### 1.1 LiveComponentTestCase Base Class
|
||||
|
||||
**Ziel**: Zentrale Test-Base-Class mit allen Helpers für einfache Component-Tests
|
||||
|
||||
**Datei**: `tests/Feature/Framework/LiveComponents/TestHarness/LiveComponentTestCase.php`
|
||||
|
||||
**Features**:
|
||||
- `mount(string $componentId, array $initialData = [])` - Component mounten
|
||||
- `call(string $action, array $params = [])` - Action aufrufen
|
||||
- `seeHtmlHas(string $needle)` - HTML-Assertion
|
||||
- `seeStateEquals(array $expectedState)` - State-Assertion
|
||||
- `seeStateKey(string $key, mixed $value)` - Einzelne State-Key-Assertion
|
||||
- `seeEventDispatched(string $eventName, ?array $payload = null)` - Event-Assertion
|
||||
- `seeFragment(string $fragmentName, string $content)` - Fragment-Assertion
|
||||
- `assertComponent(LiveComponentContract $component)` - Component-Instance-Assertion
|
||||
|
||||
**Architektur**:
|
||||
```php
|
||||
abstract class LiveComponentTestCase extends TestCase
|
||||
{
|
||||
protected ComponentRegistry $registry;
|
||||
protected LiveComponentHandler $handler;
|
||||
protected LiveComponentRenderer $renderer;
|
||||
protected ?LiveComponentContract $currentComponent = null;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->registry = container()->get(ComponentRegistry::class);
|
||||
$this->handler = container()->get(LiveComponentHandler::class);
|
||||
$this->renderer = container()->get(LiveComponentRenderer::class);
|
||||
}
|
||||
|
||||
protected function mount(string $componentId, array $initialData = []): self
|
||||
{
|
||||
$this->currentComponent = $this->registry->resolve(
|
||||
ComponentId::fromString($componentId),
|
||||
$initialData
|
||||
);
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function call(string $action, array $params = []): self
|
||||
{
|
||||
// Action ausführen und Component aktualisieren
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function seeHtmlHas(string $needle): self
|
||||
{
|
||||
$html = $this->renderer->render($this->currentComponent);
|
||||
expect($html)->toContain($needle);
|
||||
return $this;
|
||||
}
|
||||
|
||||
// ... weitere Helper-Methoden
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 Snapshot Testing
|
||||
|
||||
**Ziel**: Render-Ausgabe als Snapshots speichern und vergleichen
|
||||
|
||||
**Datei**: `tests/Feature/Framework/LiveComponents/TestHarness/ComponentSnapshotTest.php`
|
||||
|
||||
**Features**:
|
||||
- Whitespace-normalisierte Snapshots
|
||||
- Automatische Snapshot-Generierung
|
||||
- Snapshot-Vergleich mit Diff-Output
|
||||
- Snapshot-Update-Modus für Refactoring
|
||||
|
||||
**Usage**:
|
||||
```php
|
||||
it('renders counter component correctly', function () {
|
||||
$component = mountComponent('counter:test', ['count' => 5]);
|
||||
|
||||
expect($component['html'])->toMatchSnapshot('counter-initial-state');
|
||||
});
|
||||
```
|
||||
|
||||
### 1.3 Contract Compliance Tests
|
||||
|
||||
**Ziel**: Automatische Tests für Interface-Compliance
|
||||
|
||||
**Datei**: `tests/Feature/Framework/LiveComponents/ContractComplianceTest.php`
|
||||
|
||||
**Tests**:
|
||||
- `Pollable` Interface Compliance
|
||||
- `Cacheable` Interface Compliance
|
||||
- `Uploadable` Interface Compliance
|
||||
- `LifecycleAware` Interface Compliance
|
||||
- `SupportsSlots` Interface Compliance
|
||||
|
||||
**Features**:
|
||||
- Reflection-basierte Interface-Checks
|
||||
- Method-Signature-Validierung
|
||||
- Return-Type-Validierung
|
||||
- Required-Method-Checks
|
||||
|
||||
### 1.4 Security Tests
|
||||
|
||||
**Ziel**: Umfassende Security-Tests für alle Security-Features
|
||||
|
||||
**Datei**: `tests/Feature/Framework/LiveComponents/SecurityTest.php`
|
||||
|
||||
**Tests**:
|
||||
- CSRF Protection Tests
|
||||
- Rate Limiting Tests
|
||||
- Idempotency Tests
|
||||
- Action Allow-List Tests
|
||||
- Authorization Tests (`#[RequiresPermission]`)
|
||||
|
||||
**Features**:
|
||||
- Test für alle Security-Patterns
|
||||
- Edge-Case-Tests (Token-Manipulation, etc.)
|
||||
- Integration-Tests mit echten Requests
|
||||
|
||||
### 1.5 E2E Tests
|
||||
|
||||
**Ziel**: End-to-End Tests für komplexe Szenarien
|
||||
|
||||
**Datei**: `tests/e2e/livecomponents/`
|
||||
|
||||
**Tests**:
|
||||
- Partial Rendering E2E
|
||||
- Batch Operations E2E
|
||||
- SSE Reconnect E2E
|
||||
- Upload Chunking E2E
|
||||
- Island Loading E2E
|
||||
- Lazy Loading E2E
|
||||
|
||||
**Tools**:
|
||||
- Playwright für Browser-Tests
|
||||
- Real HTTP Requests
|
||||
- Component-Interaction-Tests
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Documentation Vervollständigung
|
||||
|
||||
### 2.1 End-to-End Guide
|
||||
|
||||
**Datei**: `docs/livecomponents/end-to-end-guide.md`
|
||||
|
||||
**Inhalte**:
|
||||
- Component erstellen (von Grund auf)
|
||||
- RenderData definieren
|
||||
- Actions implementieren
|
||||
- Events dispatchen
|
||||
- State Management
|
||||
- Caching konfigurieren
|
||||
- Fragments verwenden
|
||||
- Slots nutzen
|
||||
- Lifecycle Hooks
|
||||
- Best Practices
|
||||
|
||||
**Struktur**:
|
||||
1. Quick Start (5 Minuten)
|
||||
2. Component Basics
|
||||
3. State Management
|
||||
4. Actions & Events
|
||||
5. Advanced Features
|
||||
6. Performance Optimization
|
||||
7. Real-World Examples
|
||||
|
||||
### 2.2 Security Guide
|
||||
|
||||
**Datei**: `docs/livecomponents/security-guide-complete.md`
|
||||
|
||||
**Inhalte**:
|
||||
- CSRF Protection (wie es funktioniert, Best Practices)
|
||||
- Rate Limiting (Konfiguration, Custom Limits)
|
||||
- Idempotency (Use Cases, Implementation)
|
||||
- Action Allow-List (Security-Pattern)
|
||||
- Authorization (`#[RequiresPermission]`)
|
||||
- Input Validation
|
||||
- XSS Prevention
|
||||
- Secure State Management
|
||||
|
||||
**Beispiele**:
|
||||
- Secure Component Patterns
|
||||
- Common Security Pitfalls
|
||||
- Security Checklist
|
||||
|
||||
### 2.3 Performance Guide
|
||||
|
||||
**Datei**: `docs/livecomponents/performance-guide-complete.md`
|
||||
|
||||
**Inhalte**:
|
||||
- Caching Strategies (varyBy, SWR)
|
||||
- Request Batching
|
||||
- Debounce/Throttle Patterns
|
||||
- Lazy Loading Best Practices
|
||||
- Island Components für Performance
|
||||
- Fragment Updates
|
||||
- SSE Optimization
|
||||
- Memory Management
|
||||
|
||||
**Beispiele**:
|
||||
- Performance-Optimierte Components
|
||||
- Benchmarking-Strategien
|
||||
- Performance Checklist
|
||||
|
||||
### 2.4 Upload Guide
|
||||
|
||||
**Datei**: `docs/livecomponents/upload-guide-complete.md`
|
||||
|
||||
**Inhalte**:
|
||||
- File Upload Basics
|
||||
- Validation Patterns
|
||||
- Chunked Uploads
|
||||
- Progress Tracking
|
||||
- Quarantine System
|
||||
- Virus Scanning Integration
|
||||
- Error Handling
|
||||
- Security Considerations
|
||||
|
||||
**Beispiele**:
|
||||
- Image Upload Component
|
||||
- Large File Upload
|
||||
- Multi-File Upload
|
||||
|
||||
### 2.5 DevTools/Debugging Guide
|
||||
|
||||
**Datei**: `docs/livecomponents/devtools-debugging-guide.md`
|
||||
|
||||
**Inhalte**:
|
||||
- DevTools Overlay verwenden
|
||||
- Component Inspector
|
||||
- Action Logging
|
||||
- Event Monitoring
|
||||
- Network Timeline
|
||||
- Performance Profiling
|
||||
- Debugging-Tipps
|
||||
- Common Issues & Solutions
|
||||
|
||||
### 2.6 API Reference
|
||||
|
||||
**Datei**: `docs/livecomponents/api-reference-complete.md`
|
||||
|
||||
**Inhalte**:
|
||||
- Alle Contracts (LiveComponentContract, Pollable, Cacheable, etc.)
|
||||
- Alle Attributes (`#[LiveComponent]`, `#[Action]`, `#[Island]`, etc.)
|
||||
- Alle Controller-Endpoints
|
||||
- Alle Value Objects
|
||||
- JavaScript API
|
||||
- Events API
|
||||
|
||||
**Format**:
|
||||
- Vollständige Method-Signatures
|
||||
- Parameter-Beschreibungen
|
||||
- Return-Types
|
||||
- Examples für jede Methode
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Middleware Support
|
||||
|
||||
### 3.1 Component Middleware Interface
|
||||
|
||||
**Ziel**: Interface für Component-Level Middleware
|
||||
|
||||
**Datei**: `src/Framework/LiveComponents/Middleware/ComponentMiddlewareInterface.php`
|
||||
|
||||
**Interface**:
|
||||
```php
|
||||
interface ComponentMiddlewareInterface
|
||||
{
|
||||
public function handle(
|
||||
LiveComponentContract $component,
|
||||
string $action,
|
||||
ActionParameters $params,
|
||||
callable $next
|
||||
): ComponentUpdate;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 Middleware Attribute
|
||||
|
||||
**Ziel**: Attribute zum Registrieren von Middleware auf Component-Level
|
||||
|
||||
**Datei**: `src/Framework/LiveComponents/Attributes/Middleware.php`
|
||||
|
||||
**Usage**:
|
||||
```php
|
||||
#[LiveComponent('user-profile')]
|
||||
#[Middleware(LoggingMiddleware::class)]
|
||||
#[Middleware(CachingMiddleware::class, priority: 10)]
|
||||
final readonly class UserProfileComponent implements LiveComponentContract
|
||||
{
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 Built-in Middleware
|
||||
|
||||
**Ziel**: Standard-Middleware für häufige Use Cases
|
||||
|
||||
**Middleware**:
|
||||
1. **LoggingMiddleware** - Action-Logging
|
||||
2. **CachingMiddleware** - Response-Caching
|
||||
3. **RateLimitMiddleware** - Component-Level Rate Limiting
|
||||
4. **ValidationMiddleware** - Input-Validation
|
||||
5. **TransformMiddleware** - Request/Response Transformation
|
||||
|
||||
**Dateien**:
|
||||
- `src/Framework/LiveComponents/Middleware/LoggingMiddleware.php`
|
||||
- `src/Framework/LiveComponents/Middleware/CachingMiddleware.php`
|
||||
- `src/Framework/LiveComponents/Middleware/RateLimitMiddleware.php`
|
||||
- `src/Framework/LiveComponents/Middleware/ValidationMiddleware.php`
|
||||
- `src/Framework/LiveComponents/Middleware/TransformMiddleware.php`
|
||||
|
||||
### 3.4 Middleware Pipeline
|
||||
|
||||
**Ziel**: Middleware-Pipeline für Component Actions
|
||||
|
||||
**Datei**: `src/Framework/LiveComponents/Middleware/ComponentMiddlewarePipeline.php`
|
||||
|
||||
**Features**:
|
||||
- Middleware-Stack-Verwaltung
|
||||
- Priority-basierte Ausführung
|
||||
- Request/Response Transformation
|
||||
- Error Handling in Middleware
|
||||
|
||||
**Architektur**:
|
||||
```php
|
||||
final readonly class ComponentMiddlewarePipeline
|
||||
{
|
||||
public function __construct(
|
||||
private array $middlewares
|
||||
) {}
|
||||
|
||||
public function process(
|
||||
LiveComponentContract $component,
|
||||
string $action,
|
||||
ActionParameters $params
|
||||
): ComponentUpdate {
|
||||
// Execute middleware stack
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.5 Middleware Integration
|
||||
|
||||
**Ziel**: Integration in LiveComponentHandler
|
||||
|
||||
**Datei**: `src/Framework/LiveComponents/LiveComponentHandler.php`
|
||||
|
||||
**Änderungen**:
|
||||
- Middleware-Discovery aus Component-Attributes
|
||||
- Middleware-Pipeline vor Action-Execution
|
||||
- Middleware-Configuration aus Metadata
|
||||
|
||||
### 3.6 Middleware Tests
|
||||
|
||||
**Ziel**: Umfassende Tests für Middleware-System
|
||||
|
||||
**Dateien**:
|
||||
- `tests/Unit/Framework/LiveComponents/Middleware/ComponentMiddlewarePipelineTest.php`
|
||||
- `tests/Unit/Framework/LiveComponents/Middleware/LoggingMiddlewareTest.php`
|
||||
- `tests/Unit/Framework/LiveComponents/Middleware/CachingMiddlewareTest.php`
|
||||
- `tests/Feature/Framework/LiveComponents/MiddlewareIntegrationTest.php`
|
||||
|
||||
---
|
||||
|
||||
## Implementierungsreihenfolge
|
||||
|
||||
### Sprint 1: Test Infrastructure (Woche 1-2)
|
||||
1. LiveComponentTestCase Base Class
|
||||
2. Snapshot Testing
|
||||
3. Contract Compliance Tests
|
||||
4. Security Tests (Basis)
|
||||
|
||||
### Sprint 2: Documentation Phase 1 (Woche 3)
|
||||
1. End-to-End Guide
|
||||
2. Security Guide
|
||||
3. Performance Guide
|
||||
|
||||
### Sprint 3: Middleware Support (Woche 4-5)
|
||||
1. ComponentMiddlewareInterface
|
||||
2. Middleware Attribute
|
||||
3. Middleware Pipeline
|
||||
4. Built-in Middleware (Logging, Caching)
|
||||
|
||||
### Sprint 4: Documentation Phase 2 & Testing Completion (Woche 6)
|
||||
1. Upload Guide
|
||||
2. DevTools Guide
|
||||
3. API Reference
|
||||
4. E2E Tests
|
||||
5. Security Tests (Vollständig)
|
||||
|
||||
---
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
### Test Infrastructure
|
||||
- [ ] LiveComponentTestCase mit allen Helpers implementiert
|
||||
- [ ] Snapshot Testing funktional
|
||||
- [ ] Contract Compliance Tests für alle Interfaces
|
||||
- [ ] Security Tests für alle Security-Features
|
||||
- [ ] E2E Tests für komplexe Szenarien
|
||||
- [ ] Alle Tests dokumentiert mit Examples
|
||||
|
||||
### Documentation
|
||||
- [ ] End-to-End Guide vollständig
|
||||
- [ ] Security Guide vollständig
|
||||
- [ ] Performance Guide vollständig
|
||||
- [ ] Upload Guide vollständig
|
||||
- [ ] DevTools Guide vollständig
|
||||
- [ ] API Reference vollständig
|
||||
- [ ] Alle Guides mit praktischen Examples
|
||||
|
||||
### Middleware Support
|
||||
- [ ] ComponentMiddlewareInterface definiert
|
||||
- [ ] Middleware Attribute implementiert
|
||||
- [ ] Middleware Pipeline funktional
|
||||
- [ ] Built-in Middleware implementiert (Logging, Caching, RateLimit)
|
||||
- [ ] Integration in LiveComponentHandler
|
||||
- [ ] Umfassende Tests
|
||||
- [ ] Dokumentation mit Examples
|
||||
|
||||
---
|
||||
|
||||
## Technische Details
|
||||
|
||||
### Test Infrastructure
|
||||
|
||||
**Pest Integration**:
|
||||
- Custom Expectations erweitern
|
||||
- Helper-Functions in `tests/Pest.php`
|
||||
- Test-Harness als Trait für Wiederverwendung
|
||||
|
||||
**Snapshot Format**:
|
||||
- JSON-basiert für strukturierte Daten
|
||||
- HTML-Snapshots mit Whitespace-Normalisierung
|
||||
- Versionierte Snapshots für Breaking Changes
|
||||
|
||||
### Middleware Architecture
|
||||
|
||||
**Execution Order**:
|
||||
1. Request Transformation (TransformMiddleware)
|
||||
2. Validation (ValidationMiddleware)
|
||||
3. Rate Limiting (RateLimitMiddleware)
|
||||
4. Caching (CachingMiddleware)
|
||||
5. Logging (LoggingMiddleware)
|
||||
6. Action Execution
|
||||
7. Response Transformation
|
||||
|
||||
**Priority System**:
|
||||
- Higher priority = earlier execution
|
||||
- Default priority: 100
|
||||
- Built-in middleware: 50-150 range
|
||||
|
||||
### Documentation Structure
|
||||
|
||||
**Markdown Format**:
|
||||
- Code-Beispiele mit Syntax-Highlighting
|
||||
- TOC für Navigation
|
||||
- Cross-References zwischen Guides
|
||||
- Version-Info für Breaking Changes
|
||||
|
||||
---
|
||||
|
||||
## Erfolgsmetriken
|
||||
|
||||
### Test Infrastructure
|
||||
- 90%+ Test Coverage für alle Core-Features
|
||||
- Alle Security-Features getestet
|
||||
- E2E Tests für kritische Flows
|
||||
|
||||
### Documentation
|
||||
- Alle Guides vollständig
|
||||
- Praktische Examples für alle Features
|
||||
- Positive Developer Feedback
|
||||
|
||||
### Middleware Support
|
||||
- Middleware-System funktional
|
||||
- Built-in Middleware getestet
|
||||
- Developer können Custom Middleware erstellen
|
||||
|
||||
---
|
||||
|
||||
## Risiken & Mitigation
|
||||
|
||||
### Test Infrastructure
|
||||
- **Risiko**: Snapshot-Tests können bei Refactoring störend sein
|
||||
- **Mitigation**: Update-Modus für einfaches Refactoring
|
||||
|
||||
### Documentation
|
||||
- **Risiko**: Dokumentation kann schnell veralten
|
||||
- **Mitigation**: Automatische Tests für Code-Examples, regelmäßige Reviews
|
||||
|
||||
### Middleware Support
|
||||
- **Risiko**: Performance-Impact durch Middleware-Stack
|
||||
- **Mitigation**: Caching, Lazy Loading, Performance-Tests
|
||||
|
||||
---
|
||||
|
||||
## Nächste Schritte
|
||||
|
||||
1. **Sprint Planning**: Detaillierte Task-Aufteilung
|
||||
2. **Implementation Start**: Mit Test Infrastructure beginnen
|
||||
3. **Documentation**: Parallel zur Implementation
|
||||
4. **Middleware**: Nach Test Infrastructure
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
308
docs/livecomponents/island-directive.md
Normal file
308
docs/livecomponents/island-directive.md
Normal file
@@ -0,0 +1,308 @@
|
||||
# Island Directive
|
||||
|
||||
**Isolated Component Rendering** - Render heavy components separately for better performance.
|
||||
|
||||
## Overview
|
||||
|
||||
The `#[Island]` directive enables isolated rendering of LiveComponents, allowing them to be rendered separately from the main template flow. This is particularly useful for resource-intensive components that can slow down page rendering.
|
||||
|
||||
## Features
|
||||
|
||||
- **Isolated Rendering**: Components are rendered separately without template wrapper
|
||||
- **Lazy Loading**: Optional lazy loading when component enters viewport
|
||||
- **Independent Caching**: Islands can have their own caching strategies
|
||||
- **Isolated Events**: Islands don't receive parent component events
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Simple Island Component
|
||||
|
||||
```php
|
||||
use App\Framework\LiveComponents\Attributes\LiveComponent;
|
||||
use App\Framework\LiveComponents\Attributes\Island;
|
||||
|
||||
#[LiveComponent('heavy-widget')]
|
||||
#[Island]
|
||||
final readonly class HeavyWidgetComponent implements LiveComponentContract
|
||||
{
|
||||
// Component implementation
|
||||
}
|
||||
```
|
||||
|
||||
### Lazy Island Component
|
||||
|
||||
```php
|
||||
#[LiveComponent('metrics-dashboard')]
|
||||
#[Island(isolated: true, lazy: true, placeholder: 'Loading dashboard...')]
|
||||
final readonly class MetricsDashboardComponent implements LiveComponentContract
|
||||
{
|
||||
// Component implementation
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### `isolated` (bool, default: `true`)
|
||||
|
||||
Whether the component should be rendered in isolation. When `true`, the component is rendered via the `/island` endpoint without template wrapper.
|
||||
|
||||
```php
|
||||
#[Island(isolated: true)] // Isolated rendering (default)
|
||||
#[Island(isolated: false)] // Normal rendering (not recommended)
|
||||
```
|
||||
|
||||
### `lazy` (bool, default: `false`)
|
||||
|
||||
Whether the component should be lazy-loaded when entering the viewport. When `true`, a placeholder is generated and the component is loaded via IntersectionObserver.
|
||||
|
||||
```php
|
||||
#[Island(lazy: true)] // Lazy load on viewport entry
|
||||
#[Island(lazy: false)] // Load immediately (default)
|
||||
```
|
||||
|
||||
### `placeholder` (string|null, default: `null`)
|
||||
|
||||
Custom placeholder text shown while the component is loading (only used when `lazy: true`).
|
||||
|
||||
```php
|
||||
#[Island(lazy: true, placeholder: 'Loading widget...')]
|
||||
```
|
||||
|
||||
## Template Usage
|
||||
|
||||
### In Templates
|
||||
|
||||
Island components are used the same way as regular LiveComponents:
|
||||
|
||||
```html
|
||||
<x-heavy-widget id="user-123" />
|
||||
```
|
||||
|
||||
When processed, lazy Islands generate a placeholder:
|
||||
|
||||
```html
|
||||
<div data-island-component="true"
|
||||
data-live-component-lazy="heavy-widget:user-123"
|
||||
data-lazy-priority="normal"
|
||||
data-lazy-threshold="0.1">
|
||||
<div class="island-placeholder">Loading widget...</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Island vs. Lazy Component
|
||||
|
||||
### Lazy Component (`data-live-component-lazy`)
|
||||
|
||||
- Loaded when entering viewport
|
||||
- Part of normal template flow
|
||||
- Shares state/events with parent components
|
||||
- Uses `/lazy-load` endpoint
|
||||
|
||||
### Island Component (`data-island-component`)
|
||||
|
||||
- Rendered in isolation (separate request)
|
||||
- No template wrapper (no layout/meta)
|
||||
- Isolated event context (no parent events)
|
||||
- Optional lazy loading on viewport entry
|
||||
- Uses `/island` endpoint
|
||||
|
||||
## Performance Benefits
|
||||
|
||||
### Isolation
|
||||
|
||||
Islands reduce template processing overhead for heavy components by rendering them separately. This means:
|
||||
|
||||
- Heavy components don't block main page rendering
|
||||
- Independent error handling (one island failure doesn't affect others)
|
||||
- Separate caching strategies per island
|
||||
|
||||
### Lazy Loading
|
||||
|
||||
When combined with lazy loading, Islands provide:
|
||||
|
||||
- Reduced initial page load time
|
||||
- Faster Time to Interactive (TTI)
|
||||
- Better Core Web Vitals scores
|
||||
|
||||
### Example Performance Impact
|
||||
|
||||
```php
|
||||
// Without Island: 500ms page load (heavy component blocks rendering)
|
||||
// With Island: 200ms page load + 300ms island load (non-blocking)
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Heavy Widgets
|
||||
|
||||
```php
|
||||
#[LiveComponent('analytics-dashboard')]
|
||||
#[Island(isolated: true, lazy: true, placeholder: 'Loading analytics...')]
|
||||
final readonly class AnalyticsDashboardComponent implements LiveComponentContract
|
||||
{
|
||||
// Heavy component with complex data processing
|
||||
}
|
||||
```
|
||||
|
||||
### Third-Party Integrations
|
||||
|
||||
```php
|
||||
#[LiveComponent('external-map')]
|
||||
#[Island(isolated: true, lazy: true)]
|
||||
final readonly class ExternalMapComponent implements LiveComponentContract
|
||||
{
|
||||
// Component that loads external resources
|
||||
}
|
||||
```
|
||||
|
||||
### Conditional Components
|
||||
|
||||
```php
|
||||
#[LiveComponent('admin-panel')]
|
||||
#[Island(isolated: true)]
|
||||
final readonly class AdminPanelComponent implements LiveComponentContract
|
||||
{
|
||||
// Component only visible to admins
|
||||
// Isolated to prevent template processing for non-admin users
|
||||
}
|
||||
```
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Endpoint
|
||||
|
||||
Islands are rendered via:
|
||||
|
||||
```
|
||||
GET /live-component/{id}/island
|
||||
```
|
||||
|
||||
This endpoint:
|
||||
- Renders component HTML without template wrapper
|
||||
- Returns JSON with `html`, `state`, `csrf_token`
|
||||
- Uses `ComponentRegistry.render()` directly (no wrapper)
|
||||
|
||||
### Frontend Integration
|
||||
|
||||
Islands are automatically detected and handled by `LazyComponentLoader`:
|
||||
|
||||
- Lazy islands: Loaded when entering viewport
|
||||
- Non-lazy islands: Loaded immediately
|
||||
- Isolated initialization: Islands don't receive parent events
|
||||
|
||||
### Event Isolation
|
||||
|
||||
Islands have isolated event contexts:
|
||||
|
||||
- No parent component events
|
||||
- No shared state with parent components
|
||||
- Independent SSE channels (if configured)
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use Islands for Heavy Components**: Only use `#[Island]` for components that significantly impact page load time
|
||||
|
||||
2. **Combine with Lazy Loading**: Use `lazy: true` for below-the-fold content
|
||||
|
||||
3. **Provide Placeholders**: Always provide meaningful placeholder text for better UX
|
||||
|
||||
4. **Test Isolation**: Verify that islands work correctly in isolation
|
||||
|
||||
5. **Monitor Performance**: Track island load times and optimize as needed
|
||||
|
||||
## Examples
|
||||
|
||||
### Complete Example
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\LiveComponents\Dashboard;
|
||||
|
||||
use App\Framework\LiveComponents\Attributes\LiveComponent;
|
||||
use App\Framework\LiveComponents\Attributes\Island;
|
||||
use App\Framework\LiveComponents\Contracts\LiveComponentContract;
|
||||
use App\Framework\LiveComponents\ValueObjects\ComponentId;
|
||||
use App\Framework\LiveComponents\ValueObjects\ComponentRenderData;
|
||||
use App\Framework\LiveComponents\ValueObjects\ComponentState;
|
||||
|
||||
#[LiveComponent('metrics-dashboard')]
|
||||
#[Island(isolated: true, lazy: true, placeholder: 'Loading metrics...')]
|
||||
final readonly class MetricsDashboardComponent implements LiveComponentContract
|
||||
{
|
||||
public function __construct(
|
||||
public ComponentId $id,
|
||||
public ComponentState $state
|
||||
) {
|
||||
}
|
||||
|
||||
public function getRenderData(): ComponentRenderData
|
||||
{
|
||||
return new ComponentRenderData(
|
||||
templatePath: 'livecomponent-metrics-dashboard',
|
||||
data: [
|
||||
'componentId' => $this->id->toString(),
|
||||
'metrics' => $this->loadMetrics(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function loadMetrics(): array
|
||||
{
|
||||
// Heavy operation - isolated rendering prevents blocking
|
||||
return [
|
||||
'users' => 12345,
|
||||
'orders' => 6789,
|
||||
'revenue' => 123456.78,
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### Converting Regular Component to Island
|
||||
|
||||
1. Add `#[Island]` attribute to component class
|
||||
2. Test component rendering (should work identically)
|
||||
3. Optionally enable lazy loading with `lazy: true`
|
||||
4. Monitor performance improvements
|
||||
|
||||
### Backward Compatibility
|
||||
|
||||
- Components without `#[Island]` work exactly as before
|
||||
- `#[Island]` is opt-in (no breaking changes)
|
||||
- Existing lazy components continue to work
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Island Not Loading
|
||||
|
||||
- Check browser console for errors
|
||||
- Verify component is registered in ComponentRegistry
|
||||
- Ensure `/island` endpoint is accessible
|
||||
|
||||
### Placeholder Not Showing
|
||||
|
||||
- Verify `lazy: true` is set
|
||||
- Check `placeholder` parameter is provided
|
||||
- Inspect generated HTML for `data-island-component` attribute
|
||||
|
||||
### Events Not Working
|
||||
|
||||
- Islands have isolated event contexts
|
||||
- Use component-specific events, not parent events
|
||||
- Check SSE channel configuration if using real-time updates
|
||||
|
||||
## See Also
|
||||
|
||||
- [Lazy Loading Guide](livecomponents-lazy-loading.md)
|
||||
- [Component Caching](livecomponents-caching.md)
|
||||
- [Performance Optimization](livecomponents-performance.md)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
697
docs/livecomponents/performance-guide-complete.md
Normal file
697
docs/livecomponents/performance-guide-complete.md
Normal file
@@ -0,0 +1,697 @@
|
||||
# LiveComponents Performance Guide
|
||||
|
||||
**Complete performance optimization guide for LiveComponents covering caching, batching, middleware, and optimization techniques.**
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Caching Strategies](#caching-strategies)
|
||||
2. [Request Batching](#request-batching)
|
||||
3. [Middleware Performance](#middleware-performance)
|
||||
4. [Debounce & Throttle](#debounce--throttle)
|
||||
5. [Lazy Loading](#lazy-loading)
|
||||
6. [Island Components](#island-components)
|
||||
7. [Fragment Updates](#fragment-updates)
|
||||
8. [SSE Optimization](#sse-optimization)
|
||||
9. [Memory Management](#memory-management)
|
||||
10. [Performance Checklist](#performance-checklist)
|
||||
|
||||
---
|
||||
|
||||
## Caching Strategies
|
||||
|
||||
### Component-Level Caching
|
||||
|
||||
Implement `Cacheable` interface for automatic caching:
|
||||
|
||||
```php
|
||||
#[LiveComponent('user-stats')]
|
||||
final readonly class UserStatsComponent implements LiveComponentContract, Cacheable
|
||||
{
|
||||
public function getCacheKey(): string
|
||||
{
|
||||
return 'user-stats:' . $this->state->userId;
|
||||
}
|
||||
|
||||
public function getCacheTTL(): Duration
|
||||
{
|
||||
return Duration::fromMinutes(5);
|
||||
}
|
||||
|
||||
public function shouldCache(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getCacheTags(): array
|
||||
{
|
||||
return ['user-stats', 'user:' . $this->state->userId];
|
||||
}
|
||||
|
||||
public function getVaryBy(): ?VaryBy
|
||||
{
|
||||
return VaryBy::userId();
|
||||
}
|
||||
|
||||
public function getStaleWhileRevalidate(): ?Duration
|
||||
{
|
||||
return Duration::fromHours(1);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Cache Key Variations
|
||||
|
||||
**Global Cache** (same for all users):
|
||||
```php
|
||||
public function getVaryBy(): ?VaryBy
|
||||
{
|
||||
return VaryBy::none(); // Same cache for everyone
|
||||
}
|
||||
```
|
||||
|
||||
**Per-User Cache**:
|
||||
```php
|
||||
public function getVaryBy(): ?VaryBy
|
||||
{
|
||||
return VaryBy::userId(); // Different cache per user
|
||||
}
|
||||
```
|
||||
|
||||
**Per-User Per-Locale Cache**:
|
||||
```php
|
||||
public function getVaryBy(): ?VaryBy
|
||||
{
|
||||
return VaryBy::userAndLocale(); // Different cache per user and locale
|
||||
}
|
||||
```
|
||||
|
||||
**With Feature Flags**:
|
||||
```php
|
||||
public function getVaryBy(): ?VaryBy
|
||||
{
|
||||
return VaryBy::userId()
|
||||
->withFeatureFlags(['new-ui', 'dark-mode']);
|
||||
}
|
||||
```
|
||||
|
||||
### Stale-While-Revalidate (SWR)
|
||||
|
||||
Serve stale content while refreshing:
|
||||
|
||||
```php
|
||||
public function getCacheTTL(): Duration
|
||||
{
|
||||
return Duration::fromMinutes(5); // Fresh for 5 minutes
|
||||
}
|
||||
|
||||
public function getStaleWhileRevalidate(): ?Duration
|
||||
{
|
||||
return Duration::fromHours(1); // Serve stale for 1 hour while refreshing
|
||||
}
|
||||
```
|
||||
|
||||
**How it works**:
|
||||
- 0-5min: Serve fresh cache
|
||||
- 5min-1h: Serve stale cache + trigger background refresh
|
||||
- >1h: Force fresh render
|
||||
|
||||
### Cache Invalidation
|
||||
|
||||
**By Tags**:
|
||||
```php
|
||||
// Invalidate all user-stats caches
|
||||
$cache->invalidateTags(['user-stats']);
|
||||
|
||||
// Invalidate specific user's cache
|
||||
$cache->invalidateTags(['user-stats', 'user:123']);
|
||||
```
|
||||
|
||||
**Manual Invalidation**:
|
||||
```php
|
||||
$cacheKey = CacheKey::fromString('user-stats:123');
|
||||
$cache->delete($cacheKey);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Request Batching
|
||||
|
||||
### Client-Side Batching
|
||||
|
||||
Batch multiple operations in a single request:
|
||||
|
||||
```javascript
|
||||
// Batch multiple actions
|
||||
const response = await LiveComponent.executeBatch([
|
||||
{
|
||||
componentId: 'counter:demo',
|
||||
method: 'increment',
|
||||
params: { amount: 5 },
|
||||
fragments: ['counter-display']
|
||||
},
|
||||
{
|
||||
componentId: 'stats:user-123',
|
||||
method: 'refresh'
|
||||
},
|
||||
{
|
||||
componentId: 'notifications:user-123',
|
||||
method: 'markAsRead',
|
||||
params: { notificationId: 'abc' }
|
||||
}
|
||||
]);
|
||||
```
|
||||
|
||||
### Server-Side Processing
|
||||
|
||||
The framework automatically processes batch requests:
|
||||
|
||||
```php
|
||||
// BatchProcessor handles multiple operations
|
||||
$batchRequest = new BatchRequest(...$operations);
|
||||
$response = $batchProcessor->process($batchRequest);
|
||||
|
||||
// Returns:
|
||||
// {
|
||||
// "success": true,
|
||||
// "results": [
|
||||
// { "componentId": "counter:demo", "success": true, "html": "...", "fragments": {...} },
|
||||
// { "componentId": "stats:user-123", "success": true, "html": "..." },
|
||||
// { "componentId": "notifications:user-123", "success": true, "html": "..." }
|
||||
// ],
|
||||
// "totalOperations": 3,
|
||||
// "successCount": 3,
|
||||
// "failureCount": 0
|
||||
// }
|
||||
```
|
||||
|
||||
### Batch Benefits
|
||||
|
||||
- **Reduced HTTP Requests**: Multiple operations in one request
|
||||
- **Lower Latency**: Single round-trip instead of multiple
|
||||
- **Atomic Operations**: All succeed or all fail
|
||||
- **Better Performance**: Especially on slow networks
|
||||
|
||||
---
|
||||
|
||||
## Middleware Performance
|
||||
|
||||
### Middleware Overview
|
||||
|
||||
Middleware allows you to intercept and transform component actions:
|
||||
|
||||
```php
|
||||
#[LiveComponent('user-profile')]
|
||||
#[Middleware(LoggingMiddleware::class)]
|
||||
#[Middleware(CachingMiddleware::class, priority: 50)]
|
||||
final readonly class UserProfileComponent implements LiveComponentContract
|
||||
{
|
||||
// All actions have Logging + Caching middleware
|
||||
}
|
||||
```
|
||||
|
||||
### Built-in Middleware
|
||||
|
||||
**LoggingMiddleware** - Logs actions with timing:
|
||||
```php
|
||||
#[Middleware(LoggingMiddleware::class, priority: 50)]
|
||||
```
|
||||
|
||||
**CachingMiddleware** - Caches action responses:
|
||||
```php
|
||||
#[Middleware(CachingMiddleware::class, priority: 100)]
|
||||
```
|
||||
|
||||
**RateLimitMiddleware** - Rate limits actions:
|
||||
```php
|
||||
#[Middleware(RateLimitMiddleware::class, priority: 200)]
|
||||
```
|
||||
|
||||
### Custom Middleware
|
||||
|
||||
Create custom middleware for specific needs:
|
||||
|
||||
```php
|
||||
final readonly class PerformanceMonitoringMiddleware implements ComponentMiddlewareInterface
|
||||
{
|
||||
public function handle(
|
||||
LiveComponentContract $component,
|
||||
string $action,
|
||||
ActionParameters $params,
|
||||
callable $next
|
||||
): ComponentUpdate {
|
||||
$startTime = microtime(true);
|
||||
$startMemory = memory_get_usage();
|
||||
|
||||
$result = $next($component, $action, $params);
|
||||
|
||||
$duration = microtime(true) - $startTime;
|
||||
$memoryUsed = memory_get_usage() - $startMemory;
|
||||
|
||||
// Log performance metrics
|
||||
$this->metricsCollector->record($component::class, $action, $duration, $memoryUsed);
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Middleware Priority
|
||||
|
||||
Higher priority = earlier execution:
|
||||
|
||||
```php
|
||||
#[Middleware(LoggingMiddleware::class, priority: 50)] // Executes first
|
||||
#[Middleware(CachingMiddleware::class, priority: 100)] // Executes second
|
||||
#[Middleware(RateLimitMiddleware::class, priority: 200)] // Executes third
|
||||
```
|
||||
|
||||
**Execution Order**:
|
||||
1. RateLimitMiddleware (priority: 200)
|
||||
2. CachingMiddleware (priority: 100)
|
||||
3. LoggingMiddleware (priority: 50)
|
||||
4. Action execution
|
||||
|
||||
### Middleware Performance Tips
|
||||
|
||||
✅ **DO**:
|
||||
- Use caching middleware for expensive operations
|
||||
- Use logging middleware for debugging (disable in production)
|
||||
- Keep middleware lightweight
|
||||
- Use priority to optimize execution order
|
||||
|
||||
❌ **DON'T**:
|
||||
- Add unnecessary middleware
|
||||
- Perform heavy operations in middleware
|
||||
- Cache everything (be selective)
|
||||
- Use middleware for business logic
|
||||
|
||||
---
|
||||
|
||||
## Debounce & Throttle
|
||||
|
||||
### Client-Side Debouncing
|
||||
|
||||
Debounce user input to reduce requests:
|
||||
|
||||
```javascript
|
||||
let searchTimeout;
|
||||
const searchInput = document.querySelector('[data-live-action="search"]');
|
||||
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
LiveComponent.executeAction('search:demo', 'search', {
|
||||
query: e.target.value
|
||||
});
|
||||
}, 300); // 300ms debounce
|
||||
});
|
||||
```
|
||||
|
||||
### Client-Side Throttling
|
||||
|
||||
Throttle frequent actions:
|
||||
|
||||
```javascript
|
||||
let lastExecution = 0;
|
||||
const throttleDelay = 1000; // 1 second
|
||||
|
||||
function throttledAction() {
|
||||
const now = Date.now();
|
||||
if (now - lastExecution < throttleDelay) {
|
||||
return; // Skip if too soon
|
||||
}
|
||||
lastExecution = now;
|
||||
LiveComponent.executeAction('component:id', 'action', {});
|
||||
}
|
||||
```
|
||||
|
||||
### Server-Side Rate Limiting
|
||||
|
||||
Use `#[Action]` attribute with rate limit:
|
||||
|
||||
```php
|
||||
#[Action(rateLimit: 10)] // 10 requests per minute
|
||||
public function search(string $query): State
|
||||
{
|
||||
return $this->state->withResults($this->searchService->search($query));
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Lazy Loading
|
||||
|
||||
### Lazy Component Loading
|
||||
|
||||
Load components when entering viewport:
|
||||
|
||||
```php
|
||||
// In template
|
||||
{{ lazy_component('notification-center:user-123', [
|
||||
'priority' => 'high',
|
||||
'threshold' => '0.1',
|
||||
'placeholder' => 'Loading notifications...'
|
||||
]) }}
|
||||
```
|
||||
|
||||
**Options**:
|
||||
- `priority`: `'high'` | `'normal'` | `'low'`
|
||||
- `threshold`: `'0.0'` to `'1.0'` (viewport intersection threshold)
|
||||
- `placeholder`: Custom loading text
|
||||
|
||||
### Lazy Island Components
|
||||
|
||||
Use `#[Island]` for isolated lazy loading:
|
||||
|
||||
```php
|
||||
#[LiveComponent('heavy-widget')]
|
||||
#[Island(isolated: true, lazy: true, placeholder: 'Loading widget...')]
|
||||
final readonly class HeavyWidgetComponent implements LiveComponentContract
|
||||
{
|
||||
// Component implementation
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- Reduces initial page load time
|
||||
- Loads only when needed
|
||||
- Isolated rendering (no template overhead)
|
||||
|
||||
---
|
||||
|
||||
## Island Components
|
||||
|
||||
### Island Directive
|
||||
|
||||
Isolate resource-intensive components:
|
||||
|
||||
```php
|
||||
#[LiveComponent('analytics-dashboard')]
|
||||
#[Island(isolated: true, lazy: true)]
|
||||
final readonly class AnalyticsDashboardComponent implements LiveComponentContract
|
||||
{
|
||||
// Heavy component with complex calculations
|
||||
}
|
||||
```
|
||||
|
||||
**Features**:
|
||||
- **Isolated Rendering**: Separate from main template
|
||||
- **Lazy Loading**: Load on viewport entry
|
||||
- **Independent Updates**: No parent component re-renders
|
||||
|
||||
### When to Use Islands
|
||||
|
||||
✅ **Use Islands for**:
|
||||
- Heavy calculations
|
||||
- External API calls
|
||||
- Complex data visualizations
|
||||
- Third-party widgets
|
||||
- Below-the-fold content
|
||||
|
||||
❌ **Don't use Islands for**:
|
||||
- Simple components
|
||||
- Above-the-fold content
|
||||
- Components that need parent state
|
||||
- Frequently updated components
|
||||
|
||||
---
|
||||
|
||||
## Fragment Updates
|
||||
|
||||
### Partial Rendering
|
||||
|
||||
Update only specific parts of a component:
|
||||
|
||||
```html
|
||||
<!-- Template -->
|
||||
<div data-lc-fragment="counter-display">
|
||||
<h2>Count: {count}</h2>
|
||||
</div>
|
||||
|
||||
<div data-lc-fragment="actions">
|
||||
<button data-live-action="increment" data-lc-fragments="counter-display">
|
||||
Increment
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Client-side**:
|
||||
```javascript
|
||||
// Only counter-display fragment is updated
|
||||
LiveComponent.executeAction('counter:demo', 'increment', {}, {
|
||||
fragments: ['counter-display']
|
||||
});
|
||||
```
|
||||
|
||||
### Fragment Benefits
|
||||
|
||||
- **Reduced DOM Updates**: Only update what changed
|
||||
- **Better Performance**: Less re-rendering overhead
|
||||
- **Smoother UX**: Faster perceived performance
|
||||
- **Lower Bandwidth**: Smaller response payloads
|
||||
|
||||
---
|
||||
|
||||
## SSE Optimization
|
||||
|
||||
### Server-Sent Events
|
||||
|
||||
Real-time updates via SSE:
|
||||
|
||||
```php
|
||||
#[LiveComponent('live-feed')]
|
||||
final readonly class LiveFeedComponent implements LiveComponentContract, Pollable
|
||||
{
|
||||
public function getPollInterval(): int
|
||||
{
|
||||
return 5000; // Poll every 5 seconds
|
||||
}
|
||||
|
||||
public function poll(): LiveComponentState
|
||||
{
|
||||
// Fetch latest data
|
||||
$newItems = $this->feedService->getLatest();
|
||||
return $this->state->withItems($newItems);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### SSE Configuration
|
||||
|
||||
**Heartbeat Interval**:
|
||||
```env
|
||||
LIVECOMPONENT_SSE_HEARTBEAT=15 # Seconds
|
||||
```
|
||||
|
||||
**Connection Timeout**:
|
||||
```env
|
||||
LIVECOMPONENT_SSE_TIMEOUT=300 # Seconds
|
||||
```
|
||||
|
||||
### SSE Best Practices
|
||||
|
||||
✅ **DO**:
|
||||
- Use SSE for real-time updates
|
||||
- Set appropriate poll intervals
|
||||
- Handle reconnection gracefully
|
||||
- Monitor connection health
|
||||
|
||||
❌ **DON'T**:
|
||||
- Poll too frequently (< 1 second)
|
||||
- Keep connections open indefinitely
|
||||
- Ignore connection errors
|
||||
- Use SSE for one-time operations
|
||||
|
||||
---
|
||||
|
||||
## Memory Management
|
||||
|
||||
### Component State Size
|
||||
|
||||
Keep component state minimal:
|
||||
|
||||
```php
|
||||
// ✅ GOOD: Minimal state
|
||||
final readonly class CounterState extends ComponentState
|
||||
{
|
||||
public function __construct(
|
||||
public int $count = 0
|
||||
) {}
|
||||
}
|
||||
|
||||
// ❌ BAD: Large state
|
||||
final readonly class CounterState extends ComponentState
|
||||
{
|
||||
public function __construct(
|
||||
public int $count = 0,
|
||||
public array $largeDataSet = [], // Don't store large data in state
|
||||
public string $hugeString = '' // Don't store large strings
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
### State Cleanup
|
||||
|
||||
Clean up unused state:
|
||||
|
||||
```php
|
||||
#[Action]
|
||||
public function clearCache(): CounterState
|
||||
{
|
||||
// Remove cached data from state
|
||||
return $this->state->withoutCache();
|
||||
}
|
||||
```
|
||||
|
||||
### Memory Monitoring
|
||||
|
||||
Monitor component memory usage:
|
||||
|
||||
```php
|
||||
final readonly class MemoryMonitoringMiddleware implements ComponentMiddlewareInterface
|
||||
{
|
||||
public function handle($component, $action, $params, $next): ComponentUpdate
|
||||
{
|
||||
$startMemory = memory_get_usage();
|
||||
$result = $next($component, $action, $params);
|
||||
$endMemory = memory_get_usage();
|
||||
|
||||
if ($endMemory - $startMemory > 1024 * 1024) { // > 1MB
|
||||
error_log("Memory spike in {$component::class}::{$action}: " . ($endMemory - $startMemory));
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Checklist
|
||||
|
||||
### Component Development
|
||||
|
||||
- [ ] Use caching for expensive operations
|
||||
- [ ] Implement `Cacheable` interface where appropriate
|
||||
- [ ] Use fragments for partial updates
|
||||
- [ ] Use lazy loading for below-the-fold content
|
||||
- [ ] Use islands for heavy components
|
||||
- [ ] Keep component state minimal
|
||||
- [ ] Use batch requests for multiple operations
|
||||
- [ ] Debounce/throttle user input
|
||||
- [ ] Set appropriate rate limits
|
||||
|
||||
### Middleware
|
||||
|
||||
- [ ] Use caching middleware for expensive actions
|
||||
- [ ] Use logging middleware for debugging (disable in production)
|
||||
- [ ] Keep middleware lightweight
|
||||
- [ ] Set appropriate middleware priorities
|
||||
- [ ] Monitor middleware performance
|
||||
|
||||
### Caching
|
||||
|
||||
- [ ] Configure cache TTL appropriately
|
||||
- [ ] Use cache tags for grouped invalidation
|
||||
- [ ] Use `VaryBy` for user-specific caching
|
||||
- [ ] Use SWR for better perceived performance
|
||||
- [ ] Monitor cache hit rates
|
||||
|
||||
### Testing
|
||||
|
||||
- [ ] Test component performance under load
|
||||
- [ ] Monitor memory usage
|
||||
- [ ] Test with slow network conditions
|
||||
- [ ] Test batch request performance
|
||||
- [ ] Test fragment update performance
|
||||
|
||||
---
|
||||
|
||||
## Performance Patterns
|
||||
|
||||
### Pattern 1: Cached Search Component
|
||||
|
||||
```php
|
||||
#[LiveComponent('search')]
|
||||
#[Middleware(CachingMiddleware::class, priority: 100)]
|
||||
final readonly class SearchComponent implements LiveComponentContract, Cacheable
|
||||
{
|
||||
#[Action(rateLimit: 20)]
|
||||
public function search(string $query): SearchState
|
||||
{
|
||||
if (strlen($query) < 3) {
|
||||
return $this->state->withResults([]);
|
||||
}
|
||||
|
||||
$results = $this->searchService->search($query);
|
||||
return $this->state->withQuery($query)->withResults($results);
|
||||
}
|
||||
|
||||
public function getCacheKey(): string
|
||||
{
|
||||
return 'search:' . md5($this->state->query);
|
||||
}
|
||||
|
||||
public function getCacheTTL(): Duration
|
||||
{
|
||||
return Duration::fromMinutes(10);
|
||||
}
|
||||
|
||||
public function getVaryBy(): ?VaryBy
|
||||
{
|
||||
return VaryBy::userId(); // Different results per user
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: Lazy-Loaded Dashboard
|
||||
|
||||
```php
|
||||
#[LiveComponent('dashboard')]
|
||||
#[Island(isolated: true, lazy: true, placeholder: 'Loading dashboard...')]
|
||||
final readonly class DashboardComponent implements LiveComponentContract, Cacheable
|
||||
{
|
||||
public function getCacheKey(): string
|
||||
{
|
||||
return 'dashboard:' . $this->state->userId;
|
||||
}
|
||||
|
||||
public function getCacheTTL(): Duration
|
||||
{
|
||||
return Duration::fromMinutes(5);
|
||||
}
|
||||
|
||||
public function getStaleWhileRevalidate(): ?Duration
|
||||
{
|
||||
return Duration::fromHours(1); // Serve stale for 1 hour
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 3: Batch Operations
|
||||
|
||||
```javascript
|
||||
// Client-side: Batch multiple updates
|
||||
const updates = [
|
||||
{ componentId: 'counter:1', method: 'increment', params: {} },
|
||||
{ componentId: 'counter:2', method: 'increment', params: {} },
|
||||
{ componentId: 'counter:3', method: 'increment', params: {} },
|
||||
];
|
||||
|
||||
const response = await LiveComponent.executeBatch(updates);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Security Guide](security-guide-complete.md) - Security best practices
|
||||
- [End-to-End Guide](end-to-end-guide.md) - Complete development guide
|
||||
- [API Reference](api-reference-complete.md) - Complete API documentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
756
docs/livecomponents/security-guide-complete.md
Normal file
756
docs/livecomponents/security-guide-complete.md
Normal file
@@ -0,0 +1,756 @@
|
||||
# LiveComponents Security Guide
|
||||
|
||||
**Complete security guide for LiveComponents covering CSRF protection, rate limiting, idempotency, authorization, and best practices.**
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [CSRF Protection](#csrf-protection)
|
||||
2. [Rate Limiting](#rate-limiting)
|
||||
3. [Idempotency](#idempotency)
|
||||
4. [Action Allow-List](#action-allow-list)
|
||||
5. [Authorization](#authorization)
|
||||
6. [Input Validation](#input-validation)
|
||||
7. [XSS Prevention](#xss-prevention)
|
||||
8. [Secure State Management](#secure-state-management)
|
||||
9. [Security Checklist](#security-checklist)
|
||||
|
||||
---
|
||||
|
||||
## CSRF Protection
|
||||
|
||||
### How It Works
|
||||
|
||||
LiveComponents automatically protect all actions with CSRF tokens:
|
||||
|
||||
1. **Token Generation**: Unique token generated per component instance
|
||||
2. **Token Validation**: Token validated on every action request
|
||||
3. **Token Regeneration**: Token regenerated after each action
|
||||
|
||||
### Implementation
|
||||
|
||||
**Server-Side** (Automatic):
|
||||
|
||||
```php
|
||||
// CSRF token is automatically generated and validated
|
||||
#[Action]
|
||||
public function increment(): CounterState
|
||||
{
|
||||
// CSRF validation happens automatically before this method is called
|
||||
return $this->state->increment();
|
||||
}
|
||||
```
|
||||
|
||||
**Client-Side** (Automatic):
|
||||
|
||||
```html
|
||||
<!-- CSRF token is automatically included in action requests -->
|
||||
<button data-live-action="increment">Increment</button>
|
||||
```
|
||||
|
||||
### Manual CSRF Token Access
|
||||
|
||||
If you need to access CSRF tokens manually:
|
||||
|
||||
```php
|
||||
use App\Framework\LiveComponents\ComponentRegistry;
|
||||
|
||||
$registry = container()->get(ComponentRegistry::class);
|
||||
$csrfToken = $registry->generateCsrfToken($componentId);
|
||||
```
|
||||
|
||||
### CSRF Token Format
|
||||
|
||||
- **Length**: 32 hexadecimal characters
|
||||
- **Format**: `[a-f0-9]{32}`
|
||||
- **Scope**: Per component instance (not global)
|
||||
|
||||
### Best Practices
|
||||
|
||||
✅ **DO**:
|
||||
- Let the framework handle CSRF automatically
|
||||
- Include CSRF meta tag in base layout: `<meta name="csrf-token" content="{csrf_token}">`
|
||||
- Use HTTPS in production
|
||||
|
||||
❌ **DON'T**:
|
||||
- Disable CSRF protection
|
||||
- Share CSRF tokens between components
|
||||
- Store CSRF tokens in localStorage (use session)
|
||||
|
||||
---
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
### Overview
|
||||
|
||||
Rate limiting prevents abuse by limiting the number of actions per time period.
|
||||
|
||||
### Configuration
|
||||
|
||||
**Global Rate Limit** (`.env`):
|
||||
```env
|
||||
LIVECOMPONENT_RATE_LIMIT=60 # 60 requests per minute per component
|
||||
```
|
||||
|
||||
**Per-Action Rate Limit**:
|
||||
```php
|
||||
#[Action(rateLimit: 10)] // 10 requests per minute for this action
|
||||
public function expensiveOperation(): State
|
||||
{
|
||||
// Implementation
|
||||
}
|
||||
```
|
||||
|
||||
### Rate Limit Headers
|
||||
|
||||
When rate limit is exceeded, response includes:
|
||||
|
||||
```
|
||||
HTTP/1.1 429 Too Many Requests
|
||||
X-RateLimit-Limit: 60
|
||||
X-RateLimit-Remaining: 0
|
||||
X-RateLimit-Reset: 1640995200
|
||||
Retry-After: 30
|
||||
```
|
||||
|
||||
### Client-Side Handling
|
||||
|
||||
```javascript
|
||||
// Automatic retry after Retry-After header
|
||||
LiveComponent.executeAction('counter:demo', 'increment')
|
||||
.catch(error => {
|
||||
if (error.status === 429) {
|
||||
const retryAfter = error.headers['retry-after'];
|
||||
console.log(`Rate limited. Retry after ${retryAfter} seconds`);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Best Practices
|
||||
|
||||
✅ **DO**:
|
||||
- Set appropriate rate limits based on action cost
|
||||
- Use higher limits for read operations, lower for write operations
|
||||
- Monitor rate limit violations
|
||||
|
||||
❌ **DON'T**:
|
||||
- Set rate limits too low (hurts UX)
|
||||
- Set rate limits too high (allows abuse)
|
||||
- Ignore rate limit violations
|
||||
|
||||
---
|
||||
|
||||
## Idempotency
|
||||
|
||||
### Overview
|
||||
|
||||
Idempotency ensures that repeating the same action multiple times has the same effect as performing it once.
|
||||
|
||||
### Configuration
|
||||
|
||||
**Per-Action Idempotency**:
|
||||
```php
|
||||
#[Action(idempotencyTTL: 60)] // Idempotent for 60 seconds
|
||||
public function processPayment(float $amount): State
|
||||
{
|
||||
// This action will return cached result if called again within 60 seconds
|
||||
return $this->state->processPayment($amount);
|
||||
}
|
||||
```
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **First Request**: Action executes, result cached with idempotency key
|
||||
2. **Subsequent Requests**: Same idempotency key returns cached result
|
||||
3. **After TTL**: Cache expires, action can execute again
|
||||
|
||||
### Idempotency Key
|
||||
|
||||
**Client-Side**:
|
||||
```javascript
|
||||
// Generate unique idempotency key
|
||||
const idempotencyKey = `payment-${Date.now()}-${Math.random()}`;
|
||||
|
||||
LiveComponent.executeAction('payment:demo', 'processPayment', {
|
||||
amount: 100.00,
|
||||
idempotency_key: idempotencyKey
|
||||
});
|
||||
```
|
||||
|
||||
**Server-Side** (Automatic):
|
||||
- Idempotency key extracted from request
|
||||
- Key includes: component ID + action name + parameters
|
||||
- Cached result returned if key matches
|
||||
|
||||
### Use Cases
|
||||
|
||||
✅ **Good for**:
|
||||
- Payment processing
|
||||
- Order creation
|
||||
- Email sending
|
||||
- External API calls
|
||||
|
||||
❌ **Not suitable for**:
|
||||
- Incrementing counters
|
||||
- Appending to lists
|
||||
- Time-sensitive operations
|
||||
|
||||
### Best Practices
|
||||
|
||||
✅ **DO**:
|
||||
- Use idempotency for critical operations
|
||||
- Set appropriate TTL (long enough to prevent duplicates, short enough to allow retries)
|
||||
- Include idempotency key in client requests
|
||||
|
||||
❌ **DON'T**:
|
||||
- Use idempotency for operations that should execute multiple times
|
||||
- Set TTL too long (prevents legitimate retries)
|
||||
- Rely solely on idempotency for security
|
||||
|
||||
---
|
||||
|
||||
## Action Allow-List
|
||||
|
||||
### Overview
|
||||
|
||||
Only methods marked with `#[Action]` can be called from the client.
|
||||
|
||||
### Implementation
|
||||
|
||||
```php
|
||||
#[LiveComponent('user-profile')]
|
||||
final readonly class UserProfileComponent implements LiveComponentContract
|
||||
{
|
||||
// ✅ Can be called from client
|
||||
#[Action]
|
||||
public function updateProfile(array $data): State
|
||||
{
|
||||
return $this->state->updateProfile($data);
|
||||
}
|
||||
|
||||
// ❌ Cannot be called from client (no #[Action] attribute)
|
||||
public function internalMethod(): void
|
||||
{
|
||||
// This method is not exposed to the client
|
||||
}
|
||||
|
||||
// ❌ Reserved methods cannot be actions
|
||||
public function mount(): State
|
||||
{
|
||||
// Reserved method - cannot be called as action
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Reserved Methods
|
||||
|
||||
These methods cannot be actions:
|
||||
- `mount()`
|
||||
- `getRenderData()`
|
||||
- `getId()`
|
||||
- `getState()`
|
||||
- `getData()`
|
||||
- Methods starting with `_` (private convention)
|
||||
|
||||
### Security Benefits
|
||||
|
||||
- **Explicit API**: Only intended methods are callable
|
||||
- **No Accidental Exposure**: Internal methods stay internal
|
||||
- **Clear Intent**: `#[Action]` makes API surface explicit
|
||||
|
||||
### Best Practices
|
||||
|
||||
✅ **DO**:
|
||||
- Mark all public methods that should be callable with `#[Action]`
|
||||
- Keep internal methods without `#[Action]`
|
||||
- Use descriptive action names
|
||||
|
||||
❌ **DON'T**:
|
||||
- Mark internal methods with `#[Action]`
|
||||
- Expose sensitive operations without proper authorization
|
||||
- Use generic action names like `do()` or `execute()`
|
||||
|
||||
---
|
||||
|
||||
## Authorization
|
||||
|
||||
### Overview
|
||||
|
||||
Use `#[RequiresPermission]` to restrict actions to authorized users.
|
||||
|
||||
### Implementation
|
||||
|
||||
```php
|
||||
#[LiveComponent('post-editor')]
|
||||
final readonly class PostEditorComponent implements LiveComponentContract
|
||||
{
|
||||
#[Action]
|
||||
#[RequiresPermission('posts.edit')]
|
||||
public function updatePost(array $data): State
|
||||
{
|
||||
return $this->state->updatePost($data);
|
||||
}
|
||||
|
||||
#[Action]
|
||||
#[RequiresPermission('posts.delete')]
|
||||
public function deletePost(): State
|
||||
{
|
||||
return $this->state->deletePost();
|
||||
}
|
||||
|
||||
// Multiple permissions (user needs ALL)
|
||||
#[Action]
|
||||
#[RequiresPermission('posts.edit', 'posts.publish')]
|
||||
public function publishPost(): State
|
||||
{
|
||||
return $this->state->publishPost();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Permission Checking
|
||||
|
||||
**Custom Authorization Checker**:
|
||||
|
||||
```php
|
||||
use App\Framework\LiveComponents\Security\AuthorizationCheckerInterface;
|
||||
|
||||
final readonly class CustomAuthorizationChecker implements AuthorizationCheckerInterface
|
||||
{
|
||||
public function hasPermission(string $permission): bool
|
||||
{
|
||||
// Check if current user has permission
|
||||
return $this->user->hasPermission($permission);
|
||||
}
|
||||
|
||||
public function hasAllPermissions(array $permissions): bool
|
||||
{
|
||||
foreach ($permissions as $permission) {
|
||||
if (!$this->hasPermission($permission)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Unauthorized Access
|
||||
|
||||
When user lacks required permission:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "UNAUTHORIZED",
|
||||
"message": "User does not have required permission: posts.edit"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Best Practices
|
||||
|
||||
✅ **DO**:
|
||||
- Use authorization for sensitive operations
|
||||
- Check permissions server-side (never trust client)
|
||||
- Use specific permissions (not generic like 'admin')
|
||||
- Log authorization failures
|
||||
|
||||
❌ **DON'T**:
|
||||
- Rely on client-side authorization checks
|
||||
- Use overly broad permissions
|
||||
- Skip authorization for write operations
|
||||
- Expose permission names in error messages
|
||||
|
||||
---
|
||||
|
||||
## Input Validation
|
||||
|
||||
### Overview
|
||||
|
||||
Always validate input in actions before processing.
|
||||
|
||||
### Implementation
|
||||
|
||||
```php
|
||||
#[Action]
|
||||
public function updateProfile(array $data): State
|
||||
{
|
||||
// Validate input
|
||||
$errors = [];
|
||||
|
||||
if (empty($data['name'])) {
|
||||
$errors[] = 'Name is required';
|
||||
}
|
||||
|
||||
if (isset($data['email']) && !filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
|
||||
$errors[] = 'Invalid email address';
|
||||
}
|
||||
|
||||
if (isset($data['age']) && ($data['age'] < 0 || $data['age'] > 150)) {
|
||||
$errors[] = 'Age must be between 0 and 150';
|
||||
}
|
||||
|
||||
if (!empty($errors)) {
|
||||
throw new ValidationException('Validation failed', $errors);
|
||||
}
|
||||
|
||||
// Process valid data
|
||||
return $this->state->updateProfile($data);
|
||||
}
|
||||
```
|
||||
|
||||
### Type Safety
|
||||
|
||||
Use type hints for automatic validation:
|
||||
|
||||
```php
|
||||
#[Action]
|
||||
public function addAmount(int $amount): State
|
||||
{
|
||||
// PHP automatically validates $amount is an integer
|
||||
if ($amount < 0) {
|
||||
throw new \InvalidArgumentException('Amount must be positive');
|
||||
}
|
||||
|
||||
return $this->state->addAmount($amount);
|
||||
}
|
||||
```
|
||||
|
||||
### Value Objects
|
||||
|
||||
Use Value Objects for complex validation:
|
||||
|
||||
```php
|
||||
#[Action]
|
||||
public function updateEmail(Email $email): State
|
||||
{
|
||||
// Email Value Object validates format automatically
|
||||
return $this->state->updateEmail($email);
|
||||
}
|
||||
```
|
||||
|
||||
### Best Practices
|
||||
|
||||
✅ **DO**:
|
||||
- Validate all input in actions
|
||||
- Use type hints for automatic validation
|
||||
- Use Value Objects for complex data
|
||||
- Return clear error messages
|
||||
|
||||
❌ **DON'T**:
|
||||
- Trust client input
|
||||
- Skip validation for "internal" actions
|
||||
- Expose internal validation details
|
||||
- Use generic error messages
|
||||
|
||||
---
|
||||
|
||||
## XSS Prevention
|
||||
|
||||
### Overview
|
||||
|
||||
LiveComponents automatically escape output in templates.
|
||||
|
||||
### Template Escaping
|
||||
|
||||
**Automatic Escaping**:
|
||||
```html
|
||||
<!-- Automatically escaped -->
|
||||
<div>{user_input}</div>
|
||||
|
||||
<!-- Safe HTML (if needed) -->
|
||||
<div>{html_content|raw}</div>
|
||||
```
|
||||
|
||||
### Best Practices
|
||||
|
||||
✅ **DO**:
|
||||
- Let the framework escape output automatically
|
||||
- Use `|raw` filter only when necessary
|
||||
- Validate HTML content before storing
|
||||
- Use Content Security Policy (CSP)
|
||||
|
||||
❌ **DON'T**:
|
||||
- Disable automatic escaping
|
||||
- Use `|raw` with user input
|
||||
- Store unvalidated HTML
|
||||
- Trust client-provided HTML
|
||||
|
||||
---
|
||||
|
||||
## Secure State Management
|
||||
|
||||
### Overview
|
||||
|
||||
Component state should never contain sensitive data.
|
||||
|
||||
### Sensitive Data
|
||||
|
||||
❌ **DON'T Store**:
|
||||
- Passwords
|
||||
- API keys
|
||||
- Credit card numbers
|
||||
- Session tokens
|
||||
- Private keys
|
||||
|
||||
✅ **DO Store**:
|
||||
- User IDs (not passwords)
|
||||
- Display preferences
|
||||
- UI state
|
||||
- Non-sensitive configuration
|
||||
|
||||
### State Encryption
|
||||
|
||||
For sensitive state (if absolutely necessary):
|
||||
|
||||
```php
|
||||
use App\Framework\Encryption\EncryptionService;
|
||||
|
||||
final readonly class SecureComponent implements LiveComponentContract
|
||||
{
|
||||
public function __construct(
|
||||
private EncryptionService $encryption
|
||||
) {
|
||||
}
|
||||
|
||||
public function storeSensitiveData(string $data): State
|
||||
{
|
||||
$encrypted = $this->encryption->encrypt($data);
|
||||
return $this->state->withEncryptedData($encrypted);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Best Practices
|
||||
|
||||
✅ **DO**:
|
||||
- Keep state minimal
|
||||
- Store only necessary data
|
||||
- Use server-side storage for sensitive data
|
||||
- Encrypt sensitive state if needed
|
||||
|
||||
❌ **DON'T**:
|
||||
- Store passwords or secrets in state
|
||||
- Include sensitive data in state
|
||||
- Trust client-provided state
|
||||
- Expose internal implementation details
|
||||
|
||||
---
|
||||
|
||||
## Security Checklist
|
||||
|
||||
### Component Development
|
||||
|
||||
- [ ] All actions marked with `#[Action]`
|
||||
- [ ] No sensitive data in component state
|
||||
- [ ] Input validation in all actions
|
||||
- [ ] Authorization checks for sensitive operations
|
||||
- [ ] Rate limiting configured appropriately
|
||||
- [ ] Idempotency for critical operations
|
||||
- [ ] CSRF protection enabled (automatic)
|
||||
- [ ] Error messages don't expose sensitive information
|
||||
|
||||
### Testing
|
||||
|
||||
- [ ] CSRF token validation tested
|
||||
- [ ] Rate limiting tested
|
||||
- [ ] Authorization tested
|
||||
- [ ] Input validation tested
|
||||
- [ ] XSS prevention tested
|
||||
- [ ] Error handling tested
|
||||
|
||||
### Deployment
|
||||
|
||||
- [ ] HTTPS enabled
|
||||
- [ ] CSRF protection enabled
|
||||
- [ ] Rate limits configured
|
||||
- [ ] Error reporting configured
|
||||
- [ ] Security headers configured
|
||||
- [ ] Content Security Policy configured
|
||||
|
||||
---
|
||||
|
||||
## Common Security Pitfalls
|
||||
|
||||
### 1. Trusting Client State
|
||||
|
||||
❌ **WRONG**:
|
||||
```php
|
||||
#[Action]
|
||||
public function updateBalance(float $newBalance): State
|
||||
{
|
||||
// Don't trust client-provided balance!
|
||||
return $this->state->withBalance($newBalance);
|
||||
}
|
||||
```
|
||||
|
||||
✅ **CORRECT**:
|
||||
```php
|
||||
#[Action]
|
||||
public function addToBalance(float $amount): State
|
||||
{
|
||||
// Calculate new balance server-side
|
||||
$newBalance = $this->state->balance + $amount;
|
||||
return $this->state->withBalance($newBalance);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Skipping Authorization
|
||||
|
||||
❌ **WRONG**:
|
||||
```php
|
||||
#[Action]
|
||||
public function deleteUser(int $userId): State
|
||||
{
|
||||
// No authorization check!
|
||||
$this->userService->delete($userId);
|
||||
return $this->state;
|
||||
}
|
||||
```
|
||||
|
||||
✅ **CORRECT**:
|
||||
```php
|
||||
#[Action]
|
||||
#[RequiresPermission('users.delete')]
|
||||
public function deleteUser(int $userId): State
|
||||
{
|
||||
$this->userService->delete($userId);
|
||||
return $this->state;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Exposing Sensitive Data
|
||||
|
||||
❌ **WRONG**:
|
||||
```php
|
||||
public function getRenderData(): ComponentRenderData
|
||||
{
|
||||
return new ComponentRenderData(
|
||||
templatePath: 'user-profile',
|
||||
data: [
|
||||
'password' => $this->user->password, // ❌ Never expose passwords!
|
||||
'api_key' => $this->user->apiKey, // ❌ Never expose API keys!
|
||||
]
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
✅ **CORRECT**:
|
||||
```php
|
||||
public function getRenderData(): ComponentRenderData
|
||||
{
|
||||
return new ComponentRenderData(
|
||||
templatePath: 'user-profile',
|
||||
data: [
|
||||
'username' => $this->user->username,
|
||||
'email' => $this->user->email,
|
||||
// Only include safe, display data
|
||||
]
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Patterns
|
||||
|
||||
### Pattern 1: Secure File Upload
|
||||
|
||||
```php
|
||||
#[LiveComponent('file-uploader')]
|
||||
final readonly class FileUploaderComponent implements LiveComponentContract, SupportsFileUpload
|
||||
{
|
||||
public function validateUpload(UploadedFile $file): array
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
// Check file type
|
||||
$allowedTypes = ['image/jpeg', 'image/png'];
|
||||
if (!in_array($file->getMimeType(), $allowedTypes)) {
|
||||
$errors[] = 'Invalid file type';
|
||||
}
|
||||
|
||||
// Check file size
|
||||
if ($file->getSize() > 5 * 1024 * 1024) {
|
||||
$errors[] = 'File too large';
|
||||
}
|
||||
|
||||
// Check file extension
|
||||
$extension = pathinfo($file->getName(), PATHINFO_EXTENSION);
|
||||
$allowedExtensions = ['jpg', 'jpeg', 'png'];
|
||||
if (!in_array(strtolower($extension), $allowedExtensions)) {
|
||||
$errors[] = 'Invalid file extension';
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
#[Action]
|
||||
#[RequiresPermission('files.upload')]
|
||||
public function handleUpload(UploadedFile $file): State
|
||||
{
|
||||
// Additional server-side validation
|
||||
$errors = $this->validateUpload($file);
|
||||
if (!empty($errors)) {
|
||||
throw new ValidationException('Upload validation failed', $errors);
|
||||
}
|
||||
|
||||
// Process upload securely
|
||||
$path = $this->storage->store($file);
|
||||
return $this->state->withUploadedFile($path);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: Secure Payment Processing
|
||||
|
||||
```php
|
||||
#[LiveComponent('payment')]
|
||||
final readonly class PaymentComponent implements LiveComponentContract
|
||||
{
|
||||
#[Action]
|
||||
#[RequiresPermission('payments.process')]
|
||||
#[Action(rateLimit: 5, idempotencyTTL: 300)] // 5 per minute, 5 min idempotency
|
||||
public function processPayment(
|
||||
float $amount,
|
||||
string $paymentMethod,
|
||||
string $idempotencyKey
|
||||
): State {
|
||||
// Validate amount
|
||||
if ($amount <= 0 || $amount > 10000) {
|
||||
throw new ValidationException('Invalid amount');
|
||||
}
|
||||
|
||||
// Validate payment method
|
||||
$allowedMethods = ['credit_card', 'paypal'];
|
||||
if (!in_array($paymentMethod, $allowedMethods)) {
|
||||
throw new ValidationException('Invalid payment method');
|
||||
}
|
||||
|
||||
// Process payment (idempotent)
|
||||
$transaction = $this->paymentService->process(
|
||||
$amount,
|
||||
$paymentMethod,
|
||||
$idempotencyKey
|
||||
);
|
||||
|
||||
return $this->state->withTransaction($transaction);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Performance Guide](performance-guide-complete.md) - Optimization techniques
|
||||
- [End-to-End Guide](end-to-end-guide.md) - Complete development guide
|
||||
- [API Reference](api-reference-complete.md) - Complete API documentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -11,9 +11,10 @@ This guide covers the integrated UI features available in LiveComponents, includ
|
||||
1. [Tooltip System](#tooltip-system)
|
||||
2. [Loading States & Skeleton Loading](#loading-states--skeleton-loading)
|
||||
3. [UI Helper System](#ui-helper-system)
|
||||
4. [Notification Component](#notification-component)
|
||||
5. [Dialog & Modal Integration](#dialog--modal-integration)
|
||||
6. [Best Practices](#best-practices)
|
||||
4. [Event-Based UI Integration](#event-based-ui-integration)
|
||||
5. [Notification Component](#notification-component)
|
||||
6. [Dialog & Modal Integration](#dialog--modal-integration)
|
||||
7. [Best Practices](#best-practices)
|
||||
|
||||
---
|
||||
|
||||
@@ -338,6 +339,332 @@ uiHelper.hideNotification('component-id');
|
||||
|
||||
---
|
||||
|
||||
## Event-Based UI Integration
|
||||
|
||||
### Overview
|
||||
|
||||
The Event-Based UI Integration system allows LiveComponents to trigger UI components (Toasts, Modals) by dispatching events from PHP actions. The JavaScript `UIEventHandler` automatically listens to these events and displays the appropriate UI components.
|
||||
|
||||
**Features**:
|
||||
- Automatic UI component display from PHP events
|
||||
- Type-safe event payloads
|
||||
- Toast queue management
|
||||
- Modal stack management
|
||||
- Zero JavaScript code required in components
|
||||
|
||||
### Architecture
|
||||
|
||||
- **PHP Side**: Components dispatch events via `ComponentEventDispatcher`
|
||||
- **JavaScript Side**: `UIEventHandler` listens to events and displays UI components
|
||||
- **Integration**: Events are automatically dispatched after action execution
|
||||
|
||||
### Basic Usage with UIHelper
|
||||
|
||||
The `UIHelper` class provides convenient methods for dispatching UI events:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\LiveComponents\Product;
|
||||
|
||||
use App\Framework\LiveComponents\Attributes\Action;
|
||||
use App\Framework\LiveComponents\Attributes\LiveComponent;
|
||||
use App\Framework\LiveComponents\ComponentEventDispatcher;
|
||||
use App\Framework\LiveComponents\Contracts\LiveComponentContract;
|
||||
use App\Framework\LiveComponents\UI\UIHelper;
|
||||
use App\Framework\LiveComponents\ValueObjects\ComponentId;
|
||||
|
||||
#[LiveComponent('product-form')]
|
||||
final readonly class ProductFormComponent implements LiveComponentContract
|
||||
{
|
||||
public function __construct(
|
||||
public ComponentId $id,
|
||||
public ProductFormState $state
|
||||
) {
|
||||
}
|
||||
|
||||
#[Action]
|
||||
public function save(?ComponentEventDispatcher $events = null): ProductFormState
|
||||
{
|
||||
// ... save logic ...
|
||||
|
||||
// Show success toast
|
||||
(new UIHelper($events))->successToast('Product saved successfully!');
|
||||
|
||||
return $this->state->withSaved();
|
||||
}
|
||||
|
||||
#[Action]
|
||||
public function delete(int $productId, ?ComponentEventDispatcher $events = null): ProductFormState
|
||||
{
|
||||
// Show confirmation dialog with action
|
||||
(new UIHelper($events))->confirmDelete(
|
||||
$this->id,
|
||||
"product #{$productId}",
|
||||
'doDelete',
|
||||
['id' => $productId]
|
||||
);
|
||||
|
||||
return $this->state;
|
||||
}
|
||||
|
||||
#[Action]
|
||||
public function doDelete(int $id): ProductFormState
|
||||
{
|
||||
// This action is automatically called when user confirms
|
||||
// ... delete logic ...
|
||||
return $this->state->withoutProduct($id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Toast Events
|
||||
|
||||
#### Show Toast
|
||||
|
||||
```php
|
||||
use App\Framework\LiveComponents\UI\UIHelper;
|
||||
|
||||
#[Action]
|
||||
public function save(?ComponentEventDispatcher $events = null): State
|
||||
{
|
||||
// Basic success toast
|
||||
(new UIHelper($events))->successToast('Saved successfully!');
|
||||
|
||||
// Toast with custom duration
|
||||
(new UIHelper($events))->infoToast('File uploaded', 7000);
|
||||
|
||||
// Using fluent interface for multiple toasts
|
||||
(new UIHelper($events))
|
||||
->infoToast('Processing...')
|
||||
->successToast('Done!');
|
||||
|
||||
return $this->state;
|
||||
}
|
||||
```
|
||||
|
||||
**Toast Types**:
|
||||
- `info` - Blue, informational message
|
||||
- `success` - Green, success message
|
||||
- `warning` - Orange, warning message
|
||||
- `error` - Red, error message
|
||||
|
||||
**Positions**:
|
||||
- `top-right` (default)
|
||||
- `top-left`
|
||||
- `bottom-right`
|
||||
- `bottom-left`
|
||||
|
||||
#### Hide Toast
|
||||
|
||||
```php
|
||||
#[Action]
|
||||
public function dismissNotification(?ComponentEventDispatcher $events = null): State
|
||||
{
|
||||
$this->hideToast($events, 'global');
|
||||
return $this->state;
|
||||
}
|
||||
```
|
||||
|
||||
### Modal Events
|
||||
|
||||
#### Show Modal
|
||||
|
||||
```php
|
||||
use App\Framework\LiveComponents\UI\UIHelper;
|
||||
use App\Framework\LiveComponents\Events\UI\Options\ModalOptions;
|
||||
use App\Framework\LiveComponents\Events\UI\Enums\ModalSize;
|
||||
|
||||
#[Action]
|
||||
public function showEditForm(int $userId, ?ComponentEventDispatcher $events = null): State
|
||||
{
|
||||
$formHtml = $this->renderEditForm($userId);
|
||||
|
||||
(new UIHelper($events))->modal(
|
||||
$this->id,
|
||||
'Edit User',
|
||||
$formHtml,
|
||||
ModalOptions::create()
|
||||
->withSize(ModalSize::Large)
|
||||
->withButtons([
|
||||
['text' => 'Save', 'class' => 'btn-primary', 'action' => 'save'],
|
||||
['text' => 'Cancel', 'class' => 'btn-secondary', 'action' => 'close']
|
||||
])
|
||||
);
|
||||
|
||||
return $this->state;
|
||||
}
|
||||
```
|
||||
|
||||
#### Show Confirmation Dialog
|
||||
|
||||
```php
|
||||
use App\Framework\LiveComponents\UI\UIHelper;
|
||||
|
||||
#[Action]
|
||||
public function requestDelete(int $id, ?ComponentEventDispatcher $events = null): State
|
||||
{
|
||||
(new UIHelper($events))->confirmDelete(
|
||||
$this->id,
|
||||
"item #{$id}",
|
||||
'doDelete',
|
||||
['id' => $id]
|
||||
);
|
||||
|
||||
return $this->state;
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: Confirmation dialogs return a result via the `modal:confirm:result` event. You can listen to this event in JavaScript to handle the confirmation:
|
||||
|
||||
```javascript
|
||||
document.addEventListener('modal:confirm:result', (e) => {
|
||||
const { componentId, confirmed } = e.detail;
|
||||
if (confirmed) {
|
||||
// User confirmed - execute action
|
||||
LiveComponentManager.getInstance()
|
||||
.executeAction(componentId, 'confirmDelete', { id: 123 });
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### Show Alert Dialog
|
||||
|
||||
```php
|
||||
use App\Framework\LiveComponents\UI\UIHelper;
|
||||
|
||||
#[Action]
|
||||
public function showError(?ComponentEventDispatcher $events = null): State
|
||||
{
|
||||
(new UIHelper($events))->alertError(
|
||||
$this->id,
|
||||
'Error',
|
||||
'An error occurred while processing your request.'
|
||||
);
|
||||
|
||||
return $this->state;
|
||||
}
|
||||
```
|
||||
|
||||
#### Close Modal
|
||||
|
||||
```php
|
||||
use App\Framework\LiveComponents\UI\UIHelper;
|
||||
|
||||
#[Action]
|
||||
public function closeModal(?ComponentEventDispatcher $events = null): State
|
||||
{
|
||||
(new UIHelper($events))->closeModal($this->id);
|
||||
return $this->state;
|
||||
}
|
||||
```
|
||||
|
||||
### Manual Event Dispatching
|
||||
|
||||
If you prefer to dispatch events manually (without UIHelper):
|
||||
|
||||
```php
|
||||
use App\Framework\LiveComponents\ComponentEventDispatcher;
|
||||
use App\Framework\LiveComponents\Events\UI\ToastShowEvent;
|
||||
|
||||
#[Action]
|
||||
public function save(?ComponentEventDispatcher $events = null): State
|
||||
{
|
||||
// ... save logic ...
|
||||
|
||||
if ($events !== null) {
|
||||
// Option 1: Using Event classes (recommended)
|
||||
$events->dispatchEvent(ToastShowEvent::success('Saved successfully!'));
|
||||
|
||||
// Option 2: Direct instantiation
|
||||
$events->dispatchEvent(new ToastShowEvent(
|
||||
message: 'Saved successfully!',
|
||||
type: 'success',
|
||||
duration: 5000
|
||||
));
|
||||
}
|
||||
|
||||
return $this->state;
|
||||
}
|
||||
```
|
||||
|
||||
### JavaScript Event Handling
|
||||
|
||||
The `UIEventHandler` automatically listens to these events and displays UI components. You can also listen to events manually:
|
||||
|
||||
```javascript
|
||||
// Listen to toast events
|
||||
document.addEventListener('toast:show', (e) => {
|
||||
const { message, type, duration, position } = e.detail;
|
||||
console.log(`Toast: ${message} (${type})`);
|
||||
});
|
||||
|
||||
// Listen to modal events
|
||||
document.addEventListener('modal:show', (e) => {
|
||||
const { componentId, title, content } = e.detail;
|
||||
console.log(`Modal shown: ${title}`);
|
||||
});
|
||||
|
||||
// Listen to confirmation results
|
||||
document.addEventListener('modal:confirm:result', (e) => {
|
||||
const { componentId, confirmed } = e.detail;
|
||||
if (confirmed) {
|
||||
// Handle confirmation
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Toast Queue Management
|
||||
|
||||
The `ToastQueue` automatically manages multiple toasts:
|
||||
- Maximum 5 toasts per position (configurable)
|
||||
- Automatic stacking with spacing
|
||||
- Oldest toast removed when limit reached
|
||||
- Auto-dismiss with queue management
|
||||
|
||||
### Modal Stack Management
|
||||
|
||||
The `ModalManager` handles modal stacking using native `<dialog>` element:
|
||||
- **Native `<dialog>` element**: Uses `showModal()` for true modal behavior
|
||||
- **Automatic backdrop**: Native `::backdrop` pseudo-element
|
||||
- **Automatic focus management**: Browser handles focus trapping
|
||||
- **Automatic ESC handling**: Native `cancel` event
|
||||
- **Z-index management**: Manual stacking for nested modals
|
||||
- **Stack tracking**: Manages multiple open modals
|
||||
|
||||
#### Native `<dialog>` Benefits
|
||||
|
||||
The `<dialog>` element provides:
|
||||
- **True modal behavior**: Blocks background interaction via `showModal()`
|
||||
- **Native backdrop**: Automatic overlay with `::backdrop` pseudo-element
|
||||
- **Automatic focus trapping**: Browser handles focus management
|
||||
- **Automatic ESC handling**: Native `cancel` event
|
||||
- **Better accessibility**: Native ARIA attributes and semantics
|
||||
- **Better performance**: Native browser implementation
|
||||
- **Wide browser support**: Chrome 37+, Firefox 98+, Safari 15.4+
|
||||
|
||||
The system uses native `<dialog>` features for optimal modal behavior and accessibility.
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Use UIHelper**: Prefer the UIHelper class over manual event dispatching for type safety and convenience
|
||||
2. **Component IDs**: Always provide component IDs for modals to enable proper management
|
||||
3. **Toast Duration**: Use appropriate durations (5s for success, longer for errors)
|
||||
4. **Modal Sizes**: Choose appropriate sizes (small for alerts, large for forms)
|
||||
5. **Error Handling**: Always show user-friendly error messages
|
||||
|
||||
```php
|
||||
// Good: Clear, user-friendly toast
|
||||
(new UIHelper($events))->successToast('Product saved successfully!');
|
||||
|
||||
// Bad: Technical error message
|
||||
(new UIHelper($events))->errorToast('SQL Error: INSERT failed');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notification Component
|
||||
|
||||
### Overview
|
||||
@@ -519,18 +846,23 @@ final class UserSettings extends LiveComponent
|
||||
### Modal with LiveComponent Content
|
||||
|
||||
```php
|
||||
use App\Framework\LiveComponents\UI\UIHelper;
|
||||
use App\Framework\LiveComponents\Events\UI\Options\ModalOptions;
|
||||
use App\Framework\LiveComponents\Events\UI\Enums\ModalSize;
|
||||
|
||||
#[Action]
|
||||
public function showUserModal(int $userId): void
|
||||
public function showUserModal(int $userId, ?ComponentEventDispatcher $events = null): void
|
||||
{
|
||||
$userComponent = UserDetailsComponent::mount(
|
||||
ComponentId::generate('user-details'),
|
||||
userId: $userId
|
||||
);
|
||||
|
||||
$this->uiHelper->showModal(
|
||||
title: 'User Details',
|
||||
content: $userComponent->render(),
|
||||
size: 'medium'
|
||||
(new UIHelper($events))->modal(
|
||||
$this->id,
|
||||
'User Details',
|
||||
$userComponent->render(),
|
||||
ModalOptions::create()->withSize(ModalSize::Medium)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
317
docs/refactoring/discovery-registry-loading-analysis.md
Normal file
317
docs/refactoring/discovery-registry-loading-analysis.md
Normal file
@@ -0,0 +1,317 @@
|
||||
# Discovery Registry Loading - Problem-Analyse und Refactoring-Vorschläge
|
||||
|
||||
## Problem-Identifikation
|
||||
|
||||
### Aktuelles Problem
|
||||
Die `DiscoveryRegistry` wird über **zwei verschiedene Mechanismen** geladen, was zu Inkonsistenzen führt:
|
||||
|
||||
1. **`DiscoveryServiceBootstrapper::bootstrap()`** (Runtime)
|
||||
- Wird in `ContainerBootstrapper::autowire()` aufgerufen
|
||||
- Verwendet `DiscoveryCacheManager` mit `isStale()` Prüfung
|
||||
- Registriert `DiscoveryRegistry` als Singleton im Container
|
||||
- Führt Discovery durch, wenn Cache stale ist oder nicht existiert
|
||||
|
||||
2. **`DiscoveryRegistryInitializer`** (Build-Time)
|
||||
- Ist ein `#[Initializer(ContextType::ALL)]`
|
||||
- Lädt aus `storage/discovery/` Dateien (Build-Time Storage)
|
||||
- Wird möglicherweise vor/nach `DiscoveryServiceBootstrapper` ausgeführt
|
||||
- Kann eine alte Registry laden, die dann als Singleton registriert wird
|
||||
|
||||
### Konflikt-Szenario
|
||||
|
||||
**Szenario 1: DiscoveryRegistryInitializer läuft zuerst**
|
||||
```
|
||||
1. DiscoveryRegistryInitializer läuft (Initializer-System)
|
||||
2. Lädt alte Registry aus storage/discovery/ (ohne DatabaseAssetGalleryDataProvider)
|
||||
3. Registriert als Singleton im Container
|
||||
4. DiscoveryServiceBootstrapper läuft
|
||||
5. Prüft Cache -> findet alte Registry (nicht stale, weil Datei vor Cache-Erstellung geändert wurde)
|
||||
6. Verwendet alte Registry -> Provider fehlt
|
||||
```
|
||||
|
||||
**Szenario 2: DiscoveryServiceBootstrapper läuft zuerst**
|
||||
```
|
||||
1. DiscoveryServiceBootstrapper läuft (autowire)
|
||||
2. Führt Discovery durch -> findet DatabaseAssetGalleryDataProvider
|
||||
3. Registriert als Singleton
|
||||
4. DiscoveryRegistryInitializer läuft später
|
||||
5. Lädt alte Registry aus storage/discovery/
|
||||
6. Überschreibt Singleton? -> Provider geht verloren
|
||||
```
|
||||
|
||||
## Root Cause Analysis
|
||||
|
||||
### Problem 1: Zwei verschiedene Cache-Mechanismen
|
||||
- **Runtime-Cache**: `DiscoveryCacheManager` mit `isStale()` Prüfung
|
||||
- **Build-Time Storage**: `storage/discovery/` Dateien (veraltet?)
|
||||
|
||||
### Problem 2: Unklare Ausführungsreihenfolge
|
||||
- `DiscoveryServiceBootstrapper` läuft in `autowire()` (explizit)
|
||||
- `DiscoveryRegistryInitializer` läuft über Initializer-System (automatisch)
|
||||
- Keine garantierte Reihenfolge
|
||||
|
||||
### Problem 3: isStale() Prüfung funktioniert nicht korrekt
|
||||
- **BEHOBEN**: Verwendet jetzt Cache-Erstellungszeit statt Request-Zeit
|
||||
- Aber: Wenn `DiscoveryRegistryInitializer` eine alte Registry lädt, wird diese nicht als stale erkannt
|
||||
|
||||
## Refactoring-Vorschläge
|
||||
|
||||
### Option 1: DiscoveryRegistryInitializer entfernen (Empfohlen)
|
||||
|
||||
**Vorteile:**
|
||||
- Eliminiert Konflikt zwischen zwei Mechanismen
|
||||
- Einheitliche Cache-Strategie über `DiscoveryCacheManager`
|
||||
- `isStale()` Prüfung funktioniert korrekt
|
||||
|
||||
**Nachteile:**
|
||||
- Build-Time Storage wird nicht mehr verwendet
|
||||
- Möglicherweise Performance-Verlust beim ersten Request
|
||||
|
||||
**Implementierung:**
|
||||
1. `DiscoveryRegistryInitializer` entfernen oder deaktivieren
|
||||
2. Sicherstellen, dass `DiscoveryServiceBootstrapper` immer läuft
|
||||
3. `DiscoveryCacheManager` als einzige Quelle für Discovery-Registry
|
||||
|
||||
### Option 2: DiscoveryRegistryInitializer als Fallback
|
||||
|
||||
**Vorteile:**
|
||||
- Behält Build-Time Storage für Performance
|
||||
- Fallback wenn Runtime-Cache nicht verfügbar
|
||||
|
||||
**Nachteile:**
|
||||
- Komplexere Logik
|
||||
- Mögliche Inkonsistenzen zwischen Build-Time und Runtime
|
||||
|
||||
**Implementierung:**
|
||||
1. `DiscoveryServiceBootstrapper` prüft zuerst Runtime-Cache
|
||||
2. Wenn nicht verfügbar/stale, prüft `DiscoveryRegistryInitializer`
|
||||
3. Nur wenn beide fehlschlagen, führt Discovery durch
|
||||
|
||||
### Option 3: Einheitliche Cache-Strategie
|
||||
|
||||
**Vorteile:**
|
||||
- Einheitliche Quelle für Discovery-Registry
|
||||
- Klare Verantwortlichkeiten
|
||||
|
||||
**Nachteile:**
|
||||
- Größere Refactoring-Arbeit
|
||||
|
||||
**Implementierung:**
|
||||
1. `DiscoveryCacheManager` verwendet `storage/discovery/` als Backend
|
||||
2. `DiscoveryRegistryInitializer` entfernen
|
||||
3. `DiscoveryServiceBootstrapper` verwendet nur `DiscoveryCacheManager`
|
||||
|
||||
### Option 4: Priority-basierte Ausführungsreihenfolge
|
||||
|
||||
**Vorteile:**
|
||||
- Beide Mechanismen bleiben erhalten
|
||||
- Klare Ausführungsreihenfolge
|
||||
|
||||
**Nachteile:**
|
||||
- Komplexere Initializer-Logik
|
||||
- Mögliche Race-Conditions
|
||||
|
||||
**Implementierung:**
|
||||
1. `DiscoveryServiceBootstrapper` mit höherer Priority
|
||||
2. `DiscoveryRegistryInitializer` prüft ob Registry bereits existiert
|
||||
3. Nur laden wenn nicht vorhanden
|
||||
|
||||
## Empfohlene Lösung: Option 1 + Verbesserungen
|
||||
|
||||
### Schritt 1: DiscoveryRegistryInitializer deaktivieren/entfernen
|
||||
|
||||
```php
|
||||
// DiscoveryRegistryInitializer.php
|
||||
#[Initializer(ContextType::ALL)]
|
||||
public function __invoke(): DiscoveryRegistry
|
||||
{
|
||||
// DEPRECATED: This initializer is deprecated in favor of DiscoveryServiceBootstrapper
|
||||
// Check if DiscoveryRegistry already exists (from DiscoveryServiceBootstrapper)
|
||||
if ($this->container->has(DiscoveryRegistry::class)) {
|
||||
return $this->container->get(DiscoveryRegistry::class);
|
||||
}
|
||||
|
||||
// Fallback: Load from storage (for backward compatibility)
|
||||
$attributes = $this->discoveryLoader->loadAttributes() ?? new AttributeRegistry();
|
||||
$interfaces = $this->discoveryLoader->loadInterfaces() ?? new InterfaceRegistry();
|
||||
$templates = $this->discoveryLoader->loadTemplates() ?? new TemplateRegistry();
|
||||
|
||||
return new DiscoveryRegistry(
|
||||
attributes: $attributes,
|
||||
interfaces: $interfaces,
|
||||
templates: $templates
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Schritt 2: DiscoveryServiceBootstrapper als einzige Quelle
|
||||
|
||||
```php
|
||||
// DiscoveryServiceBootstrapper.php
|
||||
public function bootstrap(): DiscoveryRegistry
|
||||
{
|
||||
// Prüfe ob bereits registriert (z.B. durch DiscoveryRegistryInitializer)
|
||||
if ($this->container->has(DiscoveryRegistry::class)) {
|
||||
$existing = $this->container->get(DiscoveryRegistry::class);
|
||||
|
||||
// Prüfe ob stale
|
||||
if ($this->isRegistryStale($existing)) {
|
||||
// Force refresh
|
||||
$this->container->forget(DiscoveryRegistry::class);
|
||||
} else {
|
||||
return $existing;
|
||||
}
|
||||
}
|
||||
|
||||
// ... rest of bootstrap logic
|
||||
}
|
||||
```
|
||||
|
||||
### Schritt 3: Verbesserte isStale() Prüfung
|
||||
|
||||
```php
|
||||
// DiscoveryCacheManager.php
|
||||
private function isStale(DiscoveryContext $context, DiscoveryRegistry $registry, ?\DateTimeInterface $cacheStartTime = null): bool
|
||||
{
|
||||
// ... existing logic
|
||||
|
||||
// ZUSÄTZLICH: Prüfe ob Dateien seit Cache-Erstellung geändert wurden
|
||||
// Dies ist bereits implementiert, aber sollte auch für Build-Time Storage gelten
|
||||
}
|
||||
```
|
||||
|
||||
## Weitere Verbesserungen
|
||||
|
||||
### 1. Discovery-Registry Versionierung
|
||||
- Jede Registry hat eine Version/Timestamp
|
||||
- Vergleich zwischen Build-Time und Runtime-Cache
|
||||
- Automatische Invalidation bei Versionsunterschieden
|
||||
|
||||
### 2. Unified Discovery Storage
|
||||
- Einheitliche Storage-Strategie für Build-Time und Runtime
|
||||
- `DiscoveryCacheManager` verwendet `storage/discovery/` als Backend
|
||||
- Konsistente Cache-Invalidation
|
||||
|
||||
### 3. Discovery-Registry Factory
|
||||
- Zentrale Factory für Discovery-Registry-Erstellung
|
||||
- Einheitliche Logik für alle Quellen
|
||||
- Klare Verantwortlichkeiten
|
||||
|
||||
### 4. Logging und Monitoring
|
||||
- Besseres Logging für Discovery-Registry-Loading
|
||||
- Metriken für Cache-Hits/Misses
|
||||
- Warnungen bei Inkonsistenzen
|
||||
|
||||
## Migration-Plan
|
||||
|
||||
1. **Phase 1**: `DiscoveryRegistryInitializer` als Fallback behalten, aber prüfen ob Registry bereits existiert
|
||||
2. **Phase 2**: `DiscoveryServiceBootstrapper` prüft ob Registry stale ist, auch wenn bereits registriert
|
||||
3. **Phase 3**: `DiscoveryRegistryInitializer` vollständig entfernen
|
||||
4. **Phase 4**: Build-Time Storage optional machen (nur für Performance-Optimierung)
|
||||
|
||||
## Testing-Strategie
|
||||
|
||||
1. Unit-Tests für `DiscoveryCacheManager::isStale()`
|
||||
2. Integration-Tests für Discovery-Registry-Loading
|
||||
3. E2E-Tests für Provider-Resolution
|
||||
4. Performance-Tests für Cache-Hit-Rate
|
||||
|
||||
## Implementierte Änderungen
|
||||
|
||||
### 1. isStale() Prüfung korrigiert ✅
|
||||
- **Datei**: `src/Framework/Discovery/Storage/DiscoveryCacheManager.php`
|
||||
- **Änderung**: `isStale()` verwendet jetzt Cache-Erstellungszeit statt Request-Zeit
|
||||
- **Implementierung**: `startTime` wird beim Speichern im Cache gespeichert und beim Laden verwendet
|
||||
|
||||
### 2. DiscoveryRegistryInitializer entfernt ✅
|
||||
- **Datei**: `src/Framework/Discovery/DiscoveryRegistryInitializer.php` (entfernt)
|
||||
- **Änderung**: Initializer wurde vollständig entfernt, da er nur noch als No-Op fungierte
|
||||
- **Implementierung**: `DiscoveryServiceBootstrapper` ist jetzt die einzige Quelle für Discovery-Registry
|
||||
|
||||
### 3. DiscoveryServiceBootstrapper verbessert ✅
|
||||
- **Datei**: `src/Framework/Discovery/DiscoveryServiceBootstrapper.php`
|
||||
- **Änderung**: Prüft ob existierende Registry stale ist, bevor Discovery durchgeführt wird
|
||||
- **Implementierung**: `isRegistryStale()` Methode hinzugefügt
|
||||
|
||||
### 4. Cache-Struktur Migration ✅
|
||||
- **Datei**: `src/Framework/Discovery/Storage/DiscoveryCacheManager.php`
|
||||
- **Änderung**: Automatische Migration von altem Cache-Format zu neuem Format mit `startTime` und `version`
|
||||
- **Implementierung**: `upgradeCacheEntry()` und `validateCacheStructure()` Methoden hinzugefügt
|
||||
|
||||
### 5. Registry Versionierung ✅
|
||||
- **Datei**: `src/Framework/Discovery/Storage/DiscoveryCacheManager.php`
|
||||
- **Änderung**: Cache-Struktur erweitert um `version` Feld für Registry-Versionierung
|
||||
- **Implementierung**: `getRegistryVersion()` Methode erstellt Version basierend auf Registry-Content-Hash
|
||||
|
||||
### 6. Unified Discovery Storage ✅
|
||||
- **Datei**: `src/Framework/Discovery/Storage/DiscoveryCacheManager.php`
|
||||
- **Änderung**: Build-Time Storage als optionales Backend hinzugefügt
|
||||
- **Implementierung**: `tryGetFromBuildTimeStorage()` Methode prüft Build-Time Storage vor Runtime-Cache
|
||||
|
||||
### 7. Verbessertes Logging und Monitoring ✅
|
||||
- **Dateien**:
|
||||
- `src/Framework/Discovery/Storage/DiscoveryCacheManager.php`
|
||||
- `src/Framework/Discovery/Results/DiscoveryRegistry.php`
|
||||
- **Änderung**: Strukturiertes Logging, Metriken-Sammlung, Debug-Helpers hinzugefügt
|
||||
- **Implementierung**:
|
||||
- `getMetadata()` Methode in `DiscoveryRegistry`
|
||||
- `getSource()` Methode in `DiscoveryRegistry`
|
||||
- Erweiterte Logging-Statements mit Source-Informationen
|
||||
|
||||
## Weitere Refactoring-Vorschläge
|
||||
|
||||
### Refactoring 1: Einheitliche Cache-Struktur
|
||||
**Problem**: Cache-Struktur wurde geändert (Array mit `registry` und `startTime`), aber alte Caches haben noch das alte Format.
|
||||
|
||||
**Lösung**:
|
||||
- Migration-Script für alte Cache-Einträge
|
||||
- Oder: Automatische Erkennung und Konvertierung beim Laden
|
||||
|
||||
### Refactoring 2: DiscoveryRegistryInitializer entfernen
|
||||
**Problem**: Zwei Mechanismen für Discovery-Registry-Loading führen zu Konflikten.
|
||||
|
||||
**Lösung**:
|
||||
- `DiscoveryRegistryInitializer` vollständig entfernen
|
||||
- Oder: Nur als Fallback verwenden (bereits implementiert)
|
||||
- `DiscoveryServiceBootstrapper` als einzige Quelle
|
||||
|
||||
### Refactoring 3: Build-Time Storage optional machen
|
||||
**Problem**: Build-Time Storage (`storage/discovery/`) wird möglicherweise nicht mehr verwendet.
|
||||
|
||||
**Lösung**:
|
||||
- Build-Time Storage als Performance-Optimierung optional machen
|
||||
- Nur verwenden wenn verfügbar und aktuell
|
||||
- Runtime-Cache als primäre Quelle
|
||||
|
||||
### Refactoring 4: Discovery-Registry Versionierung
|
||||
**Problem**: Keine Möglichkeit, verschiedene Versionen der Registry zu vergleichen.
|
||||
|
||||
**Lösung**:
|
||||
- Jede Registry hat eine Version/Timestamp
|
||||
- Vergleich zwischen Build-Time und Runtime-Cache
|
||||
- Automatische Invalidation bei Versionsunterschieden
|
||||
|
||||
### Refactoring 5: Unified Discovery Storage
|
||||
**Problem**: Zwei verschiedene Storage-Mechanismen (Build-Time und Runtime).
|
||||
|
||||
**Lösung**:
|
||||
- Einheitliche Storage-Strategie
|
||||
- `DiscoveryCacheManager` verwendet `storage/discovery/` als Backend
|
||||
- Konsistente Cache-Invalidation
|
||||
|
||||
### Refactoring 6: Discovery-Registry Factory
|
||||
**Problem**: Discovery-Registry wird an mehreren Stellen erstellt.
|
||||
|
||||
**Lösung**:
|
||||
- Zentrale Factory für Discovery-Registry-Erstellung
|
||||
- Einheitliche Logik für alle Quellen
|
||||
- Klare Verantwortlichkeiten
|
||||
|
||||
### Refactoring 7: Verbessertes Logging
|
||||
**Problem**: Schwierig zu debuggen, welche Registry verwendet wird.
|
||||
|
||||
**Lösung**:
|
||||
- Besseres Logging für Discovery-Registry-Loading
|
||||
- Metriken für Cache-Hits/Misses
|
||||
- Warnungen bei Inkonsistenzen
|
||||
|
||||
131
docs/refactoring/placeholder-protection-refactoring.md
Normal file
131
docs/refactoring/placeholder-protection-refactoring.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# Placeholder Protection Refactoring
|
||||
|
||||
## Problem
|
||||
|
||||
Die Navigation-Links enthalten `PLACEHOLDER_0___` statt echte URLs. Der Parser schützt Placeholders während des Attribut-Parsings, aber sie werden nicht korrekt zurückgesetzt oder verarbeitet.
|
||||
|
||||
## Root Cause Analysis
|
||||
|
||||
1. **HtmlParser::parseAttributes** schützt Placeholders (`___PLACEHOLDER_X___`) während des Regex-Parsings
|
||||
2. Placeholders werden zurückgesetzt, bevor `setAttribute` aufgerufen wird
|
||||
3. Aber der `ForTransformer` findet sie nicht, weil:
|
||||
- Die Attribute werden HTML-encoded gespeichert
|
||||
- Der `ForTransformer` dekodiert sie mit `html_entity_decode`
|
||||
- Aber die Placeholders könnten bereits verarbeitet worden sein
|
||||
|
||||
## Refactoring-Vorschlag
|
||||
|
||||
### Option 1: Placeholder-Schutz entfernen (Einfachste Lösung)
|
||||
|
||||
**Problem**: Der Regex für Attribut-Parsing bricht bei geschachtelten Anführungszeichen in Placeholders.
|
||||
|
||||
**Lösung**: Statt Placeholders zu schützen, den Regex robuster machen, um geschachtelte Anführungszeichen zu handhaben.
|
||||
|
||||
**Vorteile**:
|
||||
- Einfacher Code
|
||||
- Keine Placeholder-Wiederherstellung nötig
|
||||
- Weniger Fehlerquellen
|
||||
|
||||
**Nachteile**:
|
||||
- Komplexerer Regex erforderlich
|
||||
|
||||
### Option 2: Placeholder-Schutz verbessern (Empfohlen)
|
||||
|
||||
**Problem**: Der aktuelle Schutz-Mechanismus ist fehleranfällig.
|
||||
|
||||
**Lösung**:
|
||||
1. Placeholders mit eindeutigen IDs schützen (bereits implementiert)
|
||||
2. Placeholders IMMER zurückersetzen, auch wenn sie nicht im Attribut-Wert gefunden werden
|
||||
3. Validierung hinzufügen, um sicherzustellen, dass alle geschützten Placeholders zurückgesetzt wurden
|
||||
|
||||
**Vorteile**:
|
||||
- Robuster
|
||||
- Bessere Fehlerbehandlung
|
||||
- Validierung verhindert Bugs
|
||||
|
||||
**Nachteile**:
|
||||
- Mehr Code-Komplexität
|
||||
|
||||
### Option 3: Placeholder-Verarbeitung in ForTransformer verbessern
|
||||
|
||||
**Problem**: Der `ForTransformer` erkennt geschützte Placeholders nicht korrekt.
|
||||
|
||||
**Lösung**:
|
||||
1. Geschützte Placeholders erkennen und verarbeiten
|
||||
2. Fallback-Mechanismus, falls HtmlParser sie nicht zurückgesetzt hat
|
||||
3. Besseres Logging für Debugging
|
||||
|
||||
**Vorteile**:
|
||||
- Funktioniert auch wenn HtmlParser fehlschlägt
|
||||
- Bessere Fehlerbehandlung
|
||||
|
||||
**Nachteile**:
|
||||
- Dupliziert Logik zwischen HtmlParser und ForTransformer
|
||||
|
||||
## Empfohlene Lösung
|
||||
|
||||
**Kombination aus Option 2 und 3**:
|
||||
|
||||
1. **HtmlParser verbessern**: Validierung hinzufügen, um sicherzustellen, dass alle geschützten Placeholders zurückgesetzt wurden
|
||||
2. **ForTransformer verbessern**: Fallback-Mechanismus für geschützte Placeholders
|
||||
3. **Besseres Logging**: Umfassendes Logging für Debugging
|
||||
|
||||
## Implementierung
|
||||
|
||||
### Schritt 1: HtmlParser::parseAttributes verbessern
|
||||
|
||||
```php
|
||||
private function parseAttributes(ElementNode $element, string $attributesString): void
|
||||
{
|
||||
// ... existing code ...
|
||||
|
||||
// After restoring placeholders, validate that all were restored
|
||||
foreach ($placeholders as $key => $placeholder) {
|
||||
if (str_contains($value, $key)) {
|
||||
throw new \RuntimeException("Placeholder {$key} was not restored in attribute {$name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Schritt 2: ForTransformer::processPlaceholdersInAllNodes verbessern
|
||||
|
||||
```php
|
||||
// If we find protected placeholders, try to restore them from context
|
||||
if (preg_match('/___PLACEHOLDER_(\d+)___/', $decodedValue, $matches)) {
|
||||
// This is a fallback - HtmlParser should have restored it
|
||||
// But if it didn't, we can't process it
|
||||
if (getenv('APP_DEBUG') === 'true') {
|
||||
error_log("ForTransformer::processPlaceholdersInAllNodes: CRITICAL - Found protected placeholder in attribute '{$attrName}': {$decodedValue}");
|
||||
error_log("ForTransformer::processPlaceholdersInAllNodes: This is a bug - HtmlParser should have restored it");
|
||||
}
|
||||
// Skip this attribute - it's malformed
|
||||
continue;
|
||||
}
|
||||
```
|
||||
|
||||
### Schritt 3: Besseres Logging
|
||||
|
||||
```php
|
||||
// Add comprehensive logging at each step
|
||||
if (getenv('APP_DEBUG') === 'true') {
|
||||
error_log("HtmlParser::parseAttributes: Processing attribute '{$name}'");
|
||||
error_log("HtmlParser::parseAttributes: Original value: " . substr($value, 0, 100));
|
||||
error_log("HtmlParser::parseAttributes: Protected placeholders: " . count($placeholders));
|
||||
error_log("HtmlParser::parseAttributes: Restored value: " . substr($value, 0, 100));
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
1. Unit-Tests für HtmlParser::parseAttributes
|
||||
2. Unit-Tests für ForTransformer::processPlaceholdersInAllNodes
|
||||
3. Integration-Tests für den gesamten Flow
|
||||
4. Edge-Case-Tests für geschachtelte Anführungszeichen
|
||||
|
||||
## Migration
|
||||
|
||||
- Keine Breaking Changes
|
||||
- Bestehende Templates funktionieren weiterhin
|
||||
- Neue Validierung verhindert zukünftige Bugs
|
||||
|
||||
Reference in New Issue
Block a user