Files
michaelschiemer/docs/claude/view-refactoring-plan.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

16 KiB

View Module Refactoring Plan

Inspired by Tempest Framework - Modernizing the View System with improved syntax and developer experience.

🎯 Goals

  1. Modern Placeholder Syntax: {{ $variable }} statt {variable} (PHP-like, IDE-friendly)
  2. Component Syntax: <x-datatable /> für LiveComponents (Laravel/Tempest-like)
  3. Expression Attributes: :attribute für dynamische Props (Vue/Alpine-like)
  4. View Compilation: Template-Compilation für Performance
  5. Better DX: Improved developer experience und tooling support

📋 Refactoring Phases

Phase 1: Placeholder Syntax Migration 🔄

Current State

<!-- Escaped output -->
{{ item.name }}

<!-- Unescaped output -->
{{{ content }}}

<!-- Functions -->
{{ format_date('Y-m-d') }}

Target State (Tempest-inspired)

<!-- Escaped output with $ prefix -->
{{ $item->name }}

<!-- Unescaped output -->
{!! $content !!}

<!-- Functions (unchanged) -->
{{ format_date('Y-m-d') }}

Benefits

  • PHP-like syntax: $variable ist vertrauter für PHP-Entwickler
  • IDE Support: PHPStorm/VSCode erkennen $ als Variable
  • Clear distinction: {{ $var }} (escaped) vs {!! $var !!} (unescaped)
  • Tempest compatibility: Ähnliche Syntax wie Tempest Framework

Implementation Tasks

  1. Update PlaceholderReplacer regex patterns
  2. Support both {{ variable }} (dotted) and {{ $variable }} (PHP-like) syntax
  3. Add {!! $variable !!} for unescaped output
  4. Deprecate {{{ variable }}} syntax (keep for backward compatibility)
  5. Write tests for new syntax patterns

Backward Compatibility Strategy

// Phase 1.1: Support BOTH syntaxes simultaneously
{{ item.name }}        // OLD - still works
{{ $item->name }}      // NEW - preferred

// Phase 1.2: Add deprecation warnings (optional)
// Phase 1.3: Migration script to convert old syntax
// Phase 1.4: Remove old syntax support (major version)

Phase 2: Component Syntax (<x-*>) 🧩

Current State

<!-- Verbose include syntax -->
<include template="livecomponent-datatable" data="{id: 'demo', page: 1}" />

Target State

<!-- Clean component syntax -->
<x-datatable id="demo" page="1" pageSize="25" />

XComponentProcessor Architecture

Input HTML
    ↓
XComponentProcessor (DOM Processor)
    ↓
1. Find all <x-*> elements via XPath
2. Extract component name: "datatable"
3. Extract props: {id: "demo", page: 1, pageSize: 25}
4. Validate props via ComponentMetadata
5. Create ComponentId + ComponentData
6. Resolve via ComponentRegistry
7. Render with LiveComponentRenderer
8. Replace <x-datatable> with rendered HTML
    ↓
Output HTML (with LiveComponent wrapper)

Implementation Tasks

  1. Create XComponentProcessor (implements DomProcessor)
  2. XPath-based element discovery: //*[starts-with(local-name(), 'x-')]
  3. Prop extraction from HTML attributes
  4. Type coercion: "123"123, "true"true, "[1,2,3]"[1,2,3]
  5. Prop validation via ComponentMetadataCache
  6. Integration with existing ComponentRegistry
  7. Support for nested components and slots

Prop Type Coercion Examples

<x-datatable
    id="users"           <!-- string: "users" -->
    page="1"             <!-- int: 1 -->
    pageSize="25"        <!-- int: 25 -->
    sortAsc="true"       <!-- bool: true -->
    filters='["active"]' <!-- array: ["active"] -->
    meta='{"total":100}' <!-- object: {total: 100} -->
