# 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**: `` 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 ```html {{ item.name }} {{{ content }}} {{ format_date('Y-m-d') }} ``` #### Target State (Tempest-inspired) ```html {{ $item->name }} {!! $content !!} {{ 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 ```php // 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 (``)** 🧩 #### Current State ```html ``` #### Target State ```html ``` #### XComponentProcessor Architecture ``` Input HTML ↓ XComponentProcessor (DOM Processor) ↓ 1. Find all 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 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 ```html page="1" pageSize="25" sortAsc="true" filters='["active"]' meta='{"total":100}' /> ``` ```php // 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 ```php // 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) ```html Click me :subtitle="$user->name" :visible="$user->isActive()" /> ``` #### Implementation ```php // 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) ```php // 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', " // Convert {!! $var !!} to // Keep static HTML as-is $php = preg_replace_callback( '/\{\{\s*\$(\w+)\s*\}\}/', fn($m) => '', $html ); $php = preg_replace_callback( '/\{!!\s*\$(\w+)\s*!!\}/', fn($m) => '', $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 ```php #[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) ```html {{ item.name }} {{ $item->name }} ``` ### Step 2: Migration Script ```bash # Automated migration for templates php console.php view:migrate-syntax ``` ```php // 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: -> $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** | `` | `` | 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 ```php 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' => 'Bold']); $result = $this->replacer->process($html, $context); expect($result)->toBe('Bold'); }); }); describe('XComponentProcessor', function() { it('processes x-components', function() { $html = ''; $context = RenderContext::create([]); $result = $this->processor->process($dom, $context); expect($result)->toContain('data-component-id="datatable:users"'); }); it('validates component props', function() { $html = ''; expect(fn() => $this->processor->process($dom, $context)) ->toThrow(InvalidComponentPropertyException::class); }); }); ``` ### Integration Tests ```php it('renders full page with new syntax', function() { $html = <<{{ $page->title }}
{!! $page->content !!}
HTML; $result = $this->engine->render('dashboard', [ 'page' => $page, 'currentPage' => 1, 'sortColumn' => 'name' ]); expect($result)->toContain('

Dashboard

'); expect($result)->toContain('data-component-id="datatable:users"'); }); ``` --- ## πŸ“š Documentation Updates ### 1. Update Template Syntax Guide ```markdown # 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 Submit ### Component with Props ### Dynamic Props ``` ### 2. Migration Guide ```markdown # 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: After: ``` --- ## 🎯 Implementation Priority ### **High Priority** (Week 1-2) 1. βœ… Phase 1: Placeholder Syntax (`{{ $variable }}`) 2. βœ… Phase 2: XComponentProcessor (``) 3. βœ… Backward compatibility support ### **Medium Priority** (Week 3-4) 4. βœ… Phase 3: Expression Attributes (`:attribute`) 5. βœ… Phase 4: View Compilation 6. βœ… Testing & Documentation ### **Low Priority** (Week 5+) 7. ⏳ Component Auto-Discovery 8. ⏳ Advanced Features (dynamic components, etc.) 9. ⏳ Migration Script 10. ⏳ Remove old syntax (v2.0) --- ## πŸš€ Next Steps **Ready to start?** 1. **Phase 1**: Update `PlaceholderReplacer` for `{{ $variable }}` syntax 2. **Phase 2**: Implement `XComponentProcessor` for `` 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?