- 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.
615 lines
16 KiB
Markdown
615 lines
16 KiB
Markdown
# 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
|
|
```html
|
|
<!-- Escaped output -->
|
|
{{ item.name }}
|
|
|
|
<!-- Unescaped output -->
|
|
{{{ content }}}
|
|
|
|
<!-- Functions -->
|
|
{{ format_date('Y-m-d') }}
|
|
```
|
|
|
|
#### Target State (Tempest-inspired)
|
|
```html
|
|
<!-- 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
|
|
```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 (`<x-*>`)** 🧩
|
|
|
|
#### Current State
|
|
```html
|
|
<!-- Verbose include syntax -->
|
|
<include template="livecomponent-datatable" data="{id: 'demo', page: 1}" />
|
|
```
|
|
|
|
#### Target State
|
|
```html
|
|
<!-- 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
|
|
```html
|
|
<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} -->
|
|
/>
|
|
```
|
|
|
|
```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
|
|
<!-- 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
|
|
```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',
|
|
"<?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
|
|
```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
|
|
<!-- 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
|
|
```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: <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
|
|
```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' => '<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
|
|
```php
|
|
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
|
|
```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
|
|
<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
|
|
```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: <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)
|
|
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 `<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?
|