Files
michaelschiemer/docs/claude/xcomponent-processor.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

19 KiB

XComponentProcessor - Unified Component Syntax

Einheitliche <x-*> Syntax für LiveComponents und HTML Components im Custom PHP Framework.

Übersicht

Der XComponentProcessor ist ein DOM-Processor, der eine einheitliche Syntax für zwei verschiedene Component-Typen bietet:

  1. LiveComponents (Interaktiv/Stateful) - Komponenten mit #[LiveComponent] Attribut
  2. HTML Components (Statisch) - Komponenten mit #[ComponentName] Attribut

Unified Syntax: <x-componentname>

<!-- LiveComponent (Interactive) -->
<x-datatable id="users" page="1" pageSize="25" />

<!-- HTML Component (Static) -->
<x-button variant="primary">Click me</x-button>

Architektur

Component Auto-Detection Flow

<x-componentname> Syntax
         ↓
XComponentProcessor
         ↓
   ┌─────┴─────┐
   ↓           ↓
LiveComponent? HTML Component?
   ↓           ↓
   YES         NO
   ↓           ↓
Process as    Process as
Interactive   Static

Detection Priority:

  1. LiveComponent prüfen (via ComponentRegistry::isRegistered())
  2. HTML Component prüfen (via HtmlComponentRegistry::has())
  3. Not Found → Helpful error message

Klassen-Struktur

final readonly class XComponentProcessor implements DomProcessor
{
    public function __construct(
        private ComponentRegistry $liveComponentRegistry,
        private HtmlComponentRegistry $htmlComponentRegistry,
        private ComponentMetadataCache $metadataCache,
        private DomComponentService $componentService
    ) {}

    public function process(DomWrapper $dom, RenderContext $context): DomWrapper
    {
        // Find all <x-*> elements
        $xComponents = $this->findXComponents($dom);

        foreach ($xComponents as $element) {
            try {
                $this->processXComponent($dom, $element, $context);
            } catch (\Throwable $e) {
                $this->handleProcessingError($dom, $element, $e);
            }
        }

        return $dom;
    }
}

LiveComponents (Interactive)

Verwendung

<!-- LiveComponent mit Props -->
<x-datatable
    id="users-table"
    page="1"
    pageSize="25"
    sortBy="created_at"
    sortOrder="desc"
/>

<!-- LiveComponent ohne explizite ID (auto-generiert) -->
<x-counter initialValue="0" />

<!-- Mit JSON Props -->
<x-chart
    data='[{"x":1,"y":10},{"x":2,"y":20}]'
    options='{"type":"line","legend":true}'
/>

LiveComponent Definition

use App\Framework\LiveComponents\Attributes\LiveComponent;
use App\Framework\LiveComponents\Attributes\LiveProp;

#[LiveComponent(name: 'datatable')]
final class DataTableComponent
{
    #[LiveProp]
    public int $page = 1;

    #[LiveProp]
    public int $pageSize = 25;

    #[LiveProp]
    public string $sortBy = 'id';

    #[LiveProp]
    public string $sortOrder = 'asc';

    public function render(): RenderData
    {
        $data = $this->loadData();

        return new RenderData(
            template: 'components/datatable',
            data: [
                'rows' => $data,
                'currentPage' => $this->page,
                'totalPages' => $this->calculateTotalPages()
            ]
        );
    }
}

Processing Flow

<x-datatable id="users" page="1" pageSize="25" />
         ↓
1. Extract component name: "datatable"
2. Check ComponentRegistry: isRegistered("datatable") → true
3. Extract props: ["id" => "users", "page" => "1", "pageSize" => "25"]
4. Type coercion: ["id" => "users", "page" => 1, "pageSize" => 25]
5. Validate props gegen ComponentMetadata
6. Create ComponentId: ComponentId::create("datatable", "users")
7. Create ComponentData: ComponentData::fromArray(["page" => 1, "pageSize" => 25])
8. Resolve component: $registry->resolve($componentId, $initialState)
9. Render with wrapper: $registry->renderWithWrapper($component)
10. Replace <x-datatable> with rendered HTML
         ↓
