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,423 @@
<?php
declare(strict_types=1);
use App\Framework\LiveComponents\Contracts\SupportsSlots;
use App\Framework\LiveComponents\SlotManager;
use App\Framework\LiveComponents\ValueObjects\ComponentId;
use App\Framework\LiveComponents\ValueObjects\SlotContent;
use App\Framework\LiveComponents\ValueObjects\SlotContext;
use App\Framework\LiveComponents\ValueObjects\SlotDefinition;
describe('SlotManager', function () {
beforeEach(function () {
$this->slotManager = new SlotManager();
});
it('registers and retrieves slot contents', function () {
$componentId = ComponentId::generate();
$contents = [
SlotContent::named('header', '<h1>Header</h1>'),
SlotContent::named('body', '<p>Body</p>'),
];
$this->slotManager->registerSlotContents($componentId, $contents);
$retrieved = $this->slotManager->getSlotContents($componentId);
expect($retrieved)->toHaveCount(2);
expect($retrieved[0]->slotName)->toBe('header');
expect($retrieved[1]->slotName)->toBe('body');
});
it('resolves provided content over default content', function () {
$component = new class () implements SupportsSlots {
public function getSlotDefinitions(): array
{
return [
SlotDefinition::named('header', '<h2>Default Header</h2>'),
];
}
public function getSlotContext(string $slotName): SlotContext
{
return SlotContext::empty();
}
public function processSlotContent(SlotContent $content): SlotContent
{
return $content;
}
public function validateSlots(array $providedSlots): array
{
return [];
}
};
$definition = SlotDefinition::named('header', '<h2>Default Header</h2>');
$providedContent = [
SlotContent::named('header', '<h1>Custom Header</h1>'),
];
$result = $this->slotManager->resolveSlotContent(
$component,
$definition,
$providedContent
);
expect($result)->toBe('<h1>Custom Header</h1>');
});
it('uses default content when no content provided', function () {
$component = new class () implements SupportsSlots {
public function getSlotDefinitions(): array
{
return [
SlotDefinition::named('header', '<h2>Default Header</h2>'),
];
}
public function getSlotContext(string $slotName): SlotContext
{
return SlotContext::empty();
}
public function processSlotContent(SlotContent $content): SlotContent
{
return $content;
}
public function validateSlots(array $providedSlots): array
{
return [];
}
};
$definition = SlotDefinition::named('header', '<h2>Default Header</h2>');
$providedContent = [];
$result = $this->slotManager->resolveSlotContent(
$component,
$definition,
$providedContent
);
expect($result)->toBe('<h2>Default Header</h2>');
});
it('injects scoped context into slot content', function () {
$component = new class () implements SupportsSlots {
public function getSlotDefinitions(): array
{
return [
SlotDefinition::scoped('content', ['userId', 'userName']),
];
}
public function getSlotContext(string $slotName): SlotContext
{
return SlotContext::create([
'userId' => 123,
'userName' => 'John Doe',
]);
}
public function processSlotContent(SlotContent $content): SlotContent
{
return $content;
}
public function validateSlots(array $providedSlots): array
{
return [];
}
};
$definition = SlotDefinition::scoped('content', ['userId', 'userName']);
$providedContent = [
SlotContent::named('content', '<p>User: {context.userName} (ID: {context.userId})</p>'),
];
$result = $this->slotManager->resolveSlotContent(
$component,
$definition,
$providedContent
);
expect($result)->toBe('<p>User: John Doe (ID: 123)</p>');
});
it('validates required slots', function () {
$component = new class () implements SupportsSlots {
public function getSlotDefinitions(): array
{
return [
SlotDefinition::named('body')->withRequired(true),
SlotDefinition::named('footer'),
];
}
public function getSlotContext(string $slotName): SlotContext
{
return SlotContext::empty();
}
public function processSlotContent(SlotContent $content): SlotContent
{
return $content;
}
public function validateSlots(array $providedSlots): array
{
return [];
}
};
// No content provided
$errors = $this->slotManager->validateSlots($component, []);
expect($errors)->toContain("Required slot 'body' is not filled");
});
it('validates slots with content provided', function () {
$component = new class () implements SupportsSlots {
public function getSlotDefinitions(): array
{
return [
SlotDefinition::named('body')->withRequired(true),
];
}
public function getSlotContext(string $slotName): SlotContext
{
return SlotContext::empty();
}
public function processSlotContent(SlotContent $content): SlotContent
{
return $content;
}
public function validateSlots(array $providedSlots): array
{
return [];
}
};
$providedContent = [
SlotContent::named('body', '<p>Body content</p>'),
];
$errors = $this->slotManager->validateSlots($component, $providedContent);
expect($errors)->toBeEmpty();
});
it('checks if component has specific slot', function () {
$component = new class () implements SupportsSlots {
public function getSlotDefinitions(): array
{
return [
SlotDefinition::named('header'),
SlotDefinition::named('body'),
];
}
public function getSlotContext(string $slotName): SlotContext
{
return SlotContext::empty();
}
public function processSlotContent(SlotContent $content): SlotContent
{
return $content;
}
public function validateSlots(array $providedSlots): array
{
return [];
}
};
expect($this->slotManager->hasSlot($component, 'header'))->toBeTrue();
expect($this->slotManager->hasSlot($component, 'body'))->toBeTrue();
expect($this->slotManager->hasSlot($component, 'footer'))->toBeFalse();
});
it('gets slot definition by name', function () {
$component = new class () implements SupportsSlots {
public function getSlotDefinitions(): array
{
return [
SlotDefinition::named('header', '<h1>Default</h1>'),
];
}
public function getSlotContext(string $slotName): SlotContext
{
return SlotContext::empty();
}
public function processSlotContent(SlotContent $content): SlotContent
{
return $content;
}
public function validateSlots(array $providedSlots): array
{
return [];
}
};
$definition = $this->slotManager->getSlotDefinition($component, 'header');
expect($definition)->not->toBeNull();
expect($definition->name)->toBe('header');
expect($definition->defaultContent)->toBe('<h1>Default</h1>');
});
it('returns null for unknown slot', function () {
$component = new class () implements SupportsSlots {
public function getSlotDefinitions(): array
{
return [];
}
public function getSlotContext(string $slotName): SlotContext
{
return SlotContext::empty();
}
public function processSlotContent(SlotContent $content): SlotContent
{
return $content;
}
public function validateSlots(array $providedSlots): array
{
return [];
}
};
$definition = $this->slotManager->getSlotDefinition($component, 'unknown');
expect($definition)->toBeNull();
});
it('processes slot content through component hook', function () {
$component = new class () implements SupportsSlots {
public function getSlotDefinitions(): array
{
return [
SlotDefinition::named('body'),
];
}
public function getSlotContext(string $slotName): SlotContext
{
return SlotContext::empty();
}
public function processSlotContent(SlotContent $content): SlotContent
{
// Wrap content in div
return $content->withContent('<div class="wrapper">' . $content->content . '</div>');
}
public function validateSlots(array $providedSlots): array
{
return [];
}
};
$definition = SlotDefinition::named('body');
$providedContent = [
SlotContent::named('body', '<p>Content</p>'),
];
$result = $this->slotManager->resolveSlotContent(
$component,
$definition,
$providedContent
);
expect($result)->toBe('<div class="wrapper"><p>Content</p></div>');
});
it('escapes HTML in scoped context values', function () {
$component = new class () implements SupportsSlots {
public function getSlotDefinitions(): array
{
return [
SlotDefinition::scoped('content', ['userInput']),
];
}
public function getSlotContext(string $slotName): SlotContext
{
return SlotContext::create([
'userInput' => '<script>alert("XSS")</script>',
]);
}
public function processSlotContent(SlotContent $content): SlotContent
{
return $content;
}
public function validateSlots(array $providedSlots): array
{
return [];
}
};
$definition = SlotDefinition::scoped('content', ['userInput']);
$providedContent = [
SlotContent::named('content', '<p>{context.userInput}</p>'),
];
$result = $this->slotManager->resolveSlotContent(
$component,
$definition,
$providedContent
);
expect($result)->toContain('&lt;script&gt;');
expect($result)->not->toContain('<script>');
});
it('provides slot statistics', function () {
$componentId1 = ComponentId::generate();
$componentId2 = ComponentId::generate();
$this->slotManager->registerSlotContents($componentId1, [
SlotContent::named('header', '<h1>H1</h1>'),
SlotContent::named('body', '<p>Body</p>'),
]);
$this->slotManager->registerSlotContents($componentId2, [
SlotContent::named('footer', '<footer>Footer</footer>'),
]);
$stats = $this->slotManager->getStats();
expect($stats['total_components_with_slots'])->toBe(2);
expect($stats['total_slot_contents'])->toBe(3);
expect($stats['avg_slots_per_component'])->toBe(1.5);
});
it('clears all registered slot contents', function () {
$componentId = ComponentId::generate();
$this->slotManager->registerSlotContents($componentId, [
SlotContent::named('header', '<h1>Header</h1>'),
]);
expect($this->slotManager->getSlotContents($componentId))->toHaveCount(1);
$this->slotManager->clear();
expect($this->slotManager->getSlotContents($componentId))->toBeEmpty();
});
});