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,199 @@
<?php
declare(strict_types=1);
use App\Framework\LiveComponents\Contracts\LiveComponent;
use App\Framework\LiveComponents\Rendering\FragmentRenderer;
use App\Framework\LiveComponents\ValueObjects\ComponentData;
use App\Framework\LiveComponents\ValueObjects\ComponentId;
use App\Framework\View\TemplateRenderer;
describe('FragmentRenderer', function () {
beforeEach(function () {
$this->templateRenderer = Mockery::mock(TemplateRenderer::class);
$this->fragmentRenderer = new FragmentRenderer($this->templateRenderer);
});
afterEach(function () {
Mockery::close();
});
it('renders single fragment from template', function () {
$component = Mockery::mock(LiveComponent::class);
$component->shouldReceive('getId')->andReturn(ComponentId::fromString('counter:demo'));
$component->shouldReceive('getData')->andReturn(ComponentData::fromArray(['count' => 5]));
$component->shouldReceive('getTemplateName')->andReturn('livecomponent-counter');
$fullHtml = <<<HTML
<div id="counter">
<div data-fragment="counter-display">
<span>Count: 5</span>
</div>
<div data-fragment="counter-controls">
<button>Increment</button>
</div>
</div>
HTML;
$this->templateRenderer
->shouldReceive('render')
->once()
->with('livecomponent-counter', ['count' => 5])
->andReturn($fullHtml);
$fragments = $this->fragmentRenderer->renderFragments(
$component,
['counter-display']
);
expect($fragments->isEmpty())->toBeFalse();
expect($fragments->has('counter-display'))->toBeTrue();
$displayFragment = $fragments->get('counter-display');
expect($displayFragment)->toContain('Count: 5');
expect($displayFragment)->toContain('span');
});
it('renders multiple fragments from template', function () {
$component = Mockery::mock(LiveComponent::class);
$component->shouldReceive('getId')->andReturn(ComponentId::fromString('counter:demo'));
$component->shouldReceive('getData')->andReturn(ComponentData::fromArray(['count' => 10]));
$component->shouldReceive('getTemplateName')->andReturn('livecomponent-counter');
$fullHtml = <<<HTML
<div id="counter">
<div data-fragment="counter-display">
<span>Count: 10</span>
</div>
<div data-fragment="counter-controls">
<button>Increment</button>
<button>Decrement</button>
</div>
</div>
HTML;
$this->templateRenderer
->shouldReceive('render')
->once()
->andReturn($fullHtml);
$fragments = $this->fragmentRenderer->renderFragments(
$component,
['counter-display', 'counter-controls']
);
expect($fragments->count())->toBe(2);
expect($fragments->has('counter-display'))->toBeTrue();
expect($fragments->has('counter-controls'))->toBeTrue();
expect($fragments->get('counter-display'))->toContain('Count: 10');
expect($fragments->get('counter-controls'))->toContain('Increment');
expect($fragments->get('counter-controls'))->toContain('Decrement');
});
it('returns empty collection when fragments not found', function () {
$component = Mockery::mock(LiveComponent::class);
$component->shouldReceive('getId')->andReturn(ComponentId::fromString('counter:demo'));
$component->shouldReceive('getData')->andReturn(ComponentData::fromArray([]));
$component->shouldReceive('getTemplateName')->andReturn('livecomponent-counter');
$fullHtml = '<div id="counter"><span>No fragments</span></div>';
$this->templateRenderer
->shouldReceive('render')
->once()
->andReturn($fullHtml);
$fragments = $this->fragmentRenderer->renderFragments(
$component,
['non-existent-fragment']
);
expect($fragments->isEmpty())->toBeTrue();
});
it('handles nested fragments correctly', function () {
$component = Mockery::mock(LiveComponent::class);
$component->shouldReceive('getId')->andReturn(ComponentId::fromString('form:demo'));
$component->shouldReceive('getData')->andReturn(ComponentData::fromArray([]));
$component->shouldReceive('getTemplateName')->andReturn('livecomponent-form');
$fullHtml = <<<HTML
<form data-fragment="form-wrapper">
<div data-fragment="form-fields">
<input type="text" name="email" />
<div data-fragment="form-errors">
<span class="error">Invalid email</span>
</div>
</div>
</form>
HTML;
$this->templateRenderer
->shouldReceive('render')
->once()
->andReturn($fullHtml);
$fragments = $this->fragmentRenderer->renderFragments(
$component,
['form-fields', 'form-errors']
);
expect($fragments->count())->toBe(2);
expect($fragments->has('form-fields'))->toBeTrue();
expect($fragments->has('form-errors'))->toBeTrue();
// Nested fragment should still be extractable
expect($fragments->get('form-errors'))->toContain('Invalid email');
});
it('preserves HTML structure in fragments', function () {
$component = Mockery::mock(LiveComponent::class);
$component->shouldReceive('getId')->andReturn(ComponentId::fromString('card:demo'));
$component->shouldReceive('getData')->andReturn(ComponentData::fromArray(['title' => 'Test Card']));
$component->shouldReceive('getTemplateName')->andReturn('livecomponent-card');
$fullHtml = <<<HTML
<div class="card">
<div data-fragment="card-header" class="card-header">
<h2>Test Card</h2>
<span class="badge">New</span>
</div>
<div data-fragment="card-body">
<p>Card content</p>
</div>
</div>
HTML;
$this->templateRenderer
->shouldReceive('render')
->once()
->andReturn($fullHtml);
$fragments = $this->fragmentRenderer->renderFragments(
$component,
['card-header']
);
$header = $fragments->get('card-header');
// Should preserve attributes and nested structure
expect($header)->toContain('class="card-header"');
expect($header)->toContain('<h2>Test Card</h2>');
expect($header)->toContain('class="badge"');
});
it('handles empty fragment list', function () {
$component = Mockery::mock(LiveComponent::class);
$component->shouldReceive('getId')->andReturn(ComponentId::fromString('test:demo'));
$component->shouldReceive('getData')->andReturn(ComponentData::fromArray([]));
$component->shouldReceive('getTemplateName')->andReturn('livecomponent-test');
// Should not call renderer if no fragments requested
$this->templateRenderer->shouldNotReceive('render');
$fragments = $this->fragmentRenderer->renderFragments($component, []);
expect($fragments->isEmpty())->toBeTrue();
});
});