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,112 @@
<?php
declare(strict_types=1);
namespace App\Framework\LiveComponents\Batch;
use App\Framework\LiveComponents\ValueObjects\ActionParameters;
/**
* Batch Operation Value Object
*
* Represents a single operation in a batch request.
*
* Example:
* {
* "componentId": "counter:demo",
* "method": "increment",
* "params": {"amount": 5},
* "fragments": ["counter-display"]
* }
*/
final readonly class BatchOperation
{
/**
* @param string $componentId - Component ID (e.g., "counter:demo")
* @param string $method - Action method name
* @param array $params - Action parameters
* @param array<string>|null $fragments - Optional fragment names to update
* @param string|null $operationId - Optional ID for tracking (client-generated)
*/
public function __construct(
public string $componentId,
public string $method,
public array $params = [],
public ?array $fragments = null,
public ?string $operationId = null
) {
$this->validate();
}
/**
* Create from array (from JSON request)
*/
public static function fromArray(array $data): self
{
return new self(
componentId: $data['componentId'] ?? '',
method: $data['method'] ?? '',
params: $data['params'] ?? [],
fragments: $data['fragments'] ?? null,
operationId: $data['operationId'] ?? null
);
}
/**
* Validate operation data
*/
private function validate(): void
{
if (empty($this->componentId)) {
throw new \InvalidArgumentException('BatchOperation: componentId is required');
}
if (empty($this->method)) {
throw new \InvalidArgumentException('BatchOperation: method is required');
}
if ($this->fragments !== null && ! is_array($this->fragments)) {
throw new \InvalidArgumentException('BatchOperation: fragments must be array or null');
}
}
/**
* Get ActionParameters for this operation
*/
public function getActionParameters(): ActionParameters
{
return ActionParameters::fromArray($this->params);
}
/**
* Check if operation requests fragments
*/
public function hasFragments(): bool
{
return $this->fragments !== null && ! empty($this->fragments);
}
/**
* Get fragment names
*
* @return array<string>
*/
public function getFragments(): array
{
return $this->fragments ?? [];
}
/**
* Convert to array for debugging
*/
public function toArray(): array
{
return [
'component_id' => $this->componentId,
'method' => $this->method,
'params' => $this->params,
'fragments' => $this->fragments,
'operation_id' => $this->operationId,
];
}
}

View File

@@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
namespace App\Framework\LiveComponents\Batch;
use App\Framework\LiveComponents\ComponentRegistry;
use App\Framework\LiveComponents\LiveComponentHandler;
use App\Framework\LiveComponents\Rendering\FragmentRenderer;
use App\Framework\LiveComponents\ValueObjects\ComponentId;
/**
* Batch Processor Service
*
* Processes batch requests with multiple LiveComponent operations.
*
* Features:
* - Executes operations sequentially with error isolation
* - Per-operation error handling (one failure doesn't stop the batch)
* - Fragment support for partial updates
* - State and event aggregation
*
* Performance:
* - 60-80% fewer HTTP requests for multi-component updates
* - Lower latency through request pipelining
* - Better UX for complex interactions
*/
final readonly class BatchProcessor
{
public function __construct(
private ComponentRegistry $componentRegistry,
private LiveComponentHandler $handler,
private FragmentRenderer $fragmentRenderer
) {
}
/**
* Process batch request and return batch response
*
* Each operation is executed independently. Failures are isolated
* and don't affect other operations in the batch.
*/
public function process(BatchRequest $batchRequest): BatchResponse
{
$results = [];
foreach ($batchRequest->getOperations() as $operation) {
$results[] = $this->processOperation($operation);
}
return new BatchResponse(...$results);
}
/**
* Process single operation
*
* Returns BatchResult with success/failure status.
* Catches exceptions and converts to error results.
*/
private function processOperation(BatchOperation $operation): BatchResult
{
try {
// Resolve component
$componentId = ComponentId::fromString($operation->componentId);
$component = $this->componentRegistry->resolve($componentId);
// Get current state from component
$currentState = $component->getData();
// Execute action
$update = $this->handler->handleAction(
component: $component,
method: $operation->method,
params: $operation->getActionParameters()
);
// Render result (full or fragments)
if ($operation->hasFragments()) {
// Fragment-based update
$fragmentCollection = $this->fragmentRenderer->renderFragments(
component: $update->component,
fragmentNames: $operation->getFragments()
);
if (! $fragmentCollection->isEmpty()) {
return BatchResult::success(
operationId: $operation->operationId,
fragments: $fragmentCollection->toAssociativeArray(),
state: $update->state->toArray(),
events: $update->events
);
}
// Fallback to full render if fragments not found
}
// Full HTML render
$html = $this->componentRegistry->render($update->component);
return BatchResult::success(
operationId: $operation->operationId,
html: $html,
state: $update->state->toArray(),
events: $update->events
);
} catch (\InvalidArgumentException $e) {
// Component or action not found
return BatchResult::failure(
error: $e->getMessage(),
errorCode: $this->getErrorCode($e),
operationId: $operation->operationId
);
} catch (\Throwable $e) {
// Unexpected error
return BatchResult::failure(
error: 'Operation failed: ' . $e->getMessage(),
errorCode: 'OPERATION_FAILED',
operationId: $operation->operationId
);
}
}
/**
* Extract error code from exception
*/
private function getErrorCode(\Throwable $e): string
{
$message = $e->getMessage();
if (str_contains($message, 'Unknown component')) {
return 'COMPONENT_NOT_FOUND';
}
if (str_contains($message, 'not allowed') || str_contains($message, 'Action')) {
return 'ACTION_NOT_ALLOWED';
}
if (str_contains($message, 'Rate limit')) {
return 'RATE_LIMIT_EXCEEDED';
}
return 'INVALID_OPERATION';
}
}