<div data-component-id="datatable:users" data-component-state="...">
    <!-- Rendered component HTML -->
</div>

Type Coercion (LiveComponents only)

Der XComponentProcessor führt automatische Type Coercion für LiveComponent-Props durch:

// Input: HTML Attributes (always strings)
<x-component
    stringProp="text"
    intProp="123"
    floatProp="12.5"
    boolTrue="true"
    boolFalse="false"
    nullProp="null"
    arrayProp="[1,2,3]"
    objectProp='{"key":"value"}'
/>

// Output: Coerced PHP types
[
    'stringProp' => 'text',              // string
    'intProp' => 123,                    // int
    'floatProp' => 12.5,                 // float
    'boolTrue' => true,                  // bool
    'boolFalse' => false,                // bool
    'nullProp' => null,                  // null
    'arrayProp' => [1, 2, 3],           // array
    'objectProp' => ['key' => 'value']   // array
]

Coercion Rules:

  • "true" / "false"bool
  • "null"null
  • "123"int
  • "12.5"float
  • "[...]" / "{...}" → JSON decode
  • Alles andere → string

Prop Validation

// Component Definition
#[LiveComponent(name: 'counter')]
final class CounterComponent
{
    #[LiveProp]
    public int $initialValue = 0;

    #[LiveProp]
    public int $step = 1;
}

// ✅ Valid Usage
<x-counter initialValue="10" step="2" />

// ❌ Invalid: Unknown prop 'invalidProp'
<x-counter invalidProp="test" />
// → Development: Error message
// → Production: Silent removal

// ❌ Invalid: Wrong type (caught at component level)
<x-counter initialValue="not-a-number" />
// → Type coercion to string, component validation throws error

Validation Error (Development Mode):

<div style="border:2px solid red;padding:1rem;background:#fee;color:#c00;">
    <strong>XComponentProcessor Error:</strong><br>
    <pre>LiveComponent 'counter' has no property 'invalidProp'.
Available properties: initialValue, step</pre>
    <small>Component: x-counter</small>
</div>

HTML Components (Static)

Verwendung

<!-- Static Button Component -->
<x-button variant="primary">Click me</x-button>

<!-- Card Component with content -->
<x-card title="User Profile">
    <p>User information goes here</p>
</x-card>

<!-- Badge Component -->
<x-badge color="blue">New</x-badge>

HTML Component Definition

use App\Framework\View\Attributes\ComponentName;

#[ComponentName('button')]
final readonly class ButtonComponent
{
    public function render(string $content, array $attributes): string
    {
        $variant = $attributes['variant'] ?? 'default';
        $class = "btn btn-{$variant}";

        return "<button class=\"{$class}\">{$content}</button>";
    }
}

Processing Flow

<x-button variant="primary">Click me</x-button>
         ↓
1. Extract component name: "button"
2. Check ComponentRegistry: isRegistered("button") → false
3. Check HtmlComponentRegistry: has("button") → true
4. Extract content: "Click me"
5. Extract attributes: ["variant" => "primary"] (as strings)
6. Render: $registry->render("button", "Click me", ["variant" => "primary"])
7. Replace <x-button> with rendered HTML
         ↓
<button class="btn btn-primary">Click me</button>

Unterschiede zu LiveComponents

HTML Components:

  • Keine Type Coercion - alle Attribute bleiben strings
  • Keine Prop Validation - Component entscheidet selbst
  • Kein State Management - rein statisches Rendering
  • Keine ComponentId/ComponentData - nur template rendering
  • Schneller - keine Metadata-Prüfung, kein State-Handling
  • Einfacher - nur render(content, attributes) Method

LiveComponents:

  • Type Coercion für alle Props
  • Prop Validation gegen Metadata
  • State Management mit ComponentData
  • Interactive mit AJAX/WebSocket updates
  • ⚠️ Komplexer - mehr Overhead für Metadata/State