/>
// XComponentProcessor::coerceType()
private function coerceType(string $value): mixed
{
    // Boolean
    if ($value === 'true') return true;
    if ($value === 'false') return false;

    // Null
    if ($value === 'null') return null;

    // Numeric
    if (is_numeric($value)) {
        return str_contains($value, '.') ? (float)$value : (int)$value;
    }

    // JSON (arrays/objects)
    if (str_starts_with($value, '[') || str_starts_with($value, '{')) {
        try {
            return json_decode($value, true, 512, JSON_THROW_ON_ERROR);
        } catch (\JsonException) {
            // Fallback to string
        }
    }

    return $value; // String
}

Prop Validation

// Validate props against ComponentMetadata
private function validateProps(string $componentName, array $props): void
{
    $className = $this->registry->getClassName($componentName);
    $metadata = $this->metadataCache->get($className);

    foreach ($props as $propName => $value) {
        if (!$metadata->hasProperty($propName)) {
            throw new InvalidComponentPropertyException(
                "Component '{$componentName}' has no property '{$propName}'"
            );
        }

        // Optional: Type validation
        $propertyMetadata = $metadata->getProperty($propName);
        $this->validatePropType($propName, $value, $propertyMetadata->type);
    }
}

Phase 3: Expression Attributes (:attribute)

Target State (Tempest-inspired)

<!-- Dynamic attributes with : prefix -->
<x-button
    :disabled="$user->isGuest()"
    :class="$isActive ? 'active' : 'inactive'"
    :data-user-id="$user->id"
>
    Click me
</x-button>

<!-- Mix static and dynamic attributes -->
<x-card
    title="User Profile"              <!-- static -->
    :subtitle="$user->name"           <!-- dynamic -->
    :visible="$user->isActive()"      <!-- dynamic expression -->
/>

Implementation

// XComponentProcessor enhancement
private function extractProps(HTMLElement $element, RenderContext $context): array
{
    $props = [];

    foreach ($element->attributes as $attr) {
        $name = $attr->nodeName;
        $value = $attr->nodeValue;

        // Expression attribute (starts with :)
        if (str_starts_with($name, ':')) {
            $propName = substr($name, 1); // Remove :
            $props[$propName] = $this->evaluateExpression($value, $context);
        } else {
            // Static attribute
            $props[$name] = $this->coerceType($value);
        }
    }

    return $props;
}

private function evaluateExpression(string $expression, RenderContext $context): mixed
{
    // Simple expression evaluation
    // Could use Symfony Expression Language or simple eval (careful!)

    // For now: Support $variable references and method calls
    if (preg_match('/^\$(\w+)(?:->(\w+)\(\))?$/', $expression, $matches)) {
        $varName = $matches[1];
        $method = $matches[2] ?? null;

        if (isset($context->data[$varName])) {
            $value = $context->data[$varName];

            if ($method && method_exists($value, $method)) {
                return $value->$method();
            }

            return $value;
        }
    }

    // Fallback: return as string
    return $expression;
}

Phase 4: View Compilation 🚀

Current State

  • Templates werden bei jedem Request verarbeitet
  • String + DOM Processors laufen sequentiell
  • Keine Caching der verarbeiteten Templates

Target State (Tempest-inspired)

// Compiled view cache
class CompiledViewCache
{
    public function compile(string $templatePath): string
    {
        // 1. Load template
        $html = file_get_contents($templatePath);

        // 2. Process with all processors
        $context = RenderContext::create([]);
        $processed = $this->templateProcessor->render($context, $html);

        // 3. Convert to PHP code
        $phpCode = $this->convertToPhp($processed);

        // 4. Cache compiled template
        $cacheKey = md5($templatePath);
        file_put_contents(
            $this->cacheDir . '/' . $cacheKey . '.php',
            "<?php\n" . $phpCode
        );

        return $phpCode;
    }

