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,177 @@
<?php
declare(strict_types=1);
namespace App\Framework\LiveComponents\Validation;
use App\Framework\LiveComponents\Exceptions\StateValidationException;
/**
* Default state validator with automatic schema derivation
*
* Validates component state against automatically derived schemas.
* Supports type checking, required fields, and structure validation.
*
* Type Matching Rules:
* - Exact type match: int === int, string === string
* - Object type match: User === User (class name comparison)
* - Null handling: null matches 'null' type from schema
* - Array matching: array === array (no deep structure validation yet)
*
* State Value Objects:
* - Works directly with State VOs (CounterState, SearchState, etc.)
* - State VOs must implement toArray() for validation
*/
final readonly class DefaultStateValidator implements StateValidator
{
/**
* Derive schema from State Value Object
*/
public function deriveSchemaFromState(object $state): DerivedSchema
{
// State VOs must implement toArray() for serialization
if (!method_exists($state, 'toArray')) {
throw new \InvalidArgumentException(
'State object ' . get_class($state) . ' must implement toArray() method'
);
}
return DerivedSchema::fromData($state->toArray());
}
/**
* Validate State VO against derived schema
*
* @throws StateValidationException If validation fails
*/
public function validateState(object $state, DerivedSchema $schema): void
{
$result = $this->checkState($state, $schema);
if ($result->isFailed()) {
// Get component name from state class
$componentName = get_class($state);
if ($result->getErrorCount() === 1 && count($result->getFieldErrors()) === 1) {
// Single field error
$field = array_key_first($result->getFieldErrors());
$error = $result->getFieldError($field);
// Parse error message to extract type info
if (str_contains($error, 'expects type')) {
preg_match("/expects type '([^']+)' but got '([^']+)'/", $error, $matches);
if (count($matches) === 3) {
throw StateValidationException::typeMismatch(
$componentName,
$field,
$matches[1],
$matches[2]
);
}
} elseif (str_contains($error, 'is missing')) {
throw StateValidationException::missingField($componentName, $field, $schema);
} elseif (str_contains($error, 'not in schema')) {
throw StateValidationException::unexpectedField($componentName, $field, $schema);
}
}
// Multiple errors
throw StateValidationException::multipleErrors(
$componentName,
array_merge($result->getErrors(), array_values($result->getFieldErrors()))
);
}
}
/**
* Check if State VO matches schema without throwing
*/
public function checkState(object $state, DerivedSchema $schema): ValidationResult
{
// State VOs must implement toArray() for validation
if (!method_exists($state, 'toArray')) {
return ValidationResult::invalid([
'State object ' . get_class($state) . ' must implement toArray() method'
], []);
}
$errors = [];
$fieldErrors = [];
$stateData = $state->toArray();
// Check for missing required fields
foreach ($schema->getFields() as $field) {
if (! array_key_exists($field, $stateData)) {
if (! $schema->isNullable($field)) {
$fieldErrors[$field] = "Field '{$field}' is missing but required by schema";
}
}
}
// Check for unexpected fields and type mismatches
foreach ($stateData as $field => $value) {
if (! $schema->hasField($field)) {
$fieldErrors[$field] = "Field '{$field}' is not in schema";
continue;
}
$expectedType = $schema->getType($field);
$actualType = $this->getValueType($value);
if (! $this->typesMatch($expectedType, $actualType, $value)) {
$fieldErrors[$field] = "Field '{$field}' expects type '{$expectedType}' but got '{$actualType}'";
}
}
if (! empty($errors) || ! empty($fieldErrors)) {
return ValidationResult::invalid($errors, $fieldErrors);
}
return ValidationResult::valid();
}
/**
* Get type of value
*/
private function getValueType(mixed $value): string
{
if ($value === null) {
return 'null';
}
if (is_object($value)) {
return get_class($value);
}
return get_debug_type($value);
}
/**
* Check if types match
*/
private function typesMatch(string $expectedType, string $actualType, mixed $value): bool
{
// Exact match
if ($expectedType === $actualType) {
return true;
}
// Null handling
if ($expectedType === 'null') {
return true;
}
// If value is null and schema expects non-null type, reject
if ($value === null && $expectedType !== 'null') {
return false;
}
// Object type matching - check if actual type is instance of expected type
if (class_exists($expectedType) && is_object($value)) {
return $value instanceof $expectedType;
}
return false;
}
}

