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
14 KiB
14 KiB
LiveComponents System
Zero-Dependency Interactive Component System f<>r das Custom PHP Framework mit Polling, File Uploads und SSE Support.
<EFBFBD>bersicht
LiveComponents erm<72>glicht interaktive UI-Komponenten ohne externe JavaScript-Frameworks. Das System nutzt ausschlie<69>lich Vanilla JavaScript und Framework-eigene Patterns.
Features
- Zero Dependencies - Nur Vanilla JavaScript (< 5KB gzipped)
- Polling Support - Automatische Updates in konfigurierbaren Intervallen
- File Upload - Progress-Tracking mit
ByteValue Object - SSE Integration - Real-time Updates via Server-Sent Events
- Framework-Compliant - Readonly Classes, Value Objects, DI Container
- Progressive Enhancement - Funktioniert ohne JavaScript
Architektur
src/Framework/LiveComponents/
Contracts/
Pollable.php # Polling-Interface
Uploadable.php # File Upload-Interface
ValueObjects/
LiveComponentState.php # Component State
ComponentAction.php # Action VO
ComponentUpdate.php # Update Response
UploadedComponentFile.php # Upload VO mit Byte
FileUploadProgress.php # Progress VO
Controllers/
LiveComponentController.php # Action Handler
UploadController.php # Upload Handler
Templates/
*.view.php # Component Templates
LiveComponent.php # Abstract Base Class
ComponentRegistry.php # Component Resolution
public/js/
live-components.js # Main Client (3KB)
sse-client.js # SSE Manager (2KB)
Quick Start
1. Component erstellen
namespace App\Application\Components;
use App\Framework\LiveComponents\Contracts\LiveComponentContract;
use App\Framework\LiveComponents\Contracts\Pollable;
use App\Framework\LiveComponents\Traits\LiveComponentTrait;
use App\Framework\View\TemplateRenderer;
final readonly class NotificationBellComponent implements LiveComponentContract, Pollable
{
use LiveComponentTrait;
public function __construct(
string $id,
array $initialData = [],
?TemplateRenderer $templateRenderer = null
) {
$this->id = $id;
$this->initialData = $initialData;
$this->templateRenderer = $templateRenderer;
}
public function render(): string
{
return $this->template('Framework/LiveComponents/Templates/notification-bell', [
'count' => $this->initialData['unread_count'] ?? 0,
'notifications' => $this->initialData['notifications'] ?? [],
'pollInterval' => $this->getPollInterval()
]);
}
public function poll(): array
{
$notifications = $this->notificationService->getUnread(
$this->initialData['user_id']
);
return [
'unread_count' => count($notifications),
'notifications' => array_map(fn($n) => $n->toArray(), $notifications)
];
}
public function getPollInterval(): int
{
return 5000; // 5 seconds
}
public function markAsRead(string $notificationId): array
{
$this->notificationService->markAsRead($notificationId);
return $this->poll();
}
}
2. Im Controller verwenden
use App\Framework\LiveComponents\ComponentRegistry;
#[Route('/dashboard', method: Method::GET)]
public function dashboard(): ViewResult
{
$bell = new NotificationBellComponent(
id: ComponentRegistry::makeId(NotificationBellComponent::class, 'user-123'),
initialData: ['user_id' => '123']
);
return new ViewResult('dashboard', [
'notificationBell' => $bell
]);
}
3. Im Template rendern
<!-- dashboard.view.php -->
<!DOCTYPE html>
<html>
<head>
<title>Dashboard</title>
<script src="/js/live-components.js" defer></script>
</head>
<body>
{!! notificationBell.toHtml() !!}
</body>
</html>
Polling Components
Pollable Interface
interface Pollable
{
public function poll(): array;
public function getPollInterval(): int; // Milliseconds
}
Beispiel: Live Dashboard
final readonly class LiveDashboardComponent implements LiveComponentContract, Pollable
{
use LiveComponentTrait;
public function __construct(
string $id,
array $initialData = [],
?TemplateRenderer $templateRenderer = null
) {
$this->id = $id;
$this->initialData = $initialData;
$this->templateRenderer = $templateRenderer;
}
public function render(): string
{
return $this->template('Framework/LiveComponents/Templates/live-dashboard', [
'metrics' => $this->initialData['metrics'] ?? []
]);
}
public function poll(): array
{
return [
'metrics' => $this->metricsService->getCurrentMetrics()
];
}
public function getPollInterval(): int
{
return 10000; // 10 seconds
}
}
Template mit Polling
<div class="dashboard" data-poll-interval="{pollInterval}">
<for items="metrics" as="metric">
<div class="metric-card">
<h3>{metric.label}</h3>
<span>{metric.value}</span>
</div>
</for>
</div>
File Upload Components
Uploadable Interface
interface Uploadable
{
public function handleUpload(UploadedComponentFile $file): array;
public function validateFile(UploadedComponentFile $file): bool;
public function getMaxFileSize(): Byte;
public function getAllowedMimeTypes(): array;
}
Beispiel: Avatar Upload
use App\Framework\Core\ValueObjects\Byte;
final readonly class AvatarUploadComponent implements LiveComponentContract, Uploadable
{
use LiveComponentTrait;
public function __construct(
string $id,
array $initialData = [],
?TemplateRenderer $templateRenderer = null
) {
$this->id = $id;
$this->initialData = $initialData;
$this->templateRenderer = $templateRenderer;
}
public function render(): string
{
return $this->template('Framework/LiveComponents/Templates/avatar-upload', [
'avatar_url' => $this->initialData['avatar_url'] ?? null,
'max_size' => $this->getMaxFileSize()->toHumanReadable()
]);
}
public function handleUpload(UploadedComponentFile $file): array
{
$avatarUrl = $this->avatarService->upload(
userId: $this->initialData['user_id'],
file: $file
);
return [
'avatar_url' => $avatarUrl,
'user_id' => $this->initialData['user_id']
];
}
public function validateFile(UploadedComponentFile $file): bool
{
return $file->isValid()
&& $file->size->lessThan($this->getMaxFileSize())
&& in_array($file->type, $this->getAllowedMimeTypes());
}
public function getMaxFileSize(): Byte
{
return Byte::fromMegabytes(5); // 5MB
}
public function getAllowedMimeTypes(): array
{
return ['image/jpeg', 'image/png', 'image/webp'];
}
}
Upload Template
<div class="avatar-upload">
<if condition="avatar_url">
<img src="{avatar_url}" alt="Avatar" />
</if>
<form data-live-upload="handleUpload" data-live-prevent>
<input
type="file"
name="avatar"
accept="image/jpeg,image/png,image/webp"
data-max-size="{max_size}"
/>
<button type="submit">Upload</button>
</form>
<div class="upload-progress" style="display: none;">
<div class="progress-bar" style="width: 0%"></div>
<span>0%</span>
</div>
</div>
SSE (Server-Sent Events) Integration
SSE Controller f<>r Components
use App\Framework\Router\Result\SseResult;
final readonly class ComponentStreamController
{
#[Route('/live-component/{id}/stream', method: Method::GET)]
public function stream(string $id): SseResult
{
$result = new SseResult(
heartbeatInterval: 30,
maxDuration: 1800 // 30 minutes
);
// Send updates callback
$result = $result->withCallback(function() use ($id) {
while (true) {
$component = $this->componentRegistry->resolve($id, []);
if ($component instanceof Pollable) {
$newData = $component->poll();
$updatedComponent = new ($component::class)(
id: $component->getId(),
initialData: $newData
);
yield [
'html' => $updatedComponent->render(),
'state' => (new LiveComponentState(
id: $id,
component: $component::class,
data: $newData
))->toJson()
];
}
sleep($component->getPollInterval() / 1000);
}
});
return $result;
}
}
SSE Client Usage
// Connect to SSE stream
window.sseManager.connect('/live-component/NotificationBellComponent:user-123/stream', {
events: {
'component-update': (data) => {
const component = window.liveComponents.components.get('NotificationBellComponent:user-123');
if (component) {
component.element.innerHTML = data.html;
component.state = JSON.parse(data.state);
}
}
},
onError: () => {
// Fallback to polling
window.liveComponents.startPolling('NotificationBellComponent:user-123', 5000);
}
});
JavaScript API
LiveComponentManager
// Manual action call
await window.liveComponents.callAction(
'NotificationBellComponent:user-123',
'markAsRead',
{ id: 'notif-1' }
);
// Start/stop polling
window.liveComponents.startPolling(componentId, intervalMs);
window.liveComponents.stopPolling(componentId);
// Debounced actions
const debouncedSearch = window.liveComponents.debounce(
(query) => window.liveComponents.callAction(componentId, 'search', { query }),
300
);
SSEManager
// Connect to SSE
window.sseManager.connect(url, {
events: { 'event-name': (data) => {} },
onOpen: () => {},
onError: (error) => {},
onMessage: (data) => {}
});
// Disconnect
window.sseManager.disconnect(url);
window.sseManager.disconnectAll();
Template Syntax
Action Buttons
<!-- Simple action -->
<button data-live-action="refresh">Refresh</button>
<!-- With parameters -->
<button
data-live-action="markAsRead"
data-param-id="{notification.id}"
data-live-prevent
>
Mark as read
</button>
Forms
<!-- Form submission -->
<form data-live-action="save" data-live-prevent>
<input name="title" value="{title}" />
<button type="submit">Save</button>
</form>
<!-- File upload -->
<form data-live-upload="handleUpload" data-live-prevent>
<input type="file" name="avatar" data-max-size="5242880" />
<button type="submit">Upload</button>
</form>
Polling
<!-- Auto-polling div -->
<div data-poll-interval="{pollInterval}">
<!-- Content updates automatically -->
</div>
Best Practices
1. Component Design
- Stateless: Components sollten stateless sein
- Idempotent: Actions mehrfach ausf<73>hrbar ohne Seiteneffekte
- Type Safety: Value Objects f<>r alle Daten verwenden
- Naming: Klare, deskriptive Namen f<>r Actions
2. Performance
- Polling Intervals: Nicht zu aggressiv (min. 3-5 Sekunden)
- SSE Fallback: Polling als Fallback bei SSE-Fehlern
- Debouncing: F<>r Suche und Eingabe-Events
- Lazy Loading: Komponenten on-demand laden
3. Error Handling
- Validation: File-Validierung vor Upload
- Graceful Degradation: Funktioniert ohne JavaScript
- User Feedback: Klare Fehlermeldungen
- Retry Logic: Automatische Reconnects bei SSE
4. Security
- File Validation: MIME-Type und Gr<47><72>e pr<70>fen
- CSRF Protection: Framework's CSRF-System nutzen
- Input Sanitization: Alle User-Inputs validieren
- Authorization: Zugriffskontrolle in Actions
Troubleshooting
Problem: Component wird nicht initialisiert
L<EFBFBD>sung: JavaScript am Ende des Body laden:
<script src="/js/live-components.js" defer></script>
Problem: Polling funktioniert nicht
Ursachen:
- Polling-Intervall nicht gesetzt:
data-poll-intervalfehlt - Component implementiert Pollable nicht
- JavaScript-Fehler in Browser Console pr<70>fen
Problem: File Upload schl<68>gt fehl
Ursachen:
- Datei zu gro<72>:
getMaxFileSize()pr<70>fen - MIME-Type nicht erlaubt:
getAllowedMimeTypes()pr<70>fen - Fehlende Upload-Berechtigung
Problem: SSE verbindet nicht
L<EFBFBD>sung: Fallback zu Polling:
window.sseManager.connect(url, {
onError: () => {
window.liveComponents.startPolling(componentId, 5000);
}
});
Framework Integration
DI Container
// In Initializer registrieren
#[Initializer]
public function initLiveComponents(Container $container): void
{
$container->singleton(ComponentRegistry::class, function($c) {
return new ComponentRegistry($c);
});
}
Routing
Controllers werden automatisch via #[Route] Attribute entdeckt:
/live-component/{id}- Action Handler/live-component/{id}/upload- Upload Handler
Erweiterungen
Das System kann einfach erweitert werden:
- Custom Interfaces: Neue Contracts f<>r spezielle Features
- Middleware: Component-spezifische Middleware
- Events: Component Events via Framework EventDispatcher
- Caching: Component-State caching
- Nested Components: Parent-Child Component Beziehungen
Zusammenfassung
LiveComponents bietet:
- Zero-Dependency Interaktivit<69>t
- Framework-Pattern Compliance
- Polling, Upload, SSE Support
- Progressive Enhancement
- Testbare Architektur
- Erweiterbare Struktur
Das System folgt konsequent Framework-Prinzipien: Readonly Classes, Value Objects, Composition over Inheritance, und Explicit Dependency Injection.