Files
michaelschiemer/docs/claude/livecomponents-system.md
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- 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.
2025-10-25 19:18:37 +02:00

14 KiB
Raw History

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 Byte Value 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-interval fehlt
  • 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.