    private function convertToPhp(string $html): string
    {
        // Convert {{ $var }} to <?= htmlspecialchars($var) ?>
        // Convert {!! $var !!} to <?= $var ?>
        // Keep static HTML as-is

        $php = preg_replace_callback(
            '/\{\{\s*\$(\w+)\s*\}\}/',
            fn($m) => '<?= htmlspecialchars($' . $m[1] . ') ?>',
            $html
        );

        $php = preg_replace_callback(
            '/\{!!\s*\$(\w+)\s*!!\}/',
            fn($m) => '<?= $' . $m[1] . ' ?>',
            $php
        );

        return $php;
    }
}

Benefits

  • Performance: Compiled templates ~10x faster
  • Production optimization: Skip processing in production
  • Dev mode: Auto-recompile on changes
  • Debugging: View compiled PHP code

Phase 5: Component File Discovery 📁

Target State (Tempest-style)

resources/views/
├── components/
│   ├── x-button.view.php        # Auto-discovered
│   ├── x-card.view.php          # Auto-discovered
│   └── x-datatable.view.php     # Auto-discovered
└── pages/
    └── dashboard.view.php

Auto-Discovery Logic

#[Initializer]
class ComponentDiscoveryInitializer
{
    public function discoverXComponents(): void
    {
        $viewPath = $this->config->get('view.path');
        $componentPath = $viewPath . '/components';

        // Find all x-*.view.php files
        $files = glob($componentPath . '/x-*.view.php');

        foreach ($files as $file) {
            $filename = basename($file);

            // Extract component name: x-button.view.php -> button
            preg_match('/^x-(.+)\.view\.php$/', $filename, $matches);
            $componentName = $matches[1];

            // Register as ViewComponent
            $this->registry->registerFileComponent(
                name: $componentName,
                templatePath: $file
            );
        }
    }
}

🔄 Migration Strategy

Step 1: Backward Compatibility Phase (v1.x)

  • Support BOTH old and new syntax
  • No breaking changes
  • Add deprecation warnings (optional)
<!-- Both work side-by-side -->
{{ item.name }}          <!-- OLD - deprecated but works -->
{{ $item->name }}        <!-- NEW - preferred -->

<include template="livecomponent-counter" />  <!-- OLD -->
<x-counter />                                  <!-- NEW -->

Step 2: Migration Script

# Automated migration for templates
php console.php view:migrate-syntax
// Migration logic
class SyntaxMigrator
{
    public function migrateTemplate(string $path): void
    {
        $content = file_get_contents($path);

        // 1. Migrate placeholders: {variable} -> {{ $variable }}
        $content = $this->migratePlaceholders($content);

        // 2. Migrate components: <include> -> <x-*>
        $content = $this->migrateComponents($content);

        // 3. Migrate unescaped: {{{ var }}} -> {!! $var !!}
        $content = $this->migrateUnescaped($content);

        file_put_contents($path, $content);
    }

    private function migratePlaceholders(string $html): string
    {
        // {{ item.name }} -> {{ $item->name }}
        return preg_replace_callback(
            '/\{\{\s*([\w.]+)\s*\}\}/',
            function($m) {
                $dotted = $m[1];
                $phpStyle = $this->convertDottedToPhp($dotted);
                return '{{ ' . $phpStyle . ' }}';
            },
            $html
        );
    }

    private function convertDottedToPhp(string $dotted): string
    {
        // item.name -> $item->name
        // user.profile.email -> $user->profile->email
        $parts = explode('.', $dotted);
        return '$' . implode('->', $parts);
    }
}

Step 3: Major Version (v2.0)

  • Remove old syntax support
  • Only new syntax works
  • Breaking change - major version bump

📊 Comparison: Current vs Target

Feature Current Target Benefit
Placeholder Syntax {variable} {{ $variable }} PHP-like, IDE support
Unescaped Output {{{ var }}} {!! $var !!} Clearer, Blade-like
Components <include template="..."/> <x-datatable /> Shorter, cleaner
Dynamic Props Not supported :disabled="expr" Vue/Alpine-like DX
View Compilation Runtime only Compiled + cached 10x performance
Component Discovery Manual registration Auto-discovery Less boilerplate
Type Coercion Manual Automatic Less bugs
Prop Validation Runtime errors Compile-time checks Better DX

