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.
This commit is contained in:
2025-10-25 19:18:37 +02:00
parent caa85db796
commit fc3d7e6357
83016 changed files with 378904 additions and 20919 deletions

View File

@@ -0,0 +1,468 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Framework\View\Processors;
use App\Framework\LiveComponents\Contracts\ComponentRegistryInterface;
use App\Framework\LiveComponents\Contracts\LiveComponentContract;
use App\Framework\LiveComponents\Performance\CompiledComponentMetadata;
use App\Framework\LiveComponents\Performance\ComponentMetadataCacheInterface;
use App\Framework\LiveComponents\Performance\ComponentPropertyMetadata;
use App\Framework\LiveComponents\ValueObjects\ComponentData;
use App\Framework\LiveComponents\ValueObjects\ComponentId;
use App\Framework\LiveComponents\ValueObjects\ComponentRenderData;
use App\Framework\Meta\MetaData;
use App\Framework\Template\Parser\DomTemplateParser;
use App\Framework\View\Contracts\HtmlComponentRegistryInterface;
use App\Framework\View\DomComponentService;
use App\Framework\View\Processors\XComponentProcessor;
use App\Framework\View\RenderContext;
describe('XComponentProcessor', function () {
beforeEach(function () {
// Mock dependencies via Interfaces
$this->liveComponentRegistry = mock(ComponentRegistryInterface::class);
$this->htmlComponentRegistry = mock(HtmlComponentRegistryInterface::class);
$this->metadataCache = mock(ComponentMetadataCacheInterface::class);
$this->componentService = new DomComponentService();
$this->processor = new XComponentProcessor(
$this->liveComponentRegistry,
$this->htmlComponentRegistry,
$this->metadataCache,
$this->componentService
);
$this->parser = new DomTemplateParser();
});
describe('LiveComponent Processing', function () {
it('processes LiveComponent with basic props', function () {
$html = '<html><body><x-counter id="demo" initialValue="5" /></body></html>';
$dom = $this->parser->parseToWrapper($html);
// Mock LiveComponent
$mockComponent = mock(LiveComponentContract::class);
$mockComponent->shouldReceive('getId')
->andReturn(ComponentId::create('counter', 'demo'));
$mockComponent->shouldReceive('getData')
->andReturn(ComponentData::fromArray(['initialValue' => 5]));
$mockComponent->shouldReceive('getRenderData')
->andReturn(new ComponentRenderData('counter-template', ['value' => 5]));
// Setup registry mocks
$this->liveComponentRegistry->shouldReceive('isRegistered')
->with('counter')
->andReturn(true);
$this->liveComponentRegistry->shouldReceive('getClassName')
->with('counter')
->andReturn('TestCounterComponent');
$this->liveComponentRegistry->shouldReceive('resolve')
->once()
->andReturn($mockComponent);
$this->liveComponentRegistry->shouldReceive('renderWithWrapper')
->with($mockComponent)
->andReturn('<div data-component-id="counter:demo">Counter HTML</div>');
// Mock metadata for validation
$mockMetadata = new CompiledComponentMetadata(
className: 'TestCounterComponent',
componentName: 'counter',
properties: [
'initialValue' => new ComponentPropertyMetadata(
name: 'initialValue',
type: 'int',
isPublic: true,
isReadonly: false
)
],
actions: [],
constructorParams: []
);
$this->metadataCache->shouldReceive('get')
->with('TestCounterComponent')
->andReturn($mockMetadata);
// Process
$context = new RenderContext(
template: 'test-template',
metaData: new MetaData('Test Component Processing'),
data: []
);
$result = $this->processor->process($dom, $context);
// Assert
$html = $result->document->saveHTML();
expect($html)->toContain('data-component-id="counter:demo"');
expect($html)->toContain('Counter HTML');
});
it('coerces prop types correctly', function () {
$html = '<html><body><x-test
stringProp="text"
intProp="123"
floatProp="12.5"
boolTrue="true"
boolFalse="false"
nullProp="null"
arrayProp="[1,2,3]"
/></body></html>';
$dom = $this->parser->parseToWrapper($html);
$capturedProps = null;
$mockComponent = mock(LiveComponentContract::class);
$mockComponent->shouldReceive('getId')
->andReturn(ComponentId::create('test', 'test-auto'));
$mockComponent->shouldReceive('getData')
->andReturn(ComponentData::fromArray([]));
$mockComponent->shouldReceive('getRenderData')
->andReturn(new ComponentRenderData('test-template', []));
$this->liveComponentRegistry->shouldReceive('isRegistered')
->with('test')
->andReturn(true);
$this->liveComponentRegistry->shouldReceive('getClassName')
->with('test')
->andReturn('TestComponent');
$this->liveComponentRegistry->shouldReceive('resolve')
->with(\Mockery::type(ComponentId::class), \Mockery::on(function ($data) use (&$capturedProps) {
$capturedProps = $data->toArray();
return true;
}))
->andReturn($mockComponent);
$this->liveComponentRegistry->shouldReceive('renderWithWrapper')
->andReturn('<div>Test</div>');
// Mock metadata - accept all props
$mockMetadata = new CompiledComponentMetadata(
className: 'TestComponent',
componentName: 'test',
properties: [
'stringProp' => new ComponentPropertyMetadata('stringProp', 'string', true, false),
'intProp' => new ComponentPropertyMetadata('intProp', 'int', true, false),
'floatProp' => new ComponentPropertyMetadata('floatProp', 'float', true, false),
'boolTrue' => new ComponentPropertyMetadata('boolTrue', 'bool', true, false),
'boolFalse' => new ComponentPropertyMetadata('boolFalse', 'bool', true, false),
'nullProp' => new ComponentPropertyMetadata('nullProp', 'mixed', true, false),
'arrayProp' => new ComponentPropertyMetadata('arrayProp', 'array', true, false),
],
actions: [],
constructorParams: []
);
$this->metadataCache->shouldReceive('get')
->andReturn($mockMetadata);
// Process
$context = new RenderContext(
template: 'test-template',
metaData: new MetaData('Test Component Processing'),
data: []
);
$this->processor->process($dom, $context);
// Assert type coercion
expect($capturedProps['stringProp'])->toBe('text');
expect($capturedProps['intProp'])->toBe(123);
expect($capturedProps['floatProp'])->toBe(12.5);
expect($capturedProps['boolTrue'])->toBeTrue();
expect($capturedProps['boolFalse'])->toBeFalse();
expect($capturedProps['nullProp'])->toBeNull();
expect($capturedProps['arrayProp'])->toBe([1, 2, 3]);
});
it('validates props against ComponentMetadata', function () {
$html = '<html><body><x-counter id="demo" invalidProp="test" /></body></html>';
$dom = $this->parser->parseToWrapper($html);
$this->liveComponentRegistry->shouldReceive('isRegistered')
->with('counter')
->andReturn(true);
$this->liveComponentRegistry->shouldReceive('getClassName')
->with('counter')
->andReturn('TestCounterComponent');
// Mock metadata without invalidProp
$mockMetadata = new CompiledComponentMetadata(
className: 'TestCounterComponent',
componentName: 'counter',
properties: [
'initialValue' => new ComponentPropertyMetadata(
name: 'initialValue',
type: 'int',
isPublic: true,
isReadonly: false
)
],
actions: [],
constructorParams: []
);
$this->metadataCache->shouldReceive('get')
->with('TestCounterComponent')
->andReturn($mockMetadata);
// Process - should handle error gracefully in dev mode
$_ENV['APP_ENV'] = 'development';
$context = new RenderContext(
template: 'test-template',
metaData: new MetaData('Test Component Processing'),
data: []
);
$result = $this->processor->process($dom, $context);
// Should contain error message in development
$html = $result->document->saveHTML();
expect($html)->toContain('XComponentProcessor Error');
});
it('generates unique ID if not provided', function () {
$html = '<html><body><x-counter initialValue="0" /></body></html>';
$dom = $this->parser->parseToWrapper($html);
$mockComponent = mock(LiveComponentContract::class);
$mockComponent->shouldReceive('getId')
->andReturn(ComponentId::create('counter', 'counter-auto'));
$mockComponent->shouldReceive('getData')
->andReturn(ComponentData::fromArray([]));
$mockComponent->shouldReceive('getRenderData')
->andReturn(new ComponentRenderData('test', []));
$this->liveComponentRegistry->shouldReceive('isRegistered')
->andReturn(true);
$this->liveComponentRegistry->shouldReceive('getClassName')
->andReturn('TestComponent');
$this->liveComponentRegistry->shouldReceive('resolve')
->with(\Mockery::on(function ($id) {
// Should have auto-generated ID
return str_starts_with($id->toString(), 'counter:counter-');
}), \Mockery::any())
->andReturn($mockComponent);
$this->liveComponentRegistry->shouldReceive('renderWithWrapper')
->andReturn('<div>Counter</div>');
$mockMetadata = new CompiledComponentMetadata(
className: 'TestComponent',
componentName: 'counter',
properties: [
'initialValue' => new ComponentPropertyMetadata(
name: 'initialValue',
type: 'int',
isPublic: true,
isReadonly: false
)
],
actions: [],
constructorParams: []
);
$this->metadataCache->shouldReceive('get')->andReturn($mockMetadata);
$context = new RenderContext(
template: 'test-template',
metaData: new MetaData('Test Component Processing'),
data: []
);
$this->processor->process($dom, $context);
// Assertion in mock expectations above
expect(true)->toBeTrue();
});
});
describe('HTML Component Processing', function () {
it('processes HTML Component', function () {
$html = '<html><body><x-button variant="primary">Click me</x-button></body></html>';
$dom = $this->parser->parseToWrapper($html);
// LiveComponent not registered
$this->liveComponentRegistry->shouldReceive('isRegistered')
->with('button')
->andReturn(false);
// HTML Component registered
$this->htmlComponentRegistry->shouldReceive('has')
->with('button')
->andReturn(true);
$this->htmlComponentRegistry->shouldReceive('render')
->with('button', 'Click me', ['variant' => 'primary'])
->andReturn('<button class="btn btn-primary">Click me</button>');
// Process
$context = new RenderContext(
template: 'test-template',
metaData: new MetaData('Test Component Processing'),
data: []
);
$result = $this->processor->process($dom, $context);
// Assert
$html = $result->document->saveHTML();
expect($html)->toContain('<button class="btn btn-primary">Click me</button>');
});
it('extracts content from HTML Component', function () {
$html = '<html><body><x-card>Card content here</x-card></body></html>';
$dom = $this->parser->parseToWrapper($html);
$this->liveComponentRegistry->shouldReceive('isRegistered')
->andReturn(false);
$this->htmlComponentRegistry->shouldReceive('has')
->with('card')
->andReturn(true);
$capturedContent = null;
$this->htmlComponentRegistry->shouldReceive('render')
->with('card', \Mockery::capture($capturedContent), \Mockery::any())
->andReturn('<div class="card">Card content here</div>');
// Process
$context = new RenderContext(
template: 'test-template',
metaData: new MetaData('Test Component Processing'),
data: []
);
$this->processor->process($dom, $context);
// Assert content was captured
expect($capturedContent)->toBe('Card content here');
});
});
describe('Error Handling', function () {
it('shows helpful error when component not found', function () {
$html = '<html><body><x-unknown id="test" /></body></html>';
$dom = $this->parser->parseToWrapper($html);
// Neither registry has the component
$this->liveComponentRegistry->shouldReceive('isRegistered')
->with('unknown')
->andReturn(false);
$this->htmlComponentRegistry->shouldReceive('has')
->with('unknown')
->andReturn(false);
// Mock available components for error message
$this->liveComponentRegistry->shouldReceive('getAllComponentNames')
->andReturn(['counter', 'datatable']);
$this->htmlComponentRegistry->shouldReceive('getAllComponentNames')
->andReturn(['button', 'card']);
// Development mode
$_ENV['APP_ENV'] = 'development';
// Process
$context = new RenderContext(
template: 'test-template',
metaData: new MetaData('Test Component Processing'),
data: []
);
$result = $this->processor->process($dom, $context);
// Should show error with available components
$html = $result->document->saveHTML();
expect($html)->toContain('Unknown component');
expect($html)->toContain('counter, datatable');
expect($html)->toContain('button, card');
});
it('removes component silently in production', function () {
$html = '<html><body><p>Before</p><x-unknown /><p>After</p></body></html>';
$dom = $this->parser->parseToWrapper($html);
$this->liveComponentRegistry->shouldReceive('isRegistered')->andReturn(false);
$this->htmlComponentRegistry->shouldReceive('has')->andReturn(false);
$this->liveComponentRegistry->shouldReceive('getAllComponentNames')->andReturn([]);
$this->htmlComponentRegistry->shouldReceive('getAllComponentNames')->andReturn([]);
// Production mode
$_ENV['APP_ENV'] = 'production';
// Process
$context = new RenderContext(
template: 'test-template',
metaData: new MetaData('Test Component Processing'),
data: []
);
$result = $this->processor->process($dom, $context);
$html = $result->document->saveHTML();
// Component should be removed, but surrounding content remains
expect($html)->toContain('Before');
expect($html)->toContain('After');
expect($html)->not->toContain('x-unknown');
expect($html)->not->toContain('error'); // No error message
});
});
describe('Auto-Detection Priority', function () {
it('prioritizes LiveComponent over HTML Component', function () {
// Component registered in BOTH registries
$html = '<html><body><x-button id="test" /></body></html>';
$dom = $this->parser->parseToWrapper($html);
// BOTH return true
$this->liveComponentRegistry->shouldReceive('isRegistered')
->with('button')
->andReturn(true); // LiveComponent wins!
$mockComponent = mock(LiveComponentContract::class);
$mockComponent->shouldReceive('getId')
->andReturn(ComponentId::create('button', 'test'));
$mockComponent->shouldReceive('getData')
->andReturn(ComponentData::fromArray([]));
$mockComponent->shouldReceive('getRenderData')
->andReturn(new ComponentRenderData('test', []));
$this->liveComponentRegistry->shouldReceive('getClassName')
->andReturn('TestButtonComponent');
$this->liveComponentRegistry->shouldReceive('resolve')
->andReturn($mockComponent);
$this->liveComponentRegistry->shouldReceive('renderWithWrapper')
->with($mockComponent)
->andReturn('<button data-component-id="button:test">LiveComponent Button</button>');
$mockMetadata = new CompiledComponentMetadata(
className: 'TestButtonComponent',
componentName: 'button',
properties: [],
actions: [],
constructorParams: []
);
$this->metadataCache->shouldReceive('get')->andReturn($mockMetadata);
// HTML Component should NOT be called
$this->htmlComponentRegistry->shouldNotReceive('render');
// Process
$context = new RenderContext(
template: 'test-template',
metaData: new MetaData('Test Component Processing'),
data: []
);
$result = $this->processor->process($dom, $context);
$html = $result->document->saveHTML();
expect($html)->toContain('LiveComponent Button');
expect($html)->toContain('data-component-id');
});
});
});