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,332 @@
<?php
declare(strict_types=1);
namespace App\Framework\LiveComponents\ParameterBinding;
use App\Framework\LiveComponents\ComponentEventDispatcher;
use App\Framework\LiveComponents\ValueObjects\ActionParameters;
use App\Framework\LiveComponents\ValueObjects\ParameterMatch;
/**
* Parameter Binder for LiveComponent Actions
*
* Handles advanced parameter binding including:
* - Builtin type casting (int, string, bool, float, array)
* - DTO instantiation via constructor promotion
* - Framework service injection (ActionParameters, ComponentEventDispatcher)
* - Multiple naming conventions (camelCase, snake_case, kebab-case)
* - Detailed error messages with type information
*/
final readonly class ParameterBinder
{
public function __construct(
private ComponentEventDispatcher $eventDispatcher
) {
}
/**
* Bind parameters to method arguments
*
* @param \ReflectionMethod $method Method to bind parameters for
* @param ActionParameters $params Action parameters from request
* @return array<mixed> Bound arguments ready for method invocation
* @throws ParameterBindingException if binding fails
*/
public function bindParameters(
\ReflectionMethod $method,
ActionParameters $params
): array {
$parameters = $method->getParameters();
$args = [];
foreach ($parameters as $param) {
$args[] = $this->bindParameter($param, $params, $method);
}
return $args;
}
/**
* Bind single parameter
*
* @param \ReflectionParameter $param Parameter to bind
* @param ActionParameters $params Action parameters
* @param \ReflectionMethod $method Method context (for error messages)
* @return mixed Bound value
* @throws ParameterBindingException if binding fails
*/
private function bindParameter(
\ReflectionParameter $param,
ActionParameters $params,
\ReflectionMethod $method
): mixed {
$paramName = $param->getName();
$paramType = $param->getType();
// 1. Framework service injection
$injected = $this->tryInjectFrameworkService($paramType, $params);
if ($injected !== null) {
return $injected;
}
// 2. Special case: 'params' array
if ($paramName === 'params' &&
$paramType instanceof \ReflectionNamedType &&
$paramType->getName() === 'array') {
return $params->toArray();
}
// 3. Try to find parameter value from ActionParameters
$match = $this->findParamValue($params->toArray(), $paramName);
if (! $match->isFound()) {
// Parameter not found - check if optional
if ($param->isDefaultValueAvailable()) {
return $param->getDefaultValue();
}
// Required parameter missing
throw ParameterBindingException::missingParameter(
parameterName: $paramName,
methodName: $method->getName(),
className: $method->getDeclaringClass()->getName(),
expectedType: $paramType?->__toString()
);
}
$value = $match->getValue();
// 4. Type binding based on parameter type
if (! $paramType instanceof \ReflectionNamedType) {
// No type hint - use value as-is
return $value;
}
$typeName = $paramType->getName();
// 5. Builtin type casting
if ($paramType->isBuiltin()) {
try {
return $this->castToBuiltinType($value, $typeName);
} catch (\Throwable $e) {
throw ParameterBindingException::typeMismatch(
parameterName: $paramName,
expectedType: $typeName,
actualValue: $value,
methodName: $method->getName(),
className: $method->getDeclaringClass()->getName()
);
}
}
// 6. DTO instantiation (non-builtin types)
try {
return $this->instantiateDTO($typeName, $value, $params);
} catch (\Throwable $e) {
throw ParameterBindingException::dtoInstantiationFailed(
parameterName: $paramName,
dtoClass: $typeName,
methodName: $method->getName(),
className: $method->getDeclaringClass()->getName(),
reason: $e->getMessage()
);
}
}
/**
* Try to inject framework service
*
* @param \ReflectionType|null $paramType Parameter type
* @param ActionParameters $params Action parameters
* @return mixed|null Injected service or null if not a framework service
*/
private function tryInjectFrameworkService(
?\ReflectionType $paramType,
ActionParameters $params
): mixed {
if (! $paramType instanceof \ReflectionNamedType) {
return null;
}
$typeName = $paramType->getName();
// ActionParameters injection
if ($typeName === ActionParameters::class) {
return $params;
}
// ComponentEventDispatcher injection
if ($typeName === ComponentEventDispatcher::class) {
return $this->eventDispatcher;
}
return null;
}
/**
* Cast value to builtin type
*
* @param mixed $value Value to cast
* @param string $typeName Target type name
* @return mixed Cast value
*/
private function castToBuiltinType(mixed $value, string $typeName): mixed
{
return match ($typeName) {
'int' => (int) $value,
'float' => (float) $value,
'string' => (string) $value,
'bool' => $this->castToBool($value),
'array' => (array) $value,
default => $value
};
}
/**
* Cast value to boolean with smart conversion
*
* Handles common string representations: 'true', 'false', '1', '0', 'yes', 'no'
*/
private function castToBool(mixed $value): bool
{
if (is_bool($value)) {
return $value;
}
if (is_string($value)) {
$lower = strtolower($value);
if (in_array($lower, ['true', '1', 'yes', 'on'], true)) {
return true;
}
if (in_array($lower, ['false', '0', 'no', 'off', ''], true)) {
return false;
}
}
return (bool) $value;
}
/**
* Instantiate DTO from value
*
* Supports:
* - Direct value passing (if DTO accepts single constructor param)
* - Constructor promotion with parameter mapping
* - Nested parameter extraction from arrays
*
* @param string $className DTO class name
* @param mixed $value Value to instantiate from
* @param ActionParameters $allParams All action parameters (for nested mapping)
* @return object Instantiated DTO
* @throws \ReflectionException if class doesn't exist
* @throws ParameterBindingException if instantiation fails
*/
private function instantiateDTO(
string $className,
mixed $value,
ActionParameters $allParams
): object {
// Check if class exists
if (! class_exists($className)) {
throw new \InvalidArgumentException("Class {$className} does not exist");
}
$reflection = new \ReflectionClass($className);
// Check if class is instantiable
if (! $reflection->isInstantiable()) {
throw new \InvalidArgumentException("Class {$className} is not instantiable");
}
$constructor = $reflection->getConstructor();
// No constructor - try direct instantiation
if ($constructor === null) {
return new $className();
}
// Get constructor parameters
$constructorParams = $constructor->getParameters();
// Single constructor param - pass value directly
if (count($constructorParams) === 1 && ! is_array($value)) {
return new $className($value);
}
// Multiple constructor params - map from array or ActionParameters
$args = [];
foreach ($constructorParams as $param) {
$paramName = $param->getName();
// Try to find value in provided value array
if (is_array($value) && array_key_exists($paramName, $value)) {
$paramValue = $value[$paramName];
} else {
// Try to find in all ActionParameters with naming conventions
$match = $this->findParamValue($allParams->toArray(), $paramName);
if ($match->isFound()) {
$paramValue = $match->getValue();
} elseif ($param->isDefaultValueAvailable()) {
$args[] = $param->getDefaultValue();
continue;
} else {
throw new \InvalidArgumentException(
"Missing required constructor parameter '{$paramName}' for DTO {$className}"
);
}
}
// Type cast constructor parameter
$paramType = $param->getType();
if ($paramType instanceof \ReflectionNamedType && $paramType->isBuiltin()) {
$args[] = $this->castToBuiltinType($paramValue, $paramType->getName());
} else {
$args[] = $paramValue;
}
}
return new $className(...$args);
}
/**
* Find parameter value supporting multiple naming conventions
*
* Tries: camelCase, snake_case, kebab-case, lowercase
*
* @param array<string, mixed> $params Parameters to search
* @param string $paramName Parameter name to find
* @return ParameterMatch Match result
*/
private function findParamValue(array $params, string $paramName): ParameterMatch
{
// Try exact match first
if (array_key_exists($paramName, $params)) {
return ParameterMatch::found($params[$paramName]);
}
// Try snake_case (rowId -> row_id)
$snakeCase = strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $paramName));
if (array_key_exists($snakeCase, $params)) {
return ParameterMatch::found($params[$snakeCase]);
}
// Try kebab-case (rowId -> row-id)
$kebabCase = strtolower(preg_replace('/([a-z])([A-Z])/', '$1-$2', $paramName));
if (array_key_exists($kebabCase, $params)) {
return ParameterMatch::found($params[$kebabCase]);
}
// Try lowercase (rowId -> rowid)
$lowercase = strtolower($paramName);
if (array_key_exists($lowercase, $params)) {
return ParameterMatch::found($params[$lowercase]);
}
return ParameterMatch::notFound();
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace App\Framework\LiveComponents\ParameterBinding;
use App\Framework\DI\Attributes\Initializer;
use App\Framework\DI\Container;
use App\Framework\LiveComponents\ComponentEventDispatcher;
/**
* ParameterBinder Initializer
*
* Registers ParameterBinder in DI container with ComponentEventDispatcher dependency.
*/
final readonly class ParameterBinderInitializer
{
#[Initializer]
public function __invoke(Container $container): ParameterBinder
{
$eventDispatcher = $container->get(ComponentEventDispatcher::class);
return new ParameterBinder($eventDispatcher);
}
}