Component Registration

LiveComponent Registration

// Automatisch via #[LiveComponent] Attribute Discovery
use App\Framework\LiveComponents\Attributes\LiveComponent;

#[LiveComponent(name: 'datatable')]
final class DataTableComponent implements LiveComponentContract
{
    // Component implementation
}

// Framework registriert automatisch bei Bootstrap:
// ComponentRegistry::register('datatable', DataTableComponent::class)

HTML Component Registration

// Automatisch via #[ComponentName] Attribute Discovery
use App\Framework\View\Attributes\ComponentName;

#[ComponentName('button')]
final readonly class ButtonComponent
{
    public function render(string $content, array $attributes): string
    {
        // Rendering logic
    }
}

// Framework registriert automatisch:
// HtmlComponentRegistry::register('button', ButtonComponent::class)

Error Handling

Component Not Found

<x-unknown-component />

Development Mode:

<div style="border:2px solid red;padding:1rem;background:#fee;color:#c00;">
    <strong>XComponentProcessor Error:</strong><br>
    <pre>Unknown component: <x-unknown-component>

Available LiveComponents: datatable, counter, chart
Available HTML Components: button, card, badge</pre>
    <small>Component: x-unknown-component</small>
</div>

Production Mode:

  • Element wird silently removed
  • Kein Error-HTML im Output
  • Optional: Logging via error_log()

Invalid Props (LiveComponents)

<x-counter invalidProp="test" />

Development Mode:

<div style="border:2px solid red;">
    <strong>XComponentProcessor Error:</strong><br>
    <pre>LiveComponent 'counter' has no property 'invalidProp'.
Available properties: initialValue, step</pre>
    <small>Component: x-counter</small>
</div>

Production Mode:

  • Component wird nicht gerendert
  • Silent removal
  • Optional: Logging

Rendering Errors

Wenn ComponentRegistry::renderWithWrapper() oder HtmlComponentRegistry::render() fehlschlägt:

Development Mode: Error message with stack trace Production Mode: Silent removal + logging

Auto-Detection Priority

Wenn Component in BEIDEN Registries existiert:

LiveComponent Priority > HTML Component

Beispiel:

// BOTH registered:
#[LiveComponent(name: 'button')]
class ButtonLiveComponent { ... }

#[ComponentName('button')]
class ButtonHtmlComponent { ... }

// Usage:
<x-button id="my-button" />
// → Processed as LiveComponent (Priority!)

Best Practice: Verwende unterschiedliche Namen für LiveComponents und HTML Components, um Verwirrung zu vermeiden.

Performance Characteristics

LiveComponent Processing

  • Prop Extraction: O(n) wo n = Anzahl Attribute
  • Type Coercion: O(n) wo n = Anzahl Props
  • Prop Validation: O(n) wo n = Anzahl Props (Metadata lookup cached)
  • Component Resolution: Container DI Resolution
  • Rendering: Depends on component logic

Durchschnittliche Latenz: ~5-15ms pro LiveComponent

HTML Component Processing

  • Attribute Extraction: O(n) wo n = Anzahl Attribute
  • Content Extraction: O(1)
  • Rendering: Component-spezifische Logic

Durchschnittliche Latenz: ~1-5ms pro HTML Component

Optimization Strategies

// 1. ComponentMetadataCache nutzen (automatisch)
$metadata = $this->metadataCache->get($className);

// 2. Registry Lookups cachen (automatisch)
if ($this->liveComponentRegistry->isRegistered($name)) { ... }

// 3. Batch-Processing für multiple Components
foreach ($xComponents as $element) {
    $this->processXComponent($dom, $element, $context);
}

Framework Integration

DomProcessingPipeline Integration

