Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
- Remove middleware reference from Gitea Traefik labels (caused routing issues) - Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s) - Add explicit service reference in Traefik labels - Fix intermittent 504 timeouts by improving PostgreSQL connection handling Fixes Gitea unreachability via git.michaelschiemer.de
1128 lines
28 KiB
Markdown
1128 lines
28 KiB
Markdown
# LiveComponents Slot System
|
|
|
|
Umfassende Dokumentation des Slot Systems für flexible Component-Komposition im Custom PHP Framework.
|
|
|
|
## Übersicht
|
|
|
|
Das Slot System ermöglicht es Components, flexible Bereiche zu definieren, in die Parent Components ihren eigenen Content einfügen können - ähnlich wie **Vue's Slots** oder **React's children/render props** Pattern.
|
|
|
|
**Key Features:**
|
|
- ✅ **Named Slots** - Spezifische Placement Points (header, footer, sidebar)
|
|
- ✅ **Default Slots** - Unnamed slot für allgemeinen Content
|
|
- ✅ **Scoped Slots** - Slots mit Zugriff auf Component-Daten via Context
|
|
- ✅ **Required Slots** - Validation dass notwendige Slots gefüllt sind
|
|
- ✅ **Default Content** - Fallback-Content wenn Slot nicht gefüllt wird
|
|
- ✅ **Content Processing** - Automatische Wrapper und Transformationen
|
|
- ✅ **Custom Validation** - Component-spezifische Slot-Validierung
|
|
- ✅ **XSS Protection** - Automatisches HTML-Escaping in Context-Values
|
|
|
|
## Architektur
|
|
|
|
```
|
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
│ SlotDefinition │───▶│ SlotManager │───▶│ SlotProcessor │
|
|
│ (What exists) │ │ (Resolution) │ │ (Rendering) │
|
|
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
|
│ │ │
|
|
SlotContent SupportsSlots DomProcessor
|
|
(Provided) (Component) (Template)
|
|
│ │ │
|
|
SlotContext ──────────────────────────────────────────┘
|
|
(Scoped Data)
|
|
```
|
|
|
|
## Kern-Komponenten
|
|
|
|
### 1. SlotDefinition - Slot-Definition Value Object
|
|
|
|
Definiert welche Slots in einer Component verfügbar sind.
|
|
|
|
```php
|
|
final readonly class SlotDefinition
|
|
{
|
|
public function __construct(
|
|
public string $name, // Slot name (z.B. 'header', 'footer')
|
|
public string $defaultContent = '', // Fallback content
|
|
public array $props = [], // Props für scoped slots
|
|
public bool $required = false // Pflicht-Slot?
|
|
) {}
|
|
}
|
|
```
|
|
|
|
**Factory Methods:**
|
|
|
|
```php
|
|
// Default (unnamed) Slot
|
|
SlotDefinition::default('<p>Default content</p>');
|
|
|
|
// Named Slot
|
|
SlotDefinition::named('header', '<h1>Default Header</h1>');
|
|
|
|
// Scoped Slot mit Props
|
|
SlotDefinition::scoped('content', ['userId', 'userName'], '<p>Default</p>');
|
|
|
|
// Required Slot
|
|
SlotDefinition::named('body')->withRequired(true);
|
|
```
|
|
|
|
**Beispiel:**
|
|
|
|
```php
|
|
public function getSlotDefinitions(): array
|
|
{
|
|
return [
|
|
SlotDefinition::named('header', '<h2>Default Header</h2>'),
|
|
SlotDefinition::named('body')->withRequired(true),
|
|
SlotDefinition::scoped('footer', ['closeFunction'], '<button onclick="{closeFunction}">Close</button>'),
|
|
];
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 2. SlotContent - Slot-Content Value Object
|
|
|
|
Repräsentiert den Content, der einen Slot füllt.
|
|
|
|
```php
|
|
final readonly class SlotContent
|
|
{
|
|
public function __construct(
|
|
public string $slotName, // Name des Slots
|
|
public string $content, // HTML/Text content
|
|
public array $data = [], // Data für scoped slots
|
|
public ?ComponentId $componentId = null // Optional: Component reference
|
|
) {}
|
|
}
|
|
```
|
|
|
|
**Factory Methods:**
|
|
|
|
```php
|
|
// Default slot content
|
|
SlotContent::default('<p>Main content</p>');
|
|
|
|
// Named slot content
|
|
SlotContent::named('header', '<h1>Custom Header</h1>');
|
|
|
|
// Named slot with data (for scoped slots)
|
|
SlotContent::named('content', '<p>{userName}</p>', ['userName' => 'John']);
|
|
|
|
// Content from component
|
|
SlotContent::fromComponent('header', $componentId, '<h1>Header</h1>');
|
|
```
|
|
|
|
**Hilfsmethoden:**
|
|
|
|
```php
|
|
$content = SlotContent::named('header', '<h1>Header</h1>');
|
|
|
|
$content->isDefault(); // false
|
|
$content->isEmpty(); // false
|
|
$content->hasData(); // false
|
|
$content->isFromComponent(); // false
|
|
|
|
// Content transformieren
|
|
$updated = $content->withContent('<h2>New Header</h2>');
|
|
$withData = $content->withData(['key' => 'value']);
|
|
```
|
|
|
|
---
|
|
|
|
### 3. SlotContext - Scoped Slot Context
|
|
|
|
Provides context/data to scoped slots, ermöglicht Parent Zugriff auf Child-Component-Daten.
|
|
|
|
```php
|
|
final readonly class SlotContext
|
|
{
|
|
public function __construct(
|
|
public array $data = [], // Context data
|
|
public array $metadata = [] // Additional metadata
|
|
) {}
|
|
}
|
|
```
|
|
|
|
**Factory Methods:**
|
|
|
|
```php
|
|
// Empty context
|
|
SlotContext::empty();
|
|
|
|
// Context with data
|
|
SlotContext::create([
|
|
'userId' => 123,
|
|
'userName' => 'John Doe',
|
|
'isAdmin' => true,
|
|
]);
|
|
|
|
// Context with metadata
|
|
SlotContext::create(
|
|
data: ['userId' => 123],
|
|
metadata: ['timestamp' => time()]
|
|
);
|
|
```
|
|
|
|
**Fluent API:**
|
|
|
|
```php
|
|
$context = SlotContext::empty()
|
|
->with('userId', 123)
|
|
->with('userName', 'John Doe')
|
|
->withData(['role' => 'admin'])
|
|
->withMetadata(['version' => '1.0']);
|
|
|
|
// Zugriff
|
|
$userId = $context->get('userId'); // 123
|
|
$role = $context->get('role', 'guest'); // 'admin'
|
|
$hasUser = $context->has('userId'); // true
|
|
|
|
// Manipulation
|
|
$updated = $context->without('role');
|
|
$merged = $context1->merge($context2);
|
|
```
|
|
|
|
---
|
|
|
|
### 4. SupportsSlots Interface
|
|
|
|
Components die Slots unterstützen müssen dieses Interface implementieren.
|
|
|
|
```php
|
|
interface SupportsSlots
|
|
{
|
|
/**
|
|
* Get slot definitions for this component
|
|
* @return array<SlotDefinition>
|
|
*/
|
|
public function getSlotDefinitions(): array;
|
|
|
|
/**
|
|
* Get context data for a specific slot (scoped slots)
|
|
*/
|
|
public function getSlotContext(string $slotName): SlotContext;
|
|
|
|
/**
|
|
* Process slot content before rendering
|
|
*/
|
|
public function processSlotContent(SlotContent $content): SlotContent;
|
|
|
|
/**
|
|
* Validate that required slots are filled
|
|
* @return array<string> Validation errors (empty if valid)
|
|
*/
|
|
public function validateSlots(array $providedSlots): array;
|
|
}
|
|
```
|
|
|
|
**Implementierung:**
|
|
|
|
```php
|
|
final readonly class CardComponent implements LiveComponent, SupportsSlots
|
|
{
|
|
public function getSlotDefinitions(): array
|
|
{
|
|
return [
|
|
SlotDefinition::named('header', '<div class="card-header-default">Card Header</div>'),
|
|
SlotDefinition::named('body')->withRequired(true),
|
|
SlotDefinition::named('footer', ''),
|
|
];
|
|
}
|
|
|
|
public function getSlotContext(string $slotName): SlotContext
|
|
{
|
|
// Card doesn't need scoped slots
|
|
return SlotContext::empty();
|
|
}
|
|
|
|
public function processSlotContent(SlotContent $content): SlotContent
|
|
{
|
|
// Apply card-specific CSS classes
|
|
$wrappedContent = match ($content->slotName) {
|
|
'header' => '<div class="card-header">' . $content->content . '</div>',
|
|
'body' => '<div class="card-body">' . $content->content . '</div>',
|
|
'footer' => '<div class="card-footer">' . $content->content . '</div>',
|
|
default => $content->content
|
|
};
|
|
|
|
return $content->withContent($wrappedContent);
|
|
}
|
|
|
|
public function validateSlots(array $providedSlots): array
|
|
{
|
|
$errors = [];
|
|
|
|
// Custom validation: warn if footer without header
|
|
$hasHeader = false;
|
|
$hasFooter = false;
|
|
|
|
foreach ($providedSlots as $slot) {
|
|
if ($slot->slotName === 'header') $hasHeader = true;
|
|
if ($slot->slotName === 'footer') $hasFooter = true;
|
|
}
|
|
|
|
if ($hasFooter && !$hasHeader) {
|
|
$errors[] = 'Card footer provided without header - consider adding a header';
|
|
}
|
|
|
|
return $errors;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 5. SlotManager - Core Slot Management
|
|
|
|
Zentrale Verwaltung von Slot-Resolution, Validation und Rendering.
|
|
|
|
```php
|
|
final class SlotManager
|
|
{
|
|
/**
|
|
* Register slot contents for a component
|
|
*/
|
|
public function registerSlotContents(ComponentId $componentId, array $contents): void;
|
|
|
|
/**
|
|
* Get registered slot contents for a component
|
|
* @return array<SlotContent>
|
|
*/
|
|
public function getSlotContents(ComponentId $componentId): array;
|
|
|
|
/**
|
|
* Resolve slot content for rendering
|
|
* Priority: Provided content > Default content
|
|
*/
|
|
public function resolveSlotContent(
|
|
SupportsSlots $component,
|
|
SlotDefinition $definition,
|
|
array $providedContents
|
|
): string;
|
|
|
|
/**
|
|
* Validate slots for a component
|
|
* @return array<string> Validation errors (empty if valid)
|
|
*/
|
|
public function validateSlots(SupportsSlots $component, array $providedContents): array;
|
|
|
|
/**
|
|
* Check if component has specific slot
|
|
*/
|
|
public function hasSlot(SupportsSlots $component, string $slotName): bool;
|
|
|
|
/**
|
|
* Get slot definition by name
|
|
*/
|
|
public function getSlotDefinition(SupportsSlots $component, string $slotName): ?SlotDefinition;
|
|
|
|
/**
|
|
* Clear all registered slot contents
|
|
*/
|
|
public function clear(): void;
|
|
|
|
/**
|
|
* Get statistics about registered slots
|
|
*/
|
|
public function getStats(): array;
|
|
}
|
|
```
|
|
|
|
**Resolution-Logik:**
|
|
|
|
1. **Find Provided Content** - Suche SlotContent für Slot-Name
|
|
2. **Process Content** - Führe `processSlotContent()` aus
|
|
3. **Inject Context** (bei scoped slots) - Ersetze `{context.key}` Placeholders
|
|
4. **Return Content** - Oder Default Content wenn nichts provided
|
|
|
|
**Context Injection:**
|
|
|
|
```php
|
|
// Scoped slot content
|
|
$content = '<p>User: {context.userName} (ID: {context.userId})</p>';
|
|
|
|
// Context data
|
|
$context = SlotContext::create([
|
|
'userId' => 123,
|
|
'userName' => 'John Doe',
|
|
]);
|
|
|
|
// After injection
|
|
// Result: '<p>User: John Doe (ID: 123)</p>'
|
|
```
|
|
|
|
---
|
|
|
|
### 6. SlotProcessor - Template Integration
|
|
|
|
DomProcessor für Template-Rendering mit Slot-Unterstützung.
|
|
|
|
```php
|
|
final readonly class SlotProcessor implements DomProcessor
|
|
{
|
|
public function __construct(
|
|
private SlotManager $slotManager
|
|
) {}
|
|
|
|
public function process(DomWrapper $dom, RenderContext $context): DomWrapper
|
|
{
|
|
// Check if component supports slots
|
|
if ($component instanceof SupportsSlots) {
|
|
return $this->processWithSlotSystem($dom, $context, $component);
|
|
}
|
|
|
|
// Fallback to legacy slot processing
|
|
return $this->processLegacySlots($dom, $context);
|
|
}
|
|
}
|
|
```
|
|
|
|
**Features:**
|
|
- ✅ Integration mit SlotManager
|
|
- ✅ Backward compatibility (legacy slots)
|
|
- ✅ Automatic slot validation
|
|
- ✅ Error handling (development vs production)
|
|
- ✅ Context injection für scoped slots
|
|
|
|
---
|
|
|
|
## Slot-Patterns
|
|
|
|
### Pattern 1: Named Slots - Basic Layout Composition
|
|
|
|
**Use Case:** Component mit mehreren spezifischen Bereichen (Header, Body, Footer).
|
|
|
|
**Component Definition:**
|
|
|
|
```php
|
|
final readonly class CardComponent implements SupportsSlots
|
|
{
|
|
public function getSlotDefinitions(): array
|
|
{
|
|
return [
|
|
SlotDefinition::named('header', '<div class="card-header-default">Default Header</div>'),
|
|
SlotDefinition::named('body')->withRequired(true),
|
|
SlotDefinition::named('footer', ''),
|
|
];
|
|
}
|
|
}
|
|
```
|
|
|
|
**Template:**
|
|
|
|
```html
|
|
<div class="card">
|
|
<slot name="header">
|
|
<div class="card-header-default">Default Header</div>
|
|
</slot>
|
|
|
|
<slot name="body"></slot>
|
|
|
|
<slot name="footer"></slot>
|
|
</div>
|
|
```
|
|
|
|
**Usage:**
|
|
|
|
```html
|
|
<component name="card" id="user-card">
|
|
<slot name="header">
|
|
<h2>User Profile</h2>
|
|
<p>John Doe</p>
|
|
</slot>
|
|
|
|
<slot name="body">
|
|
<p><strong>Email:</strong> john@example.com</p>
|
|
<p><strong>Role:</strong> Administrator</p>
|
|
</slot>
|
|
|
|
<slot name="footer">
|
|
<button>Edit Profile</button>
|
|
<button>View Activity</button>
|
|
</slot>
|
|
</component>
|
|
```
|
|
|
|
**Result:**
|
|
- ✅ Custom header mit User-Info
|
|
- ✅ Custom body mit Details
|
|
- ✅ Custom footer mit Actions
|
|
- ✅ Alle Slots mit CSS-Wrappern via `processSlotContent()`
|
|
|
|
---
|
|
|
|
### Pattern 2: Default Slot - Unnamed Content
|
|
|
|
**Use Case:** Container-Component die beliebigen Content aufnimmt.
|
|
|
|
**Component Definition:**
|
|
|
|
```php
|
|
final readonly class ContainerComponent implements SupportsSlots
|
|
{
|
|
public function getSlotDefinitions(): array
|
|
{
|
|
return [
|
|
SlotDefinition::default('<div class="empty-container">No content</div>'),
|
|
SlotDefinition::named('title'),
|
|
SlotDefinition::named('actions', ''),
|
|
];
|
|
}
|
|
}
|
|
```
|
|
|
|
**Template:**
|
|
|
|
```html
|
|
<div class="container">
|
|
<slot name="title">
|
|
<h2>Container</h2>
|
|
</slot>
|
|
|
|
<!-- Default slot: all unnamed content goes here -->
|
|
<slot>
|
|
<div class="empty-container">No content provided</div>
|
|
</slot>
|
|
|
|
<slot name="actions"></slot>
|
|
</div>
|
|
```
|
|
|
|
**Usage:**
|
|
|
|
```html
|
|
<component name="container" id="wrapper">
|
|
<slot name="title">
|
|
<h2>Welcome Container</h2>
|
|
</slot>
|
|
|
|
<!-- This goes into the default slot -->
|
|
<h1>Welcome to the Platform</h1>
|
|
<p>This is the main content.</p>
|
|
<p>All unnamed content appears in the default slot.</p>
|
|
|
|
<slot name="actions">
|
|
<button>Get Started</button>
|
|
<button>Learn More</button>
|
|
</slot>
|
|
</component>
|
|
```
|
|
|
|
**Result:**
|
|
- ✅ Named slots (title, actions) für spezifischen Content
|
|
- ✅ Default slot für flexiblen, unstrukturierten Content
|
|
- ✅ Fallback Content wenn Default Slot leer
|
|
|
|
---
|
|
|
|
### Pattern 3: Scoped Slots - Component Data Access
|
|
|
|
**Use Case:** Parent braucht Zugriff auf Child-Component-Daten (z.B. Modal ID, Close Function).
|
|
|
|
**Component Definition:**
|
|
|
|
```php
|
|
final readonly class ModalComponent implements SupportsSlots
|
|
{
|
|
public function getSlotDefinitions(): array
|
|
{
|
|
return [
|
|
SlotDefinition::named('title', '<h3>Modal Title</h3>'),
|
|
SlotDefinition::scoped('content', ['modalId', 'isOpen', 'closeFunction'])->withRequired(true),
|
|
SlotDefinition::scoped('actions', ['closeFunction', 'modalId']),
|
|
];
|
|
}
|
|
|
|
public function getSlotContext(string $slotName): SlotContext
|
|
{
|
|
$modalId = $this->id->toString();
|
|
$isOpen = $this->state->get('isOpen', false);
|
|
|
|
return match ($slotName) {
|
|
'content', 'actions' => SlotContext::create([
|
|
'modalId' => $modalId,
|
|
'isOpen' => $isOpen,
|
|
'closeFunction' => "closeModal('{$modalId}')",
|
|
]),
|
|
default => SlotContext::empty()
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
**Template:**
|
|
|
|
```html
|
|
<div class="modal" data-modal-id="{component.id}">
|
|
<slot name="title">
|
|
<h3>Modal Title</h3>
|
|
</slot>
|
|
|
|
<!-- Scoped slot: parent can access context -->
|
|
<slot name="content"></slot>
|
|
|
|
<!-- Scoped slot: parent can use closeFunction -->
|
|
<slot name="actions">
|
|
<button onclick="{context.closeFunction}">Close</button>
|
|
</slot>
|
|
</div>
|
|
```
|
|
|
|
**Usage:**
|
|
|
|
```html
|
|
<component name="modal" id="confirm-delete-modal">
|
|
<slot name="title">
|
|
<h3>Confirm Delete</h3>
|
|
</slot>
|
|
|
|
<slot name="content">
|
|
<p>Are you sure you want to delete this item?</p>
|
|
<p>This action cannot be undone.</p>
|
|
<!-- Access component data via {context.key} -->
|
|
<p><small>Modal ID: {context.modalId}</small></p>
|
|
</slot>
|
|
|
|
<slot name="actions">
|
|
<!-- Use component's close function -->
|
|
<button onclick="{context.closeFunction}">Cancel</button>
|
|
<button class="btn-danger">Delete</button>
|
|
</slot>
|
|
</component>
|
|
```
|
|
|
|
**Result:**
|
|
- ✅ Parent-Slot-Content hat Zugriff auf Child-Component-Daten
|
|
- ✅ `{context.modalId}` wird durch echte Modal-ID ersetzt
|
|
- ✅ `{context.closeFunction}` wird durch `closeModal('modal-id')` ersetzt
|
|
- ✅ XSS-Protection: Alle Context-Values werden HTML-escaped
|
|
|
|
---
|
|
|
|
### Pattern 4: Complex Layout - Multi-Slot Composition
|
|
|
|
**Use Case:** Page-Layout mit mehreren Bereichen (Header, Sidebar, Main, Footer).
|
|
|
|
**Component Definition:**
|
|
|
|
```php
|
|
final readonly class LayoutComponent implements SupportsSlots
|
|
{
|
|
public function getSlotDefinitions(): array
|
|
{
|
|
return [
|
|
SlotDefinition::scoped('sidebar', ['sidebarWidth', 'isCollapsed'], '<aside>Default Sidebar</aside>'),
|
|
SlotDefinition::named('main')->withRequired(true),
|
|
SlotDefinition::named('footer', '<footer>Default Footer</footer>'),
|
|
SlotDefinition::named('header', ''),
|
|
];
|
|
}
|
|
|
|
public function getSlotContext(string $slotName): SlotContext
|
|
{
|
|
$sidebarWidth = $this->state->get('sidebarWidth', '250px');
|
|
$isCollapsed = $this->state->get('sidebarCollapsed', false);
|
|
|
|
return match ($slotName) {
|
|
'sidebar' => SlotContext::create([
|
|
'sidebarWidth' => $sidebarWidth,
|
|
'isCollapsed' => $isCollapsed,
|
|
]),
|
|
default => SlotContext::empty()
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
**Usage:**
|
|
|
|
```html
|
|
<component name="layout" id="dashboard-layout">
|
|
<slot name="header">
|
|
<header class="app-header">
|
|
<h1>My Application</h1>
|
|
<nav>...</nav>
|
|
</header>
|
|
</slot>
|
|
|
|
<slot name="sidebar">
|
|
<!-- Scoped: access layout dimensions -->
|
|
<aside style="width: {context.sidebarWidth}">
|
|
<nav class="sidebar-nav">
|
|
<ul>
|
|
<li><a href="/dashboard">Dashboard</a></li>
|
|
<li><a href="/users">Users</a></li>
|
|
<li><a href="/settings">Settings</a></li>
|
|
</ul>
|
|
</nav>
|
|
</aside>
|
|
</slot>
|
|
|
|
<slot name="main">
|
|
<main class="main-content">
|
|
<h2>Dashboard</h2>
|
|
<p>Welcome to your dashboard!</p>
|
|
</main>
|
|
</slot>
|
|
|
|
<slot name="footer">
|
|
<footer class="app-footer">
|
|
<p>© 2025 My Application</p>
|
|
</footer>
|
|
</slot>
|
|
</component>
|
|
```
|
|
|
|
---
|
|
|
|
## Slot Validation
|
|
|
|
### Required Slots
|
|
|
|
```php
|
|
SlotDefinition::named('body')->withRequired(true);
|
|
```
|
|
|
|
**Validation:**
|
|
- ✅ Automatische Prüfung via `SlotManager::validateSlots()`
|
|
- ✅ Fehler wenn Required Slot nicht gefüllt oder leer
|
|
- ✅ Validation-Errors als Array zurückgegeben
|
|
|
|
**Error Handling:**
|
|
|
|
```php
|
|
$errors = $slotManager->validateSlots($component, $providedSlots);
|
|
|
|
if (!empty($errors)) {
|
|
// Development: Show errors
|
|
// Production: Log errors
|
|
foreach ($errors as $error) {
|
|
error_log("Slot validation error: $error");
|
|
}
|
|
}
|
|
```
|
|
|
|
### Custom Validation
|
|
|
|
Components können eigene Validation-Logik in `validateSlots()` implementieren:
|
|
|
|
```php
|
|
public function validateSlots(array $providedSlots): array
|
|
{
|
|
$errors = [];
|
|
|
|
// Custom logic: warn if footer without header
|
|
$hasHeader = false;
|
|
$hasFooter = false;
|
|
|
|
foreach ($providedSlots as $slot) {
|
|
if ($slot->slotName === 'header') $hasHeader = true;
|
|
if ($slot->slotName === 'footer') $hasFooter = true;
|
|
}
|
|
|
|
if ($hasFooter && !$hasHeader) {
|
|
$errors[] = 'Card footer provided without header - visual consistency issue';
|
|
}
|
|
|
|
return $errors;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Content Processing
|
|
|
|
### Automatic Wrapping
|
|
|
|
Components können Slot-Content automatisch wrappen via `processSlotContent()`:
|
|
|
|
```php
|
|
public function processSlotContent(SlotContent $content): SlotContent
|
|
{
|
|
$wrappedContent = match ($content->slotName) {
|
|
'header' => '<div class="card-header">' . $content->content . '</div>',
|
|
'body' => '<div class="card-body">' . $content->content . '</div>',
|
|
'footer' => '<div class="card-footer">' . $content->content . '</div>',
|
|
default => $content->content
|
|
};
|
|
|
|
return $content->withContent($wrappedContent);
|
|
}
|
|
```
|
|
|
|
**Result:**
|
|
- ✅ Automatische CSS-Wrapper für jeden Slot
|
|
- ✅ Konsistentes Styling ohne manuelle Wrapper im Parent
|
|
- ✅ Component-spezifische Transformationen
|
|
|
|
### State-Based Processing
|
|
|
|
```php
|
|
public function processSlotContent(SlotContent $content): SlotContent
|
|
{
|
|
// Get padding from state
|
|
$padding = $this->state->get('padding', 'medium');
|
|
$paddingClass = 'container-padding-' . $padding;
|
|
|
|
$wrappedContent = match ($content->slotName) {
|
|
'default' => "<div class=\"container-content {$paddingClass}\">" . $content->content . '</div>',
|
|
default => $content->content
|
|
};
|
|
|
|
return $content->withContent($wrappedContent);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Security
|
|
|
|
### XSS Protection
|
|
|
|
**Automatic HTML Escaping** in scoped context values:
|
|
|
|
```php
|
|
private function formatValue(mixed $value): string
|
|
{
|
|
if (is_scalar($value)) {
|
|
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
|
}
|
|
|
|
if (is_array($value)) {
|
|
return htmlspecialchars(json_encode($value), ENT_QUOTES, 'UTF-8');
|
|
}
|
|
|
|
return '';
|
|
}
|
|
```
|
|
|
|
**Result:**
|
|
- ✅ Alle Context-Values werden automatisch escaped
|
|
- ✅ `<script>` → `<script>`
|
|
- ✅ Schutz vor XSS-Attacken via Scoped Slots
|
|
|
|
**Beispiel:**
|
|
|
|
```php
|
|
// Malicious context value
|
|
$context = SlotContext::create([
|
|
'userInput' => '<script>alert("XSS")</script>',
|
|
]);
|
|
|
|
// In slot content
|
|
'<p>{context.userInput}</p>'
|
|
|
|
// After injection (safe!)
|
|
'<p><script>alert("XSS")</script></p>'
|
|
```
|
|
|
|
---
|
|
|
|
## Best Practices
|
|
|
|
### 1. Slot Design
|
|
|
|
**✅ DO:**
|
|
- Klare Slot-Namen verwenden (`header`, `body`, `footer` statt `slot1`, `slot2`)
|
|
- Default Content für alle Optional Slots definieren
|
|
- Required Slots nur für essentiellen Content
|
|
- Scoped Slots für Component-Daten die Parent braucht
|
|
|
|
**❌ DON'T:**
|
|
- Zu viele Slots definieren (max. 5-6 für Übersichtlichkeit)
|
|
- Alle Slots als Required markieren
|
|
- Sensitive Daten in Scoped Context legen
|
|
- Komplexe Logik in `processSlotContent()`
|
|
|
|
### 2. Content Processing
|
|
|
|
**✅ DO:**
|
|
- Automatische Wrapper für konsistentes Styling
|
|
- State-basierte Transformationen (padding, themes)
|
|
- Slot-spezifische CSS-Klassen
|
|
- Immutable Content-Transformationen (`withContent()`)
|
|
|
|
**❌ DON'T:**
|
|
- Content manipulieren ohne `withContent()`
|
|
- Seiteneffekte in `processSlotContent()`
|
|
- Zu aggressive Transformationen
|
|
- Parent-Content überschreiben
|
|
|
|
### 3. Validation
|
|
|
|
**✅ DO:**
|
|
- Required Slots für kritischen Content
|
|
- Custom Validation für logische Konsistenz
|
|
- Hilfreiche Error-Messages
|
|
- Validation in Development zeigen, in Production loggen
|
|
|
|
**❌ DON'T:**
|
|
- Validation-Errors swallowing
|
|
- Generische Error-Messages
|
|
- Validation-Logic in Templates
|
|
- Performance-intensive Validation
|
|
|
|
### 4. Scoped Slots
|
|
|
|
**✅ DO:**
|
|
- Nur notwendige Daten in Context legen
|
|
- HTML-Escaping ist automatisch (vertrauen)
|
|
- Dokumentierte Context-Props in SlotDefinition
|
|
- Klare Naming Convention (`{context.key}`)
|
|
|
|
**❌ DON'T:**
|
|
- Sensitive Daten in Context (Passwords, Tokens)
|
|
- Zu viele Context-Props (max. 5-6)
|
|
- Komplexe Objekte in Context
|
|
- Manual HTML-Escaping (ist redundant)
|
|
|
|
---
|
|
|
|
## Testing
|
|
|
|
### Unit Tests - SlotManager
|
|
|
|
```php
|
|
it('resolves provided content over default content', function () {
|
|
$component = new TestComponent();
|
|
$definition = SlotDefinition::named('header', '<h2>Default</h2>');
|
|
$providedContent = [
|
|
SlotContent::named('header', '<h1>Custom</h1>'),
|
|
];
|
|
|
|
$result = $this->slotManager->resolveSlotContent(
|
|
$component,
|
|
$definition,
|
|
$providedContent
|
|
);
|
|
|
|
expect($result)->toBe('<h1>Custom</h1>');
|
|
});
|
|
```
|
|
|
|
### Integration Tests - Components
|
|
|
|
```php
|
|
it('renders CardComponent with custom slots', function () {
|
|
$component = new CardComponent(
|
|
id: ComponentId::generate(),
|
|
state: ComponentState::fromArray([])
|
|
);
|
|
|
|
$providedSlots = [
|
|
SlotContent::named('header', '<h2>User Profile</h2>'),
|
|
SlotContent::named('body', '<p>User details...</p>'),
|
|
];
|
|
|
|
$errors = $this->slotManager->validateSlots($component, $providedSlots);
|
|
expect($errors)->toBeEmpty();
|
|
});
|
|
```
|
|
|
|
### Template Tests
|
|
|
|
```php
|
|
it('processes slots in template', function () {
|
|
$html = '<component name="card">
|
|
<slot name="header"><h1>Header</h1></slot>
|
|
<slot name="body"><p>Body</p></slot>
|
|
</component>';
|
|
|
|
$result = $this->templateRenderer->render($html, [
|
|
'component' => new CardComponent(...),
|
|
]);
|
|
|
|
expect($result)->toContain('card-header');
|
|
expect($result)->toContain('card-body');
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### Problem: Slot Content wird nicht angezeigt
|
|
|
|
**Ursache:** Slot-Name stimmt nicht überein.
|
|
|
|
**Lösung:**
|
|
```php
|
|
// Check slot definitions
|
|
$slotNames = $this->slotManager->getSlotNames($component);
|
|
var_dump($slotNames); // ['header', 'body', 'footer']
|
|
|
|
// Verify provided content
|
|
foreach ($providedSlots as $slot) {
|
|
echo $slot->slotName; // Muss mit Definition übereinstimmen
|
|
}
|
|
```
|
|
|
|
### Problem: Scoped Context wird nicht injiziert
|
|
|
|
**Ursache:** Slot nicht als Scoped definiert.
|
|
|
|
**Lösung:**
|
|
```php
|
|
// ❌ WRONG: Named slot (no context injection)
|
|
SlotDefinition::named('content');
|
|
|
|
// ✅ CORRECT: Scoped slot with props
|
|
SlotDefinition::scoped('content', ['modalId', 'closeFunction']);
|
|
```
|
|
|
|
### Problem: XSS in Context Values
|
|
|
|
**Ursache:** Custom formatValue() ohne Escaping.
|
|
|
|
**Lösung:**
|
|
```php
|
|
// ❌ WRONG: No escaping
|
|
private function formatValue(mixed $value): string
|
|
{
|
|
return (string) $value; // XSS vulnerability!
|
|
}
|
|
|
|
// ✅ CORRECT: Automatic escaping
|
|
private function formatValue(mixed $value): string
|
|
{
|
|
if (is_scalar($value)) {
|
|
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
|
}
|
|
return '';
|
|
}
|
|
```
|
|
|
|
### Problem: Required Slot Validation schlägt fehl
|
|
|
|
**Ursache:** Slot ist empty oder nur Whitespace.
|
|
|
|
**Lösung:**
|
|
```php
|
|
// Check if slot is truly empty
|
|
$content = SlotContent::named('body', ' ');
|
|
if ($content->isEmpty()) {
|
|
// Will fail required validation
|
|
}
|
|
|
|
// ✅ Provide actual content
|
|
SlotContent::named('body', '<p>Content</p>');
|
|
```
|
|
|
|
---
|
|
|
|
## Performance
|
|
|
|
### Slot Resolution Performance
|
|
|
|
**Typical Performance:**
|
|
- Slot Resolution: < 1ms per slot
|
|
- Context Injection: < 0.5ms per placeholder
|
|
- Validation: < 2ms für 5-6 slots
|
|
- Total Overhead: < 5ms per component
|
|
|
|
**Optimizations:**
|
|
- ✅ Slot definitions gecached in Component
|
|
- ✅ Keine Reflection für Slot-Discovery
|
|
- ✅ Simple string replacement für Context
|
|
- ✅ Minimal regex usage
|
|
|
|
### Best Practices für Performance
|
|
|
|
**✅ DO:**
|
|
- Slot definitions im Constructor definieren
|
|
- Simple Context-Values (strings, ints)
|
|
- Cached Component-Instances
|
|
- Minimal Validation-Logic
|
|
|
|
**❌ DON'T:**
|
|
- Dynamic Slot-Definitions
|
|
- Complex Objects in Context
|
|
- Database-Queries in `getSlotContext()`
|
|
- Expensive Transformations in `processSlotContent()`
|
|
|
|
---
|
|
|
|
## Migration Guide
|
|
|
|
### Von Legacy Slots zu Slot System
|
|
|
|
**Before (Legacy):**
|
|
|
|
```php
|
|
final class OldCard
|
|
{
|
|
public function render(array $slots): string
|
|
{
|
|
$header = $slots['header'] ?? '<h2>Default</h2>';
|
|
$body = $slots['body'] ?? '';
|
|
|
|
return "<div class='card'>$header $body</div>";
|
|
}
|
|
}
|
|
```
|
|
|
|
**After (Slot System):**
|
|
|
|
```php
|
|
final readonly class NewCard implements SupportsSlots
|
|
{
|
|
public function getSlotDefinitions(): array
|
|
{
|
|
return [
|
|
SlotDefinition::named('header', '<h2>Default</h2>'),
|
|
SlotDefinition::named('body')->withRequired(true),
|
|
];
|
|
}
|
|
|
|
public function processSlotContent(SlotContent $content): SlotContent
|
|
{
|
|
// Automatic wrapping
|
|
$wrapped = "<div class='card-{$content->slotName}'>{$content->content}</div>";
|
|
return $content->withContent($wrapped);
|
|
}
|
|
}
|
|
```
|
|
|
|
**Benefits:**
|
|
- ✅ Type-safe Slot-Definitionen
|
|
- ✅ Automatische Validation
|
|
- ✅ Scoped Slots Support
|
|
- ✅ Content Processing Hooks
|
|
- ✅ Better Developer Experience
|
|
|
|
---
|
|
|
|
## Zusammenfassung
|
|
|
|
Das **Slot System** bietet eine flexible, typsichere Lösung für Component-Komposition:
|
|
|
|
**✅ Features:**
|
|
- Named, Default und Scoped Slots
|
|
- Required Slots mit Validation
|
|
- Default Content Fallbacks
|
|
- Automatic Content Processing
|
|
- XSS Protection
|
|
- Custom Validation Hooks
|
|
- Template Integration
|
|
- Backward Compatibility
|
|
|
|
**✅ Architecture:**
|
|
- Value Objects für Type Safety
|
|
- SlotManager für Orchestration
|
|
- SupportsSlots Interface für Components
|
|
- SlotProcessor für Template-Rendering
|
|
- Framework-compliant (readonly, final, composition)
|
|
|
|
**✅ Use Cases:**
|
|
- Layout Components (Header/Sidebar/Main/Footer)
|
|
- Modal/Dialog Components
|
|
- Card/Container Components
|
|
- Complex Nested Compositions
|
|
- Dynamic Content Injection
|
|
|
|
**Nächste Schritte:**
|
|
- Weitere Example Components erstellen
|
|
- Performance Optimizations
|
|
- Advanced Caching für Slot-Resolution
|
|
- Visual Slot Editor (optional)
|