View File

@@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
namespace App\Framework\LiveComponents\Batch;
/**
* Batch Request Value Object
*
* Represents a batch of operations to execute.
*
* Example:
* {
* "operations": [
* {
* "componentId": "counter:demo",
* "method": "increment",
* "params": {"amount": 5}
* },
* {
* "componentId": "user-stats:123",
* "method": "refresh",
* "fragments": ["stats-display"]
* }
* ]
* }
*/
final readonly class BatchRequest
{
/** @var array<BatchOperation> */
public array $operations;
/**
* @param BatchOperation ...$operations - Variadic parameter for type safety
*/
public function __construct(BatchOperation ...$operations)
{
$this->operations = $operations;
$this->validate();
}
/**
* Create from array (from JSON request)
*/
public static function fromArray(array $data): self
{
if (! isset($data['operations']) || ! is_array($data['operations'])) {
throw new \InvalidArgumentException('BatchRequest: operations array is required');
}
$operations = array_map(
fn (array $op) => BatchOperation::fromArray($op),
$data['operations']
);
return new self(...$operations);
}
/**
* Validate batch request
*/
private function validate(): void
{
if (empty($this->operations)) {
throw new \InvalidArgumentException('BatchRequest: at least one operation is required');
}
// Enforce max batch size
$maxBatchSize = 50; // Configurable limit
if (count($this->operations) > $maxBatchSize) {
throw new \InvalidArgumentException(
"BatchRequest: batch size exceeds maximum ({$maxBatchSize} operations)"
);
}
}
/**
* Get number of operations
*/
public function count(): int
{
return count($this->operations);
}
/**
* Get operation by index
*/
public function getOperation(int $index): ?BatchOperation
{
return $this->operations[$index] ?? null;
}
/**
* Get all operations
*
* @return array<BatchOperation>
*/
public function getOperations(): array
{
return $this->operations;
}
/**
* Convert to array for debugging
*/
public function toArray(): array
{
return [
'operations' => array_map(
fn (BatchOperation $op) => $op->toArray(),
$this->operations
),
];
}
}

View File