// In TemplateRendererInitializer
$domProcessors = [
    ForProcessor::class,              // Priority 100
    IfProcessor::class,               // Priority 200
    XComponentProcessor::class,       // Priority 300 ← Here
    ComponentProcessor::class,        // Priority 400 (legacy <component>)
    IncludeProcessor::class,          // Priority 500
    // ...
];

Processing Order:

  1. <for> loops werden zuerst verarbeitet
  2. <if> conditionals danach
  3. <x-*> Components werden resolved
  4. Legacy <component> tags (fallback)
  5. <include> templates einbinden

RenderContext Integration

$context = RenderContext::create([
    'users' => $users,
    'currentPage' => $page
]);

// XComponentProcessor hat Zugriff auf $context->data
// Aber: Props werden via Attribute übergeben, nicht via Context

Testing

Unit Tests

Der XComponentProcessor hat umfassende Test-Coverage:

describe('XComponentProcessor', function () {
    describe('LiveComponent Processing', function () {
        it('processes LiveComponent with basic props');
        it('coerces prop types correctly');
        it('validates props against ComponentMetadata');
        it('generates unique ID if not provided');
    });

    describe('HTML Component Processing', function () {
        it('processes HTML Component');
        it('extracts content from HTML Component');
    });

    describe('Error Handling', function () {
        it('shows helpful error when component not found');
        it('removes component silently in production');
    });

    describe('Auto-Detection Priority', function () {
        it('prioritizes LiveComponent over HTML Component');
    });
});

Test File: tests/Unit/Framework/View/Processors/XComponentProcessorTest.php

Integration Testing

// Full Template Processing mit XComponentProcessor
$template = <<<HTML
<html>
<body>
    <h1>Dashboard</h1>
    <x-datatable id="users" page="1" />
    <x-button variant="primary">Add User</x-button>
</body>
</html>
HTML;

$context = RenderContext::create([]);
$rendered = $this->templateProcessor->render($context, $template);

// Assert: LiveComponent rendered with data-component-id
expect($rendered)->toContain('data-component-id="datatable:users"');

// Assert: HTML Component rendered as button
expect($rendered)->toContain('<button class="btn btn-primary">');

Best Practices

1. Naming Conventions

// ✅ Good: Clear, descriptive names
#[LiveComponent(name: 'user-datatable')]
#[LiveComponent(name: 'product-filter')]
#[ComponentName('primary-button')]

// ❌ Bad: Generic or conflicting names
#[LiveComponent(name: 'table')]  // Too generic
#[ComponentName('button')]       // Conflicts with LiveComponent 'button'

2. Prop Design (LiveComponents)

// ✅ Good: Specific, typed props
#[LiveComponent(name: 'datatable')]
final class DataTableComponent
{
    #[LiveProp]
    public int $page = 1;

    #[LiveProp]
    public int $pageSize = 25;

    #[LiveProp]
    public string $sortBy = 'id';
}

// ❌ Bad: Generic array props (lose type safety)
#[LiveProp]
public array $config = [];  // What keys? What types?

3. ID Management

// ✅ Good: Explicit IDs for wichtige Components
<x-datatable id="users-table" page="1" />
<x-datatable id="products-table" page="1" />

// ⚠️ OK: Auto-generated IDs für einmalige Components
<x-counter initialValue="0" />
// → "counter-a3f2d8e1" (auto-generated)

// ❌ Bad: Duplicate IDs
<x-counter id="counter-1" />
<x-counter id="counter-1" />  // ID collision!

4. Content vs Props

// ✅ HTML Component: Content via innerHTML
<x-button variant="primary">Click me</x-button>

// ✅ LiveComponent: Data via Props
<x-datatable data='[{"id":1,"name":"Alice"}]' />

// ❌ Bad: Mixing approaches
<x-datatable>
    <!-- Content ignored for LiveComponents! -->
    Some data here
</x-datatable>

5. Error Handling

// ✅ Good: Graceful error handling in component
#[LiveComponent(name: 'chart')]
final class ChartComponent
{
    #[LiveProp]
    public array $data = [];

