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
11 KiB
11 KiB
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 ❌
// Component
public function getRenderData(): ComponentRenderData
{
return new ComponentRenderData('user-card', [
'user' => $this->user,
'permissions' => $this->permissions
]);
}
<!-- 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 ✅
// 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' : '');
}
<!-- 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:
{{#if user.orders.length > 0}}
<div>User has {{user.orders.length}} orders</div>
{{/if}}
✅ Best Practice:
// 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' : '');
}
<!-- Template -->
{{#if hasOrders}}
<div>{{orderText}}</div>
{{/if}}
Beispiel 2: Formatierung & Berechnungen
❌ Anti-Pattern:
<div class="price">€ {{price * 1.19}}</div>
<div class="date">{{created.format('d.m.Y')}}</div>
✅ Best Practice:
// 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, ',', '.');
}
<!-- Template -->
<div class="price">{{priceDisplay}}</div>
<div class="date">{{formattedDate}}</div>
Beispiel 3: CSS-Klassen basierend auf Status
❌ Anti-Pattern:
<div class="status {{status === 'active' ? 'status-active' : 'status-inactive'}}">
{{status}}
</div>
✅ Best Practice:
// 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 => '?'
};
}
<!-- Template -->
<div class="status {{statusClass}}">
<span class="icon">{{statusIcon}}</span>
{{statusText}}
</div>
Beispiel 4: Listen mit berechneten Werten
❌ Anti-Pattern:
{{#each items}}
<div class="item">
{{name}} - {{price * quantity}} €
{{#if inStock && quantity > 0}}
<span class="available">Available</span>
{{/if}}
</div>
{{/each}}
✅ Best Practice:
// 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, ',', '.') . ' €';
}
<!-- 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
// 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
// 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'
]);
}
<!-- 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
// 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:
{{variableName}}
{{object.property}}
{{array.0.name}}
Conditionals:
{{#if condition}}
Content when true
{{/if}}
{{#if condition}}
True content
{{else}}
False content
{{/if}}
Loops:
{{#each items}}
{{name}} - {{value}}
{{/each}}
Nested Templates:
{{#if user}}
{{#each user.orders}}
<div>Order {{id}}: {{total}}</div>
{{/each}}
{{/if}}
Zusammenfassung
Goldene Regeln:
- ✅ Bereite alle Daten im Component vor - keine Logik im Template
- ✅ Verwende aussagekräftige Property-Namen -
showAdminBadgestattisAdminAndActive - ✅ Formatiere Daten in PHP -
priceFormattedstatt Berechnung im Template - ✅ CSS-Klassen vorbereiten -
statusClassstatt Conditional im Template - ✅ Boolean Flags für Conditionals -
hasOrdersstattorders.length > 0 - ✅ Listen vorverarbeiten - Arrays mit allen Display-Daten vorbereiten
- ✅ Teste Component-Logik - nicht Template-Rendering
Das Template-System ist bewusst einfach gehalten, um saubere Separation of Concerns zu fördern.