🧪 Testing Strategy

Unit Tests

describe('PlaceholderReplacer', function() {
    it('processes new placeholder syntax', function() {
        $html = '{{ $user->name }}';
        $context = RenderContext::create(['user' => $user]);

        $result = $this->replacer->process($html, $context);

        expect($result)->toBe('John Doe');
    });

    it('processes unescaped output', function() {
        $html = '{!! $htmlContent !!}';
        $context = RenderContext::create(['htmlContent' => '<b>Bold</b>']);

        $result = $this->replacer->process($html, $context);

        expect($result)->toBe('<b>Bold</b>');
    });
});

describe('XComponentProcessor', function() {
    it('processes x-components', function() {
        $html = '<x-datatable id="users" page="1" />';
        $context = RenderContext::create([]);

        $result = $this->processor->process($dom, $context);

        expect($result)->toContain('data-component-id="datatable:users"');
    });

    it('validates component props', function() {
        $html = '<x-datatable invalidProp="test" />';

        expect(fn() => $this->processor->process($dom, $context))
            ->toThrow(InvalidComponentPropertyException::class);
    });
});

Integration Tests

it('renders full page with new syntax', function() {
    $html = <<<HTML
    <h1>{{ $page->title }}</h1>

    <x-datatable
        id="users"
        :page="$currentPage"
        :sortBy="$sortColumn"
    />

    <div>{!! $page->content !!}</div>
    HTML;

    $result = $this->engine->render('dashboard', [
        'page' => $page,
        'currentPage' => 1,
        'sortColumn' => 'name'
    ]);

    expect($result)->toContain('<h1>Dashboard</h1>');
    expect($result)->toContain('data-component-id="datatable:users"');
});

📚 Documentation Updates

1. Update Template Syntax Guide

# Template Syntax

## Variables

### Escaped Output (Default)
Use `{{ $variable }}` for HTML-escaped output:

{{ $user->name }}
{{ $product->price }}

### Unescaped Output
Use `{!! $variable !!}` for raw HTML:

{!! $page->content !!}
{!! $richText !!}

## Components

### Basic Component
<x-button id="submit">Submit</x-button>

### Component with Props
<x-datatable
    id="users"
    page="1"
    pageSize="25"
/>

### Dynamic Props
<x-card
    :title="$page->title"
    :visible="$user->isAdmin()"
/>

2. Migration Guide

# Migrating to New Syntax

## Automatic Migration

php console.php view:migrate-syntax

## Manual Migration

### Placeholders
Before: {{ user.name }}
After:  {{ $user->name }}

### Unescaped
Before: {{{ content }}}
After:  {!! $content !!}

### Components
Before: <include template="livecomponent-button" />
After:  <x-button />

🎯 Implementation Priority

High Priority (Week 1-2)

  1. Phase 1: Placeholder Syntax ({{ $variable }})
  2. Phase 2: XComponentProcessor (<x-datatable />)
  3. Backward compatibility support

Medium Priority (Week 3-4)

  1. Phase 3: Expression Attributes (:attribute)
  2. Phase 4: View Compilation
  3. Testing & Documentation

Low Priority (Week 5+)

  1. Component Auto-Discovery
  2. Advanced Features (dynamic components, etc.)
  3. Migration Script
  4. Remove old syntax (v2.0)

🚀 Next Steps

Ready to start?

  1. Phase 1: Update PlaceholderReplacer for {{ $variable }} syntax
  2. Phase 2: Implement XComponentProcessor for <x-datatable /> syntax
  3. Testing: Write comprehensive tests
  4. Documentation: Update guides with examples

Your Input:

  • Soll ich mit Phase 1 (Placeholder Syntax) oder Phase 2 (XComponentProcessor) starten?
  • Backward compatibility: Wie lange sollen wir beide Syntaxen supporten?
  • Migration: Automated script oder manual migration?