    public function render(): RenderData
    {
        if (empty($this->data)) {
            return new RenderData(
                template: 'components/chart-empty',
                data: ['message' => 'No data available']
            );
        }

        return new RenderData(
            template: 'components/chart',
            data: ['chartData' => $this->processData()]
        );
    }
}

// ❌ Bad: Throwing exceptions ohne Fallback
public function render(): RenderData
{
    if (empty($this->data)) {
        throw new \Exception('No data!'); // User sieht error!
    }
    // ...
}

Migration Guide

Von Legacy <component> zu <x-*> Syntax

Vorher (Legacy Syntax):

<component name="button" variant="primary">Click me</component>

Nachher (Unified Syntax):

<x-button variant="primary">Click me</x-button>

Migration Steps:

  1. HTML Components: Einfach Syntax ändern

    <!-- Old -->
    <component name="card" title="Profile">Content</component>
    
    <!-- New -->
    <x-card title="Profile">Content</x-card>
    
  2. LiveComponents: Prüfen ob #[LiveComponent] Attribute vorhanden

    // Ensure component is registered
    #[LiveComponent(name: 'datatable')]
    final class DataTableComponent { ... }
    
  3. Props: Gleiche Attribute-Namen funktionieren

    <!-- Old -->
    <component name="datatable" page="1" pageSize="25" />
    
    <!-- New -->
    <x-datatable page="1" pageSize="25" />
    

Troubleshooting

Problem: Component wird nicht gefunden

Unknown component: <x-my-component>
Available LiveComponents: datatable, counter
Available HTML Components: button, card

Lösung:

  1. Prüfe Attribute: #[LiveComponent(name: 'my-component')] oder #[ComponentName('my-component')]
  2. Prüfe Auto-Discovery: php console.php framework:discover
  3. Cache leeren: php console.php cache:clear

Problem: Props werden nicht übergeben

<x-counter initialValue="10" />
<!-- Component erhält initialValue nicht -->

Lösung:

  1. Prüfe #[LiveProp] Attribute auf Property
  2. Prüfe ComponentMetadata Cache
  3. Development Mode aktivieren für Validation Errors

Problem: Type Coercion funktioniert nicht

<x-chart data="[1,2,3]" />
<!-- Component erhält string statt array -->

Lösung:

  1. HTML Component? → Keine Type Coercion, nur LiveComponents!
  2. JSON valid? → Teste mit json_decode('[1,2,3]')
  3. Alternativ: Props als separate Attribute

Problem: Component rendered nicht in Production

Ursache: Silent error handling in production mode

Lösung:

  1. Development Mode aktivieren: APP_ENV=development
  2. Error logs prüfen
  3. Component-Validation durchführen

Framework-Compliance

Der XComponentProcessor folgt allen Framework-Patterns:

readonly class - Unveränderlichkeit final - Keine Vererbung Composition - Dependencies via Constructor Injection Interface Implementation - DomProcessor Interface Value Objects - ComponentId, ComponentData, RenderData Attribute Discovery - Automatische Component Registration Error Handling - Graceful degradation (dev/prod modes) Type Safety - Strict types via declare(strict_types=1)

Zusammenfassung

XComponentProcessor bietet:

  • Unified Syntax - <x-*> für alle Component-Typen
  • Auto-Detection - LiveComponent vs HTML Component
  • Type Coercion - Automatische Typ-Konvertierung (LiveComponents)
  • Prop Validation - Gegen ComponentMetadata (LiveComponents)
  • Error Handling - Development vs Production Modes
  • Performance - Optimized mit Caching
  • Framework-Compliant - Alle Patterns korrekt umgesetzt

Use Cases:

  • Interactive Components: DataTables, Forms, Counters, Charts
  • Static Components: Buttons, Cards, Badges, Icons
  • Unified Developer Experience: Eine Syntax für beide Typen
  • Progressive Enhancement: Start mit HTML Component, upgrade zu LiveComponent