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

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

View File

@@ -24,6 +24,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)

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

213
docs/components/README.md Normal file
View 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.

View 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
View 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
View 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
View File

@@ -0,0 +1,633 @@
# Database Seeding
Das Seed-System ermöglicht die Initialisierung der Datenbank mit Standard-Daten, getrennt von Schema-Migrationen. Es bietet eine saubere Trennung zwischen Schema-Änderungen (Migrations) und Initial-Daten (Seeds).
## Übersicht
**Migrations** sind für Schema-Änderungen (CREATE TABLE, ALTER TABLE, etc.)
**Seeds** sind für Initial-Daten (Standard-Content-Types, Default-Rollen, etc.)
Seeds sind idempotent - sie können mehrfach ausgeführt werden ohne Duplikate zu erstellen. Das System trackt ausgeführte Seeds in einer `seeds` Tabelle.
## Architektur
Das Seed-System besteht aus folgenden Komponenten:
### Core-Komponenten
- **`Seeder` Interface** - Basis-Interface für alle Seeder-Klassen
- **`SeedLoader`** - Lädt Seeder-Klassen über das Discovery-System
- **`SeedRunner`** - Führt Seeder aus und verwaltet Idempotenz
- **`SeedRepository`** - Verwaltet die `seeds` Tabelle für Tracking
- **`SeedCommand`** - Console Command für manuelle Ausführung
### Discovery-Integration
Das Seed-System nutzt das Framework Discovery-System zur automatischen Erkennung von Seeder-Klassen. Alle Klassen, die das `Seeder` Interface implementieren, werden automatisch gefunden und können ausgeführt werden.
### Seeds Tabelle
Die `seeds` Tabelle wird durch die Migration `CreateSeedsTable` erstellt und trackt alle ausgeführten Seeders:
- `id` (VARCHAR, PRIMARY KEY) - Eindeutige ID (SHA256 Hash des Seeder-Namens)
- `name` (VARCHAR, UNIQUE) - Seeder-Name
- `description` (TEXT) - Beschreibung des Seeders
- `executed_at` (TIMESTAMP) - Zeitpunkt der Ausführung
Diese Tabelle verhindert, dass Seeders mehrfach ausgeführt werden.
## Installation
### 1. Migration ausführen
Die `seeds` Tabelle wird automatisch erstellt, wenn die Migration `CreateSeedsTable` ausgeführt wird:
```bash
php console.php db:migrate
```
Die Migration ist Teil des Framework-Seed-Systems und wird automatisch erkannt.
### 2. Discovery-Cache aktualisieren
Nach dem Erstellen neuer Seeder-Klassen sollte der Discovery-Cache geleert werden:
```bash
php console.php cache:clear
```
## Verwendung
### Alle Seeders ausführen
Führt alle gefundenen Seeder aus, die noch nicht ausgeführt wurden:
```bash
php console.php db:seed
```
**Ausgabe:**
```
Running all seeders...
Found 1 seeder(s):
- DefaultContentTypesSeeder: Seeds default CMS content types (page, post, landing_page)
[info] Running seeder 'DefaultContentTypesSeeder'...
[info] Seeder 'DefaultContentTypesSeeder' completed successfully
✅ All seeders completed.
```
### Spezifischen Seeder ausführen
Führt nur einen bestimmten Seeder aus:
```bash
php console.php db:seed --class=DefaultContentTypesSeeder
```
**Ausgabe:**
```
Running seeder: DefaultContentTypesSeeder
[info] Running seeder 'DefaultContentTypesSeeder'...
[info] Seeder 'DefaultContentTypesSeeder' completed successfully
✅ Seeder 'DefaultContentTypesSeeder' completed.
```
### Alle Seeds neu ausführen (--fresh)
Löscht die `seeds` Tabelle und führt alle Seeders neu aus:
```bash
php console.php db:seed --fresh
```
**Warnung:** Die `--fresh` Option sollte nur in Development-Umgebungen verwendet werden.
**Ausgabe:**
```
⚠️ Clearing seeds table (--fresh option)...
✅ Seeds table cleared.
Running all seeders...
...
```
## Seeder erstellen
### 1. Seeder-Klasse erstellen
Erstelle eine Klasse, die das `Seeder` Interface implementiert:
```php
<?php
declare(strict_types=1);
namespace App\Domain\YourDomain\Seeds;
use App\Framework\Database\Seed\Seeder;
use App\Domain\YourDomain\Services\YourService;
final readonly class YourSeeder implements Seeder
{
public function __construct(
private YourService $yourService
) {}
public function seed(): void
{
// Your seeding logic here
// Should be idempotent - check if data exists before creating
}
public function getName(): string
{
return 'YourSeeder';
}
public function getDescription(): string
{
return 'Description of what this seeder does';
}
}
```
### 2. Idempotenz sicherstellen
Seeds sollten idempotent sein - sie sollten prüfen, ob Daten bereits existieren, bevor sie erstellt werden:
**Beispiel 1: Mit Exception-Handling**
```php
public function seed(): void
{
try {
$this->service->findBySlug('example');
// Skip if already exists
} catch (\RuntimeException $e) {
// Create if not exists
$this->service->create(
name: 'Example',
slug: 'example',
description: 'Example description'
);
}
}
```
**Beispiel 2: Mit expliziter Existenz-Prüfung**
```php
public function seed(): void
{
if (!$this->repository->exists(ExampleId::fromString('example'))) {
$this->service->create(...);
}
}
```
**Beispiel 3: Mit Config-Datei**
```php
public function seed(): void
{
$configPath = $this->pathProvider->getBasePath()
->join('config', 'your-domain', 'default-data.php');
$defaultData = require $configPath->toString();
foreach ($defaultData as $slug => $data) {
try {
$this->service->findBySlug($slug);
// Skip if already exists
} catch (\RuntimeException $e) {
$this->service->create(
name: $data['name'],
slug: $slug,
description: $data['description'] ?? null
);
}
}
}
```
### 3. Verzeichnis-Struktur
Seeder sollten im `Seeds` Verzeichnis der jeweiligen Domain liegen:
```
src/Domain/
├── Cms/
│ └── Seeds/
│ └── DefaultContentTypesSeeder.php
└── User/
└── Seeds/
└── DefaultRolesSeeder.php
```
Das Discovery-System findet automatisch alle Seeder-Klassen, die das `Seeder` Interface implementieren.
### 4. Dependency Injection
Seeder können alle Framework-Services über Dependency Injection verwenden:
```php
final readonly class YourSeeder implements Seeder
{
public function __construct(
private YourService $yourService,
private PathProvider $pathProvider,
private Clock $clock,
private Logger $logger
) {}
// ...
}
```
Der DI Container stellt automatisch alle benötigten Dependencies bereit.
## Standard-Content-Types
Das CMS-System stellt Standard-Content-Types über den `DefaultContentTypesSeeder` bereit:
- **`page`** - Standard-Seiten (z.B. "Über uns", "Impressum", "Kontakt")
- **`post`** - Blog-Artikel/News
- **`landing_page`** - Marketing-Landing-Pages
Diese werden automatisch erstellt, wenn der Seeder ausgeführt wird. Die Definitionen können in `config/cms/default-content-types.php` angepasst werden.
### Config-Datei anpassen
Die Standard-Content-Types werden in `config/cms/default-content-types.php` definiert:
```php
<?php
declare(strict_types=1);
return [
'page' => [
'name' => 'Page',
'description' => 'Standard pages for general content',
'isSystem' => true,
],
'post' => [
'name' => 'Post',
'description' => 'Blog posts and news articles',
'isSystem' => true,
],
'landing_page' => [
'name' => 'Landing Page',
'description' => 'Marketing landing pages for campaigns',
'isSystem' => true,
],
];
```
Du kannst diese Datei anpassen, um weitere Content-Types hinzuzufügen oder bestehende zu ändern. Nach Änderungen führe `db:seed --fresh` aus, um die Seeds neu auszuführen.
## Seeds Tabelle
Das Seed-System verwendet eine `seeds` Tabelle, um zu tracken, welche Seeders bereits ausgeführt wurden:
### Schema
```sql
CREATE TABLE seeds (
id VARCHAR(255) PRIMARY KEY,
name VARCHAR(255) UNIQUE NOT NULL,
description TEXT,
executed_at TIMESTAMP NOT NULL
);
CREATE INDEX idx_seeds_name ON seeds(name);
```
### Spalten
- **`id`** (VARCHAR, PRIMARY KEY) - Eindeutige ID (SHA256 Hash des Seeder-Namens)
- **`name`** (VARCHAR, UNIQUE) - Seeder-Name (muss eindeutig sein)
- **`description`** (TEXT) - Beschreibung des Seeders
- **`executed_at`** (TIMESTAMP) - Zeitpunkt der Ausführung
### Verwendung
Die Tabelle wird automatisch erstellt, wenn die Migration `CreateSeedsTable` ausgeführt wird. Der `SeedRepository` verwaltet diese Tabelle automatisch:
- `hasRun(string $name): bool` - Prüft, ob ein Seeder bereits ausgeführt wurde
- `markAsRun(string $name, string $description): void` - Markiert einen Seeder als ausgeführt
- `clearAll(): void` - Löscht alle Einträge (für `--fresh` Option)
## Best Practices
### 1. Idempotenz
Seeds sollten immer idempotent sein - prüfe auf Existenz vor dem Erstellen:
```php
public function seed(): void
{
// ✅ GUT: Prüft Existenz vor Erstellung
try {
$this->service->findBySlug('example');
return; // Skip if exists
} catch (\RuntimeException $e) {
$this->service->create(...);
}
// ❌ SCHLECHT: Erstellt ohne Prüfung
$this->service->create(...); // Kann Duplikate erzeugen
}
```
### 2. Config-Dateien
Verwende Config-Dateien für anpassbare Seed-Daten:
```php
// config/your-domain/default-data.php
return [
'example' => [
'name' => 'Example',
'description' => 'Example description',
'isSystem' => true,
],
];
```
Vorteile:
- Einfache Anpassung ohne Code-Änderung
- Versionierbar in Git
- Kann in verschiedenen Umgebungen unterschiedlich sein
### 3. System-Daten
Markiere System-Daten mit `isSystem: true` um sie vor Löschung zu schützen:
```php
$this->service->create(
name: 'System Role',
slug: 'admin',
isSystem: true // Kann nicht gelöscht werden
);
```
### 4. Fehlerbehandlung
Seeds sollten aussagekräftige Fehlermeldungen werfen:
```php
public function seed(): void
{
try {
$configPath = $this->pathProvider->getBasePath()
->join('config', 'domain', 'data.php');
if (!file_exists($configPath->toString())) {
throw new \RuntimeException(
"Config file not found: {$configPath->toString()}"
);
}
// ... seeding logic
} catch (\Throwable $e) {
throw new \RuntimeException(
"Failed to seed data: {$e->getMessage()}",
0,
$e
);
}
}
```
### 5. Dokumentation
Dokumentiere, was jeder Seeder macht:
```php
/**
* Seeds default user roles for the application
*
* Creates the following roles:
* - admin: Full system access
* - editor: Content editing access
* - viewer: Read-only access
*/
final readonly class DefaultRolesSeeder implements Seeder
{
// ...
}
```
### 6. Abhängigkeiten zwischen Seeders
Wenn Seeders voneinander abhängen, stelle sicher, dass sie in der richtigen Reihenfolge ausgeführt werden:
```php
// Seeder 1: Muss zuerst ausgeführt werden
final readonly class DefaultRolesSeeder implements Seeder
{
public function getName(): string
{
return 'DefaultRolesSeeder'; // Wird alphabetisch zuerst ausgeführt
}
}
// Seeder 2: Kann danach ausgeführt werden
final readonly class DefaultUsersSeeder implements Seeder
{
public function getName(): string
{
return 'DefaultUsersSeeder'; // Wird nach DefaultRolesSeeder ausgeführt
}
}
```
**Hinweis:** Seeders werden alphabetisch nach Namen sortiert. Verwende Präfixe für die Reihenfolge, falls nötig (z.B. `01_DefaultRolesSeeder`, `02_DefaultUsersSeeder`).
## Migration vs. Seed
| Migration | Seed |
|-----------|------|
| Schema-Änderungen | Initial-Daten |
| Forward-only (optional rollback) | Idempotent (kein rollback) |
| Versioniert | Getrackt in `seeds` Tabelle |
| Automatisch beim `db:migrate` | Manuell mit `db:seed` |
## Beispiele
### Beispiel: Default Roles Seeder
```php
<?php
declare(strict_types=1);
namespace App\Domain\User\Seeds;
use App\Domain\User\Services\RoleService;
use App\Framework\Database\Seed\Seeder;
final readonly class DefaultRolesSeeder implements Seeder
{
public function __construct(
private RoleService $roleService
) {}
public function seed(): void
{
$roles = [
['name' => 'Admin', 'slug' => 'admin', 'permissions' => ['*']],
['name' => 'Editor', 'slug' => 'editor', 'permissions' => ['content.edit']],
['name' => 'Viewer', 'slug' => 'viewer', 'permissions' => ['content.view']],
];
foreach ($roles as $roleData) {
try {
$this->roleService->findBySlug($roleData['slug']);
// Skip if already exists
} catch (\RuntimeException $e) {
$this->roleService->create(
name: $roleData['name'],
slug: $roleData['slug'],
permissions: $roleData['permissions']
);
}
}
}
public function getName(): string
{
return 'DefaultRolesSeeder';
}
public function getDescription(): string
{
return 'Seeds default user roles (admin, editor, viewer)';
}
}
```
### Beispiel: Seeder mit Config-Datei
```php
<?php
declare(strict_types=1);
namespace App\Domain\Settings\Seeds;
use App\Domain\Settings\Services\SettingService;
use App\Framework\Core\PathProvider;
use App\Framework\Database\Seed\Seeder;
final readonly class DefaultSettingsSeeder implements Seeder
{
public function __construct(
private SettingService $settingService,
private PathProvider $pathProvider
) {}
public function seed(): void
{
$configPath = $this->pathProvider->getBasePath()
->join('config', 'settings', 'defaults.php');
if (!file_exists($configPath->toString())) {
throw new \RuntimeException(
"Config file not found: {$configPath->toString()}"
);
}
$defaultSettings = require $configPath->toString();
foreach ($defaultSettings as $key => $value) {
if (!$this->settingService->exists($key)) {
$this->settingService->create($key, $value);
}
}
}
public function getName(): string
{
return 'DefaultSettingsSeeder';
}
public function getDescription(): string
{
return 'Seeds default application settings';
}
}
```
## Troubleshooting
### Seeder wird nicht gefunden
**Problem:** Der Seeder wird nicht vom Discovery-System gefunden.
**Lösung:**
1. Stelle sicher, dass die Klasse das `Seeder` Interface implementiert
2. Stelle sicher, dass die Klasse im `Seeds` Verzeichnis liegt
3. Leere den Discovery-Cache: `php console.php cache:clear`
4. Prüfe, ob das `Seeder` Interface in den `targetInterfaces` der Discovery-Konfiguration ist
**Debug:**
```bash
# Prüfe, ob Seeder gefunden werden
php console.php db:seed
# Sollte "Found X seeder(s):" anzeigen
```
### Seeder wird mehrfach ausgeführt
**Problem:** Der Seeder wird trotz Tracking mehrfach ausgeführt.
**Lösung:**
1. Prüfe, ob die `seeds` Tabelle existiert: `php console.php db:status | grep seeds`
2. Prüfe, ob der Seeder-Name korrekt ist (muss eindeutig sein)
3. Prüfe die `seeds` Tabelle direkt:
```sql
SELECT * FROM seeds WHERE name = 'YourSeeder';
```
### SQL Syntax Error (ON DUPLICATE KEY UPDATE)
**Problem:** `SQLSTATE[42601]: Syntax error: syntax error at or near "DUPLICATE"`
**Ursache:** Die Datenbank ist PostgreSQL, aber der Code verwendet MySQL-Syntax.
**Lösung:** Verwende PostgreSQL-Syntax (`ON CONFLICT`) statt MySQL-Syntax (`ON DUPLICATE KEY UPDATE`):
```php
// ❌ MySQL-Syntax (funktioniert nicht mit PostgreSQL)
INSERT INTO table (...) VALUES (...)
ON DUPLICATE KEY UPDATE ...
// ✅ PostgreSQL-Syntax
INSERT INTO table (...) VALUES (...)
ON CONFLICT (id) DO UPDATE SET ...
```
### Seeder schlägt fehl
**Problem:** Der Seeder wirft eine Exception.
**Lösung:**
1. Prüfe die Fehlermeldung in der Console-Ausgabe
2. Stelle sicher, dass alle Dependencies verfügbar sind
3. Prüfe, ob Config-Dateien existieren und korrekt formatiert sind
4. Verwende `--fresh` um alle Seeds neu auszuführen (nur Development)
### Migration für seeds Tabelle fehlt
**Problem:** `SQLSTATE[42P01]: Undefined table: relation "seeds" does not exist`
**Lösung:**
1. Führe die Migration aus: `php console.php db:migrate`
2. Prüfe, ob die Migration `CreateSeedsTable` existiert
3. Prüfe den Migrations-Status: `php console.php db:status`

View 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

View 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.

View 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);
```

View 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/)

View 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)

View 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)

View 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)

View 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

View File

@@ -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>

View 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

View File

@@ -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

View 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)

View 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

View 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

View File

@@ -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)
);
}
```

View 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

View 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