- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
462 lines
11 KiB
Markdown
462 lines
11 KiB
Markdown
# LiveComponents Best Practices
|
|
|
|
## Template-System Philosophie
|
|
|
|
**Grundprinzip**: Templates sollten **nur für die Darstellung** zuständig sein, nicht für Logik.
|
|
|
|
### ✅ Was Templates können
|
|
|
|
- **Variable Substitution**: `{{variableName}}`
|
|
- **Conditional Rendering**: `{{#if condition}}...{{/if}}`
|
|
- **Loops**: `{{#each items}}...{{/each}}`
|
|
- **Nested Properties**: `{{user.name}}`, `{{item.value}}`
|
|
|
|
### ❌ Was Templates NICHT können
|
|
|
|
- Komplexe Expressions: `{{user.role === 'admin' && user.active}}`
|
|
- Berechnungen: `{{count * 2}}`, `{{items.length}}`
|
|
- Method Calls: `{{formatDate(created)}}`, `{{user.getName()}}`
|
|
- Vergleichsoperatoren in Platzhaltern: `{{price > 100}}`
|
|
|
|
## Best Practice: Daten im Component vorbereiten
|
|
|
|
### Anti-Pattern ❌
|
|
|
|
```php
|
|
// Component
|
|
public function getRenderData(): ComponentRenderData
|
|
{
|
|
return new ComponentRenderData('user-card', [
|
|
'user' => $this->user,
|
|
'permissions' => $this->permissions
|
|
]);
|
|
}
|
|
```
|
|
|
|
```html
|
|
<!-- Template mit komplexer Logik -->
|
|
{{#if user.role}}
|
|
{{#if user.role === 'admin'}}
|
|
{{#if user.isActive}}
|
|
<span class="badge">{{permissions.length}} Permissions</span>
|
|
{{/if}}
|
|
{{/if}}
|
|
{{/if}}
|
|
```
|
|
|
|
**Probleme:**
|
|
- ❌ Logik im Template schwer testbar
|
|
- ❌ Template-Syntax unterstützt keine Vergleichsoperatoren
|
|
- ❌ Keine Type Safety
|
|
- ❌ Schwer zu debuggen
|
|
|
|
### Best Practice ✅
|
|
|
|
```php
|
|
// Component - Daten vollständig vorbereiten
|
|
public function getRenderData(): ComponentRenderData
|
|
{
|
|
return new ComponentRenderData('user-card', [
|
|
'user' => $this->user,
|
|
'showAdminBadge' => $this->shouldShowAdminBadge(),
|
|
'permissionCount' => $this->getPermissionCount(),
|
|
'badgeClass' => $this->getBadgeClass(),
|
|
'badgeText' => $this->getBadgeText()
|
|
]);
|
|
}
|
|
|
|
private function shouldShowAdminBadge(): bool
|
|
{
|
|
return $this->user->role === 'admin' && $this->user->isActive;
|
|
}
|
|
|
|
private function getPermissionCount(): int
|
|
{
|
|
return count($this->permissions);
|
|
}
|
|
|
|
private function getBadgeClass(): string
|
|
{
|
|
return $this->user->isActive ? 'badge-success' : 'badge-secondary';
|
|
}
|
|
|
|
private function getBadgeText(): string
|
|
{
|
|
$count = $this->getPermissionCount();
|
|
return "{$count} Permission" . ($count !== 1 ? 's' : '');
|
|
}
|
|
```
|
|
|
|
```html
|
|
<!-- Template - nur Darstellung -->
|
|
{{#if showAdminBadge}}
|
|
<span class="badge {{badgeClass}}">{{badgeText}}</span>
|
|
{{/if}}
|
|
```
|
|
|
|
**Vorteile:**
|
|
- ✅ Business-Logik testbar in Component
|
|
- ✅ Template einfach und lesbar
|
|
- ✅ Type Safety durch PHP
|
|
- ✅ Einfach zu debuggen
|
|
- ✅ Wiederverwendbare Component-Methoden
|
|
|
|
## Praktische Beispiele
|
|
|
|
### Beispiel 1: Conditional Rendering
|
|
|
|
**❌ Anti-Pattern:**
|
|
```html
|
|
{{#if user.orders.length > 0}}
|
|
<div>User has {{user.orders.length}} orders</div>
|
|
{{/if}}
|
|
```
|
|
|
|
**✅ Best Practice:**
|
|
```php
|
|
// Component
|
|
public function getRenderData(): ComponentRenderData
|
|
{
|
|
return new ComponentRenderData('user-summary', [
|
|
'hasOrders' => $this->hasOrders(),
|
|
'orderCount' => $this->getOrderCount(),
|
|
'orderText' => $this->getOrderText()
|
|
]);
|
|
}
|
|
|
|
private function hasOrders(): bool
|
|
{
|
|
return count($this->user->orders) > 0;
|
|
}
|
|
|
|
private function getOrderCount(): int
|
|
{
|
|
return count($this->user->orders);
|
|
}
|
|
|
|
private function getOrderText(): string
|
|
{
|
|
$count = $this->getOrderCount();
|
|
return "User has {$count} order" . ($count !== 1 ? 's' : '');
|
|
}
|
|
```
|
|
|
|
```html
|
|
<!-- Template -->
|
|
{{#if hasOrders}}
|
|
<div>{{orderText}}</div>
|
|
{{/if}}
|
|
```
|
|
|
|
### Beispiel 2: Formatierung & Berechnungen
|
|
|
|
**❌ Anti-Pattern:**
|
|
```html
|
|
<div class="price">€ {{price * 1.19}}</div>
|
|
<div class="date">{{created.format('d.m.Y')}}</div>
|
|
```
|
|
|
|
**✅ Best Practice:**
|
|
```php
|
|
// Component
|
|
public function getRenderData(): ComponentRenderData
|
|
{
|
|
return new ComponentRenderData('product-card', [
|
|
'priceWithTax' => $this->getPriceWithTax(),
|
|
'formattedDate' => $this->getFormattedDate(),
|
|
'priceDisplay' => $this->getPriceDisplay()
|
|
]);
|
|
}
|
|
|
|
private function getPriceWithTax(): float
|
|
{
|
|
return $this->price * 1.19;
|
|
}
|
|
|
|
private function getFormattedDate(): string
|
|
{
|
|
return $this->created->format('d.m.Y');
|
|
}
|
|
|
|
private function getPriceDisplay(): string
|
|
{
|
|
return '€ ' . number_format($this->getPriceWithTax(), 2, ',', '.');
|
|
}
|
|
```
|
|
|
|
```html
|
|
<!-- Template -->
|
|
<div class="price">{{priceDisplay}}</div>
|
|
<div class="date">{{formattedDate}}</div>
|
|
```
|
|
|
|
### Beispiel 3: CSS-Klassen basierend auf Status
|
|
|
|
**❌ Anti-Pattern:**
|
|
```html
|
|
<div class="status {{status === 'active' ? 'status-active' : 'status-inactive'}}">
|
|
{{status}}
|
|
</div>
|
|
```
|
|
|
|
**✅ Best Practice:**
|
|
```php
|
|
// Component
|
|
public function getRenderData(): ComponentRenderData
|
|
{
|
|
return new ComponentRenderData('status-badge', [
|
|
'statusClass' => $this->getStatusClass(),
|
|
'statusText' => $this->getStatusText(),
|
|
'statusIcon' => $this->getStatusIcon()
|
|
]);
|
|
}
|
|
|
|
private function getStatusClass(): string
|
|
{
|
|
return match($this->status) {
|
|
'active' => 'status-active',
|
|
'pending' => 'status-pending',
|
|
'inactive' => 'status-inactive',
|
|
default => 'status-unknown'
|
|
};
|
|
}
|
|
|
|
private function getStatusText(): string
|
|
{
|
|
return ucfirst($this->status);
|
|
}
|
|
|
|
private function getStatusIcon(): string
|
|
{
|
|
return match($this->status) {
|
|
'active' => '✓',
|
|
'pending' => '⏳',
|
|
'inactive' => '✗',
|
|
default => '?'
|
|
};
|
|
}
|
|
```
|
|
|
|
```html
|
|
<!-- Template -->
|
|
<div class="status {{statusClass}}">
|
|
<span class="icon">{{statusIcon}}</span>
|
|
{{statusText}}
|
|
</div>
|
|
```
|
|
|
|
### Beispiel 4: Listen mit berechneten Werten
|
|
|
|
**❌ Anti-Pattern:**
|
|
```html
|
|
{{#each items}}
|
|
<div class="item">
|
|
{{name}} - {{price * quantity}} €
|
|
{{#if inStock && quantity > 0}}
|
|
<span class="available">Available</span>
|
|
{{/if}}
|
|
</div>
|
|
{{/each}}
|
|
```
|
|
|
|
**✅ Best Practice:**
|
|
```php
|
|
// Component
|
|
public function getRenderData(): ComponentRenderData
|
|
{
|
|
return new ComponentRenderData('order-items', [
|
|
'items' => $this->prepareItems()
|
|
]);
|
|
}
|
|
|
|
private function prepareItems(): array
|
|
{
|
|
return array_map(function($item) {
|
|
return [
|
|
'name' => $item->name,
|
|
'totalPrice' => $this->formatPrice($item->price * $item->quantity),
|
|
'showAvailable' => $item->inStock && $item->quantity > 0,
|
|
'itemClass' => $item->inStock ? 'item-available' : 'item-unavailable'
|
|
];
|
|
}, $this->items);
|
|
}
|
|
|
|
private function formatPrice(float $price): string
|
|
{
|
|
return number_format($price, 2, ',', '.') . ' €';
|
|
}
|
|
```
|
|
|
|
```html
|
|
<!-- Template -->
|
|
{{#each items}}
|
|
<div class="item {{itemClass}}">
|
|
{{name}} - {{totalPrice}}
|
|
{{#if showAvailable}}
|
|
<span class="available">Available</span>
|
|
{{/if}}
|
|
</div>
|
|
{{/each}}
|
|
```
|
|
|
|
## LiveComponent-Spezifische Patterns
|
|
|
|
### Pattern 1: Event-Daten vorbereiten
|
|
|
|
```php
|
|
// Component
|
|
public function increment(): ComponentUpdate
|
|
{
|
|
$oldValue = $this->initialData['count'];
|
|
$newValue = $oldValue + 1;
|
|
|
|
return new ComponentUpdate(
|
|
newState: ['count' => $newValue],
|
|
events: [
|
|
new ComponentEvent(
|
|
name: 'counter:changed',
|
|
data: [
|
|
'old_value' => $oldValue,
|
|
'new_value' => $newValue,
|
|
'change' => '+1',
|
|
'isEven' => $newValue % 2 === 0,
|
|
'isMilestone' => $newValue % 10 === 0
|
|
]
|
|
)
|
|
]
|
|
);
|
|
}
|
|
```
|
|
|
|
### Pattern 2: Conditional Actions basierend auf State
|
|
|
|
```php
|
|
// Component
|
|
public function getRenderData(): ComponentRenderData
|
|
{
|
|
$count = $this->initialData['count'];
|
|
|
|
return new ComponentRenderData('counter', [
|
|
'count' => $count,
|
|
'canDecrement' => $count > 0,
|
|
'canIncrement' => $count < 100,
|
|
'showReset' => $count !== 0,
|
|
'decrementClass' => $count > 0 ? 'btn-danger' : 'btn-disabled',
|
|
'incrementClass' => $count < 100 ? 'btn-success' : 'btn-disabled'
|
|
]);
|
|
}
|
|
```
|
|
|
|
```html
|
|
<!-- Template -->
|
|
<div class="counter">
|
|
<h2>Count: {{count}}</h2>
|
|
|
|
{{#if canDecrement}}
|
|
<button data-live-action="decrement" class="{{decrementClass}}">
|
|
- Decrement
|
|
</button>
|
|
{{/if}}
|
|
|
|
{{#if canIncrement}}
|
|
<button data-live-action="increment" class="{{incrementClass}}">
|
|
+ Increment
|
|
</button>
|
|
{{/if}}
|
|
|
|
{{#if showReset}}
|
|
<button data-live-action="reset" class="btn-secondary">
|
|
Reset
|
|
</button>
|
|
{{/if}}
|
|
</div>
|
|
```
|
|
|
|
### Pattern 3: Cache-optimierte Datenvorbereitung
|
|
|
|
```php
|
|
// Component mit Caching
|
|
final readonly class StatsComponent implements LiveComponentContract, Cacheable
|
|
{
|
|
public function getRenderData(): ComponentRenderData
|
|
{
|
|
// Teure Berechnung einmal durchführen
|
|
$stats = $this->computeExpensiveStats();
|
|
|
|
// Alle Darstellungs-Daten vorbereiten
|
|
return new ComponentRenderData('stats', [
|
|
'stats' => $stats,
|
|
'totalUsers' => number_format($stats['total_users'], 0, ',', '.'),
|
|
'activeSessionsText' => $this->getActiveSessionsText($stats['active_sessions']),
|
|
'revenueFormatted' => $this->formatRevenue($stats['revenue']),
|
|
'showAlert' => $stats['total_users'] > 5000,
|
|
'alertClass' => $stats['total_users'] > 5000 ? 'alert-warning' : 'alert-info'
|
|
]);
|
|
}
|
|
|
|
private function getActiveSessionsText(int $count): string
|
|
{
|
|
return "{$count} active session" . ($count !== 1 ? 's' : '');
|
|
}
|
|
|
|
private function formatRevenue(int $revenue): string
|
|
{
|
|
return '€ ' . number_format($revenue, 2, ',', '.');
|
|
}
|
|
}
|
|
```
|
|
|
|
## Template-System Referenz
|
|
|
|
### Unterstützte Syntax
|
|
|
|
**Variable Substitution:**
|
|
```html
|
|
{{variableName}}
|
|
{{object.property}}
|
|
{{array.0.name}}
|
|
```
|
|
|
|
**Conditionals:**
|
|
```html
|
|
{{#if condition}}
|
|
Content when true
|
|
{{/if}}
|
|
|
|
{{#if condition}}
|
|
True content
|
|
{{else}}
|
|
False content
|
|
{{/if}}
|
|
```
|
|
|
|
**Loops:**
|
|
```html
|
|
{{#each items}}
|
|
{{name}} - {{value}}
|
|
{{/each}}
|
|
```
|
|
|
|
**Nested Templates:**
|
|
```html
|
|
{{#if user}}
|
|
{{#each user.orders}}
|
|
<div>Order {{id}}: {{total}}</div>
|
|
{{/each}}
|
|
{{/if}}
|
|
```
|
|
|
|
## Zusammenfassung
|
|
|
|
**Goldene Regeln:**
|
|
|
|
1. ✅ **Bereite alle Daten im Component vor** - keine Logik im Template
|
|
2. ✅ **Verwende aussagekräftige Property-Namen** - `showAdminBadge` statt `isAdminAndActive`
|
|
3. ✅ **Formatiere Daten in PHP** - `priceFormatted` statt Berechnung im Template
|
|
4. ✅ **CSS-Klassen vorbereiten** - `statusClass` statt Conditional im Template
|
|
5. ✅ **Boolean Flags für Conditionals** - `hasOrders` statt `orders.length > 0`
|
|
6. ✅ **Listen vorverarbeiten** - Arrays mit allen Display-Daten vorbereiten
|
|
7. ✅ **Teste Component-Logik** - nicht Template-Rendering
|
|
|
|
**Das Template-System ist bewusst einfach gehalten, um saubere Separation of Concerns zu fördern.**
|