View File

@@ -0,0 +1,127 @@
<?php
declare(strict_types=1);
namespace App\Framework\LiveComponents\Validation;
/**
* Derived schema from actual component data
*
* Represents the schema automatically derived from first getData() call.
* Captures actual types, structure, and nullability of component state.
*
* Example:
* ```php
* ComponentData::fromArray([
* 'count' => 0,
* 'items' => ['a', 'b'],
* 'user' => new User(...),
* 'metadata' => null
* ])
*
* Derives to:
* DerivedSchema([
* 'count' => 'int',
* 'items' => 'array',
* 'user' => User::class,
* 'metadata' => 'null'
* ])
* ```
*/
final readonly class DerivedSchema
{
/**
* @param array<string, string> $fields Field name => type mapping
*/
public function __construct(
public array $fields
) {
if (empty($this->fields)) {
throw new \InvalidArgumentException('Schema cannot be empty');
}
}
/**
* Create from component data
*/
public static function fromData(array $data): self
{
$fields = [];
foreach ($data as $key => $value) {
$fields[$key] = self::deriveType($value);
}
return new self($fields);
}
/**
* Derive type from value
*/
private static function deriveType(mixed $value): string
{
if ($value === null) {
return 'null';
}
if (is_object($value)) {
return get_class($value);
}
return get_debug_type($value);
}
/**
* Get type for field
*/
public function getType(string $field): ?string
{
return $this->fields[$field] ?? null;
}
/**
* Check if field exists in schema
*/
public function hasField(string $field): bool
{
return isset($this->fields[$field]);
}
/**
* Get all field names
*
* @return array<string>
*/
public function getFields(): array
{
return array_keys($this->fields);
}
/**
* Get field count
*/
public function getFieldCount(): int
{
return count($this->fields);
}
/**
* Check if field allows null
*/
public function isNullable(string $field): bool
{
$type = $this->getType($field);
return $type === 'null';
}
/**
* Convert to array representation
*
* @return array<string, string>
*/
public function toArray(): array
{
return $this->fields;
}
}

View File

