- 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.
16 KiB
16 KiB
View Module Refactoring Plan
Inspired by Tempest Framework - Modernizing the View System with improved syntax and developer experience.
🎯 Goals
- Modern Placeholder Syntax:
{{ $variable }}statt{variable}(PHP-like, IDE-friendly) - Component Syntax:
<x-datatable />für LiveComponents (Laravel/Tempest-like) - Expression Attributes:
:attributefür dynamische Props (Vue/Alpine-like) - View Compilation: Template-Compilation für Performance
- 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:
$variableist 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
- ✅ Update
PlaceholderReplacerregex patterns - ✅ Support both
{{ variable }}(dotted) and{{ $variable }}(PHP-like) syntax - ✅ Add
{!! $variable !!}for unescaped output - ✅ Deprecate
{{{ variable }}}syntax (keep for backward compatibility) - ✅ 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
- ✅ Create
XComponentProcessor(implementsDomProcessor) - ✅ XPath-based element discovery:
//*[starts-with(local-name(), 'x-')] - ✅ Prop extraction from HTML attributes
- ✅ Type coercion:
"123"→123,"true"→true,"[1,2,3]"→[1,2,3] - ✅ Prop validation via
ComponentMetadataCache - ✅ Integration with existing
ComponentRegistry - ✅ 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)
- ✅ Phase 1: Placeholder Syntax (
{{ $variable }}) - ✅ Phase 2: XComponentProcessor (
<x-datatable />) - ✅ Backward compatibility support
Medium Priority (Week 3-4)
- ✅ Phase 3: Expression Attributes (
:attribute) - ✅ Phase 4: View Compilation
- ✅ Testing & Documentation
Low Priority (Week 5+)
- ⏳ Component Auto-Discovery
- ⏳ Advanced Features (dynamic components, etc.)
- ⏳ Migration Script
- ⏳ Remove old syntax (v2.0)
🚀 Next Steps
Ready to start?
- Phase 1: Update
PlaceholderReplacerfor{{ $variable }}syntax - Phase 2: Implement
XComponentProcessorfor<x-datatable />syntax - Testing: Write comprehensive tests
- 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?