View File

@@ -0,0 +1,164 @@
<?php
declare(strict_types=1);
namespace App\Framework\LiveComponents\ParameterBinding;
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\Core\ValueObjects\MethodName;
use App\Framework\Core\ValueObjects\QualifiedMethodName;
use App\Framework\Exception\Core\ValidationErrorCode;
use App\Framework\Exception\FrameworkException;
/**
* Parameter Binding Exception
*
* Thrown when parameter binding fails for LiveComponent actions.
* Provides detailed error messages with parameter names, types, and context.
*/
final class ParameterBindingException extends FrameworkException
{
/**
* Missing required parameter
*/
public static function missingParameter(
string $parameterName,
QualifiedMethodName $method,
?string $expectedType = null
): self {
$typeInfo = $expectedType ? " (expected type: {$expectedType})" : '';
return self::create(
ValidationErrorCode::BUSINESS_RULE_VIOLATION,
"Missing required parameter '{$parameterName}' for action {$method->toString}(){$typeInfo}"
)->withData([
'parameter_name' => $parameterName,
'method' => $method->methodName->toString(),
'class' => $method->className->toString(),
'expected_type' => $expectedType,
'error_type' => 'missing_parameter',
])->withContextMetadata([
'component' => 'LiveComponentParameterBinding',
]);
}
/**
* Type mismatch error
*/
public static function typeMismatch(
string $parameterName,
string $expectedType,
mixed $actualValue,
QualifiedMethodName $method
): self {
$actualType = get_debug_type($actualValue);
$valuePreview = self::previewValue($actualValue);
return self::create(
ValidationErrorCode::BUSINESS_RULE_VIOLATION,
"Type mismatch for parameter '{$parameterName}' in {$method->toString}(): " .
"expected {$expectedType}, got {$actualType}"
)->withData([
'parameter_name' => $parameterName,
'expected_type' => $expectedType,
'actual_type' => $actualType,
'value_preview' => $valuePreview,
'method' => $method->methodName->toString(),
'class' => $method->className->toString(),
'error_type' => 'type_mismatch',
])->withContextMetadata([
'component' => 'LiveComponentParameterBinding',
]);
}
/**
* DTO instantiation failed
*/
public static function dtoInstantiationFailed(
string $parameterName,
ClassName $dtoClass,
QualifiedMethodName $method,
string $reason
): self {
return self::create(
ValidationErrorCode::BUSINESS_RULE_VIOLATION,
"Failed to instantiate DTO '{$dtoClass->toString}' for parameter '{$parameterName}' " .
"in {$method->toString}(): {$reason}"
)->withData([
'parameter_name' => $parameterName,
'dto_class' => $dtoClass->toString(),
'method' => $method->methodName->toString(),
'class' => $method->className->toString(),
'reason' => $reason,
'error_type' => 'dto_instantiation_failed',
])->withContextMetadata([
'component' => 'LiveComponentParameterBinding',
]);
}
/**
* Invalid DTO structure
*/
public static function invalidDtoStructure(
ClassName $dtoClass,
string $parameterName,
string $reason
): self {
return self::create(
ValidationErrorCode::BUSINESS_RULE_VIOLATION,
"Invalid DTO structure for '{$dtoClass->toString()}' (parameter: {$parameterName}): {$reason}"
)->withData([
'dto_class' => $dtoClass->toString(),
'parameter_name' => $parameterName,
'reason' => $reason,
'error_type' => 'invalid_dto_structure',
])->withContextMetadata([
'component' => 'LiveComponentParameterBinding',
]);
}
/**
* Create preview of value for error messages
*
* Truncates long values and masks sensitive data.
*/
private static function previewValue(mixed $value): string
{
if (is_null($value)) {
return 'null';
}
if (is_bool($value)) {
return $value ? 'true' : 'false';
}
if (is_scalar($value)) {
$str = (string) $value;
// Truncate long strings
if (strlen($str) > 100) {
return substr($str, 0, 97) . '...';
}
return $str;
}
if (is_array($value)) {
$count = count($value);
$keys = array_keys($value);
$keyPreview = implode(', ', array_slice($keys, 0, 5));
if ($count > 5) {
$keyPreview .= ', ...';
}
return "array({$count}) [{$keyPreview}]";
}
if (is_object($value)) {
return get_class($value) . ' instance';
}
return get_debug_type($value);
}
}