@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace App\Framework\LiveComponents\Validation;
/**
* Cache for component state schemas
*
* Stores derived schemas per component class to avoid repeated derivation.
* Schema is derived once from first getData() call and reused for all
* subsequent validations of that component class.
*
* Performance:
* - Eliminates repeated get_debug_type() calls
* - O(1) lookup by component class name
* - Memory-efficient - one schema per component class
*/
final class SchemaCache
{
/**
* @var array<string, DerivedSchema> Component class => schema mapping
*/
private array $cache = [];
/**
* Get cached schema for component class
*
* @param string $componentClass Fully qualified component class name
* @return DerivedSchema|null Cached schema or null if not cached
*/
public function get(string $componentClass): ?DerivedSchema
{
return $this->cache[$componentClass] ?? null;
}
/**
* Store schema for component class
*
* @param string $componentClass Fully qualified component class name
* @param DerivedSchema $schema Derived schema to cache
*/
public function set(string $componentClass, DerivedSchema $schema): void
{
$this->cache[$componentClass] = $schema;
}
/**
* Check if schema is cached for component class
*
* @param string $componentClass Fully qualified component class name
*/
public function has(string $componentClass): bool
{
return isset($this->cache[$componentClass]);
}
/**
* Clear cache for specific component class
*
* @param string $componentClass Fully qualified component class name
*/
public function clear(string $componentClass): void
{
unset($this->cache[$componentClass]);
}
/**
* Clear all cached schemas
*/
public function clearAll(): void
{
$this->cache = [];
}
/**
* Get cache statistics
*
* @return array{cached_count: int, cached_classes: array<string>}
*/
public function getStats(): array
{
return [
'cached_count' => count($this->cache),
'cached_classes' => array_keys($this->cache),
];
}
}

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace App\Framework\LiveComponents\Validation;
/**
* Validates LiveComponent state against derived schema
*
* Automatic Schema Derivation:
* - First getState() call derives schema from actual State VO types
* - Schema is cached per component class
* - Subsequent state updates are validated against derived schema
* - Prevents state drift and runtime type errors
*
* Framework Integration:
* - LiveComponentHandler validates state after action execution
* - Throws StateValidationException on schema violations
* - Zero configuration - works automatically
*
* State Value Objects:
* - Components return typed State VOs (CounterState, SearchState, etc.)
* - State VOs must implement toArray() for serialization
* - Validation works directly with State VOs
*/
interface StateValidator
{
/**
* Derive schema from State Value Object
*
* Analyzes State VO structure and creates schema definition.
* Called on first getState() for each component class.
*
* @param object $state State Value Object (e.g., CounterState, SearchState)
* @return DerivedSchema Schema definition
*/
public function deriveSchemaFromState(object $state): DerivedSchema;
/**
* Validate State VO against derived schema
*
* @param object $state State Value Object to validate
* @param DerivedSchema $schema Expected schema
* @throws StateValidationException If validation fails
*/
public function validateState(object $state, DerivedSchema $schema): void;
/**
* Check if State VO matches schema without throwing
*
* @param object $state State Value Object to validate
* @param DerivedSchema $schema Expected schema
* @return ValidationResult Validation result with errors
*/
public function checkState(object $state, DerivedSchema $schema): ValidationResult;
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Framework\LiveComponents\Validation;
use App\Framework\DI\Initializer;
/**
* Initializer for StateValidator
*
* Registers DefaultStateValidator as default implementation.
* Provides automatic state validation with schema derivation.
*/
final readonly class StateValidatorInitializer
{
#[Initializer]
public function __invoke(): StateValidator
{
// Return DefaultStateValidator as default implementation
return new DefaultStateValidator();
}
}

View File

@@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace App\Framework\LiveComponents\Validation;
/**
* Result of state validation check
*
* Contains validation status and detailed error information.
* Used for non-throwing validation checks.
*/
final readonly class ValidationResult
{
/**
* @param bool $valid Whether validation passed
* @param array<string> $errors Validation error messages
* @param array<string, string> $fieldErrors Field-specific errors
*/
public function __construct(
public bool $valid,
public array $errors = [],
public array $fieldErrors = []
) {
}
/**
* Create successful validation result
*/
public static function valid(): self
{
return new self(true, [], []);
}
/**
* Create failed validation result
*
* @param array<string> $errors General errors
* @param array<string, string> $fieldErrors Field-specific errors
*/
public static function invalid(array $errors, array $fieldErrors = []): self
{
return new self(false, $errors, $fieldErrors);
}
/**
* Check if validation passed
*/
public function isValid(): bool
{
return $this->valid;
}
/**
* Check if validation failed
*/
public function isFailed(): bool
{
return ! $this->valid;
}
/**
* Get all errors
*
* @return array<string>
*/
public function getErrors(): array
{
return $this->errors;
}
/**
* Get field-specific errors
*
* @return array<string, string>
*/
public function getFieldErrors(): array
{
return $this->fieldErrors;
}
/**
* Get error for specific field
*/
public function getFieldError(string $field): ?string
{
return $this->fieldErrors[$field] ?? null;
}
/**
* Check if field has error
*/
public function hasFieldError(string $field): bool
{
return isset($this->fieldErrors[$field]);
}
/**
* Get total error count
*/
public function getErrorCount(): int
{
return count($this->errors) + count($this->fieldErrors);
}
}