@@ -0,0 +1,142 @@
<?php
declare(strict_types=1);
namespace App\Framework\LiveComponents\Batch;
/**
* Batch Response Value Object
*
* Represents the response for a batch of operations.
*
* Example:
* {
* "results": [
* {
* "success": true,
* "operationId": "op-1",
* "html": "<div>Counter: 5</div>",
* "state": {"count": 5}
* },
* {
* "success": false,
* "operationId": "op-2",
* "error": "Component not found",
* "errorCode": "COMPONENT_NOT_FOUND"
* }
* ],
* "totalOperations": 2,
* "successCount": 1,
* "failureCount": 1
* }
*/
final readonly class BatchResponse
{
/** @var array<BatchResult> */
public array $results;
public int $totalOperations;
public int $successCount;
public int $failureCount;
/**
* @param BatchResult ...$results - Variadic parameter for type safety
*/
public function __construct(BatchResult ...$results)
{
$this->results = $results;
$this->totalOperations = count($results);
$this->successCount = count(array_filter($results, fn ($r) => $r->success));
$this->failureCount = $this->totalOperations - $this->successCount;
}
/**
* Create from array of results
*
* @param array<BatchResult> $results
*/
public static function fromResults(array $results): self
{
return new self(...$results);
}
/**
* Get result by index
*/
public function getResult(int $index): ?BatchResult
{
return $this->results[$index] ?? null;
}
/**
* Get all results
*
* @return array<BatchResult>
*/
public function getResults(): array
{
return $this->results;
}
/**
* Get only successful results
*
* @return array<BatchResult>
*/
public function getSuccessfulResults(): array
{
return array_filter($this->results, fn ($r) => $r->success);
}
/**
* Get only failed results
*
* @return array<BatchResult>
*/
public function getFailedResults(): array
{
return array_filter($this->results, fn ($r) => ! $r->success);
}
/**
* Check if all operations succeeded
*/
public function isFullSuccess(): bool
{
return $this->failureCount === 0;
}
/**
* Check if all operations failed
*/
public function isFullFailure(): bool
{
return $this->successCount === 0;
}
/**
* Check if there are partial failures
*/
public function hasPartialFailure(): bool
{
return $this->successCount > 0 && $this->failureCount > 0;
}
/**
* Convert to array for JSON response
*/
public function toArray(): array
{
return [
'results' => array_map(
fn (BatchResult $result) => $result->toArray(),
$this->results
),
'total_operations' => $this->totalOperations,
'success_count' => $this->successCount,
'failure_count' => $this->failureCount,
];
}
}

View File

@@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
namespace App\Framework\LiveComponents\Batch;
/**
* Batch Result Value Object
*
* Represents the result of a single operation in a batch.
*
* Success Response:
* {
* "success": true,
* "operationId": "op-1",
* "html": "<div>...</div>", // OR fragments
* "fragments": {"counter": "<span>5</span>"},
* "state": {"count": 5},
* "events": []
* }
*
* Error Response:
* {
* "success": false,
* "operationId": "op-2",
* "error": "Action method not found",
* "errorCode": "ACTION_NOT_FOUND"
* }
*/
final readonly class BatchResult
{
/**
* @param bool $success - Operation success status
* @param string|null $operationId - Optional operation ID from request
* @param string|null $html - Full HTML (if no fragments)
* @param array|null $fragments - Fragment map {name: html}
* @param array|null $state - Component state
* @param array $events - Component events
* @param string|null $error - Error message (if failed)
* @param string|null $errorCode - Error code (if failed)
*/
public function __construct(
public bool $success,
public ?string $operationId = null,
public ?string $html = null,
public ?array $fragments = null,
public ?array $state = null,
public array $events = [],
public ?string $error = null,
public ?string $errorCode = null
) {
}
/**
* Create successful result
*/
public static function success(
?string $operationId = null,
?string $html = null,
?array $fragments = null,
?array $state = null,
array $events = []
): self {
return new self(
success: true,
operationId: $operationId,
html: $html,
fragments: $fragments,
state: $state,
events: $events
);
}
/**
* Create failed result
*/
public static function failure(
string $error,
string $errorCode = 'OPERATION_FAILED',
?string $operationId = null
): self {
return new self(
success: false,
operationId: $operationId,
error: $error,
errorCode: $errorCode
);
}
/**
* Check if result has fragments
*/
public function hasFragments(): bool
{
return $this->fragments !== null && ! empty($this->fragments);
}
/**
* Check if result has full HTML
*/
public function hasHtml(): bool
{
return $this->html !== null;
}
/**
* Convert to array for JSON response
*/
public function toArray(): array
{
if (! $this->success) {
return array_filter([
'success' => false,
'operation_id' => $this->operationId,
'error' => $this->error,
'error_code' => $this->errorCode,
], fn ($value) => $value !== null);
}
return array_filter([
'success' => true,
'operation_id' => $this->operationId,
'html' => $this->html,
'fragments' => $this->fragments,
'state' => $this->state,
'events' => $this->events,
], fn ($value) => $value !== null && $value !== []);
}
}