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,32 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI\Attributes;
use Attribute;
/**
* Marks a class as the default implementation for an interface
*
* When applied to a class, this attribute enables automatic registration
* in the DI container with the specified interface (or auto-detected interface).
*
* Usage:
* - Explicit interface: #[DefaultImplementation(UserRepository::class)]
* - Auto-detect: #[DefaultImplementation] (uses first implemented interface)
*
* Validation:
* - If interface is specified: class must implement that interface
* - If interface is not specified: class must implement at least one interface
*/
#[Attribute(Attribute::TARGET_CLASS)]
final readonly class DefaultImplementation
{
/**
* @param class-string|null $interface Optional interface to bind to. If null, auto-detect first interface.
*/
public function __construct(
public ?string $interface = null
) {}
}

View File

@@ -9,6 +9,7 @@ use App\Framework\Core\ValueObjects\ClassName;
interface Container
{
public MethodInvoker $invoker {get;}
public ContainerIntrospector $introspector {get;}
/**

View File

@@ -62,6 +62,7 @@ final readonly class ContainerIntrospector
/** @var array<class-string> $chain */
$chain = $f();
return $chain;
}
@@ -75,6 +76,7 @@ final readonly class ContainerIntrospector
if (! $className->exists()) {
return false;
}
return $this->reflectionProvider->getClass($className)->isInstantiable();
}

View File

@@ -24,7 +24,9 @@ final class DefaultContainer implements Container
private readonly LazyInstantiator $lazyInstantiator;
public readonly MethodInvoker $invoker;
public readonly ContainerIntrospector $introspector;
public readonly FrameworkMetricsCollector $metrics;
public function __construct(
@@ -44,7 +46,7 @@ final class DefaultContainer implements Container
$this->instances,
$this->bindings,
$this->reflectionProvider,
fn(): array => $this->resolving
fn (): array => $this->resolving
);
$this->metrics = new FrameworkMetricsCollector();
@@ -55,7 +57,11 @@ final class DefaultContainer implements Container
public function bind(string $abstract, callable|string|object $concrete): void
{
$this->bindings->bind($abstract, $concrete);
$this->clearCaches(ClassName::create($abstract));
// Only clear caches for valid class names, skip string keys like 'filesystem.storage.local'
if (class_exists($abstract) || interface_exists($abstract)) {
$this->clearCaches(ClassName::create($abstract));
}
}
public function singleton(string $abstract, callable|string|object $concrete): void
@@ -121,7 +127,9 @@ final class DefaultContainer implements Container
try {
$instance = $this->buildInstance($class);
if ($this->singletonDetector->isSingleton(ClassName::create($class))) {
// Only check singleton attribute for actual classes
if ((class_exists($class) || interface_exists($class)) &&
$this->singletonDetector->isSingleton(ClassName::create($class))) {
$this->instances->setSingleton($class, $instance);
} else {
$this->instances->setInstance($class, $instance);
@@ -131,6 +139,7 @@ final class DefaultContainer implements Container
} catch (Throwable $e) {
// Track resolution failures
$this->metrics->increment('container.resolve.failures');
throw $e;
} finally {
array_pop($this->resolving);
@@ -139,11 +148,20 @@ final class DefaultContainer implements Container
private function buildInstance(string $class): object
{
$className = ClassName::create($class);
if ($this->bindings->hasBinding($class)) {
return $this->resolveBinding($class, $this->bindings->getBinding($class));
}
// For string keys without bindings, throw immediately
if (!class_exists($class) && !interface_exists($class)) {
throw new \RuntimeException(
"Cannot resolve '{$class}': not a valid class and no binding exists. " .
"Available bindings: " . implode(', ', array_keys($this->bindings->getAllBindings()))
);
}
$className = ClassName::create($class);
// Enhanced diagnostics for missing bindings
try {
$reflection = $this->reflectionProvider->getClass($className);

View File

@@ -0,0 +1,144 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI;
use App\Framework\DI\Attributes\DefaultImplementation;
use App\Framework\DI\Exceptions\DefaultImplementationException;
use App\Framework\Discovery\Results\DiscoveryRegistry;
use App\Framework\Discovery\ValueObjects\DiscoveredAttribute;
use ReflectionClass;
/**
* Processes DefaultImplementation attributes and registers bindings in the DI container
*
* This processor:
* 1. Discovers classes with #[DefaultImplementation] attribute
* 2. Validates interface implementation
* 3. Auto-detects interface if not explicitly specified
* 4. Registers bindings in the container
*/
final readonly class DefaultImplementationProcessor
{
public function __construct(
private Container $container
) {}
/**
* Process all DefaultImplementation attributes from discovery results
*
* @param DiscoveryRegistry $registry
* @return int Number of bindings registered
*/
public function process(DiscoveryRegistry $registry): int
{
$attributes = $registry->attributes->get(DefaultImplementation::class);
if ($attributes === null) {
return 0;
}
$registered = 0;
foreach ($attributes as $discoveredAttribute) {
try {
$this->processAttribute($discoveredAttribute);
$registered++;
} catch (DefaultImplementationException $e) {
// Re-throw validation exceptions - these should be caught during development
throw $e;
}
}
return $registered;
}
/**
* Process a single DefaultImplementation attribute
*
* @param DiscoveredAttribute $discovered
* @throws DefaultImplementationException
*/
private function processAttribute(DiscoveredAttribute $discovered): void
{
$className = $discovered->className->getFullyQualified();
// Create the attribute instance to access its properties
$attribute = $discovered->createAttributeInstance();
if (!($attribute instanceof DefaultImplementation)) {
return;
}
// Determine the interface to bind to
$interface = $this->resolveInterface($className, $attribute->interface);
// Register binding in container
$this->container->singleton($interface, $className);
}
/**
* Resolve the interface for binding
*
* If interface is explicitly provided, validate it.
* Otherwise, auto-detect the first interface the class implements.
*
* @param class-string $className
* @param class-string|null $explicitInterface
* @return class-string
* @throws DefaultImplementationException
*/
private function resolveInterface(string $className, ?string $explicitInterface): string
{
if ($explicitInterface !== null) {
return $this->validateExplicitInterface($className, $explicitInterface);
}
return $this->autoDetectInterface($className);
}
/**
* Validate that the class implements the explicitly specified interface
*
* @param class-string $className
* @param class-string $interface
* @return class-string
* @throws DefaultImplementationException
*/
private function validateExplicitInterface(string $className, string $interface): string
{
// Check if interface exists
if (!interface_exists($interface)) {
throw DefaultImplementationException::interfaceDoesNotExist($className, $interface);
}
// Check if class implements the interface
$reflection = new ReflectionClass($className);
if (!$reflection->implementsInterface($interface)) {
throw DefaultImplementationException::doesNotImplementInterface($className, $interface);
}
return $interface;
}
/**
* Auto-detect the first interface the class implements
*
* @param class-string $className
* @return class-string
* @throws DefaultImplementationException
*/
private function autoDetectInterface(string $className): string
{
$reflection = new ReflectionClass($className);
$interfaces = $reflection->getInterfaceNames();
if (empty($interfaces)) {
throw DefaultImplementationException::noInterfacesImplemented($className);
}
// Return the first interface
return $interfaces[0];
}
}

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI\Exceptions;
use App\Framework\Exception\ExceptionContext;
use App\Framework\Exception\FrameworkException;
use RuntimeException;
/**
* Exception thrown when DefaultImplementation attribute validation fails
*/
final class DefaultImplementationException extends RuntimeException
{
/**
* Thrown when a class with DefaultImplementation attribute does not implement the specified interface
*
* @param class-string $className
* @param class-string $interface
*/
public static function doesNotImplementInterface(string $className, string $interface): self
{
return new self(
sprintf(
'Class "%s" has #[DefaultImplementation] for interface "%s" but does not implement that interface',
$className,
$interface
)
);
}
/**
* Thrown when a class with DefaultImplementation attribute (without explicit interface) implements no interfaces
*
* @param class-string $className
*/
public static function noInterfacesImplemented(string $className): self
{
return new self(
sprintf(
'Class "%s" has #[DefaultImplementation] without explicit interface but implements no interfaces. ' .
'Either specify an interface explicitly or ensure the class implements at least one interface.',
$className
)
);
}
/**
* Thrown when the specified interface does not exist
*
* @param class-string $className
* @param string $interface
*/
public static function interfaceDoesNotExist(string $className, string $interface): self
{
return new self(
sprintf(
'Class "%s" has #[DefaultImplementation] for interface "%s" which does not exist',
$className,
$interface
)
);
}
}

View File

@@ -21,6 +21,11 @@ final readonly class LazyInstantiator
public function canUseLazyLoading(string $class, InstanceRegistry $registry): bool
{
// Don't use lazy loading for string keys (e.g., 'filesystem.storage.local')
if (!class_exists($class) && !interface_exists($class)) {
return false;
}
return ! $registry->hasSingleton($class);
}

View File

@@ -13,10 +13,13 @@ use App\Framework\Reflection\WrappedReflectionMethod;
*/
final readonly class MethodInvoker
{
private ParameterResolver $parameterResolver;
public function __construct(
private Container $container,
private Container $container,
private ReflectionProvider $reflectionProvider
) {
$this->parameterResolver = new ParameterResolver($container, $reflectionProvider);
}
/**
@@ -87,56 +90,42 @@ final readonly class MethodInvoker
return $nativeMethod->invokeArgs(null, $parameters);
}
/**
* Erstellt eine neue Instanz mit automatischer Dependency Injection
* und optionalen Parameter-Overrides für den Constructor
*
* @template T of object
* @param class-string<T> $className
* @param array<string, mixed> $overrides Named constructor parameters
* @return T
*/
public function make(string $className, array $overrides = []): object
{
$classNameObj = ClassName::create($className);
$reflection = $this->reflectionProvider->getClass($classNameObj);
if (! $reflection->isInstantiable()) {
throw new \InvalidArgumentException(
"Class '{$className}' is not instantiable (interface, abstract class, or trait)"
);
}
// Check if class has constructor
if (! $reflection->hasMethod('__construct')) {
return $reflection->newInstance();
}
// Resolve constructor parameters with overrides
$parameters = $this->resolveMethodParameters($classNameObj, '__construct', $overrides);
return $reflection->newInstance(...$parameters);
}
/**
* Löst alle Parameter einer Methode auf
*/
private function resolveMethodParameters(ClassName $className, string $methodName, array $overrides): array
{
$parameters = [];
// Framework ParameterCollection verwenden
$methodParameters = $this->reflectionProvider->getMethodParameters($className, $methodName);
foreach ($methodParameters as $param) {
$paramName = $param->getName();
// Override-Werte haben Priorität
if (array_key_exists($paramName, $overrides)) {
$parameters[] = $overrides[$paramName];
continue;
}
// Type-based injection mit Framework API
$typeName = $param->getTypeName();
if ($typeName !== null && ! $param->isBuiltin()) {
if ($this->container->has($typeName)) {
$parameters[] = $this->container->get($typeName);
continue;
}
}
// Default-Werte verwenden
if ($param->hasDefaultValue()) {
$parameters[] = $param->getDefaultValue();
continue;
}
// Nullable Parameter
if ($param->allowsNull()) {
$parameters[] = null;
continue;
}
throw new \InvalidArgumentException(
"Cannot resolve parameter '{$paramName}' for method '{$methodName}' on class '{$className->getFullyQualified()}'"
);
}
return $parameters;
return $this->parameterResolver->resolveMethodParameters($className, $methodName, $overrides);
}
}

View File

@@ -0,0 +1,164 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI;
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\Logging\Attributes\LogChannel;
use App\Framework\Logging\SupportsChannels;
use App\Framework\Reflection\ReflectionProvider;
use App\Framework\Reflection\WrappedReflectionParameter;
/**
* Löst Method-Parameter mit Unterstützung für Attributes auf
*
* Erweiterbar für verschiedene Attribute:
* - #[LogChannel] - Injiziert Channel-spezifische Logger
* - Weitere Attributes können einfach hinzugefügt werden
*/
final readonly class ParameterResolver
{
public function __construct(
private Container $container,
private ReflectionProvider $reflectionProvider
) {
}
/**
* Löst alle Parameter einer Methode auf
*
* Resolution-Reihenfolge:
* 1. Override-Werte (höchste Priorität)
* 2. Attribute-basierte Injection (#[LogChannel], etc.)
* 3. Type-basierte Injection vom Container
* 4. Default-Werte
* 5. Nullable Parameter (null)
*
* @param array<string, mixed> $overrides Named parameter overrides
* @return array<int, mixed> Resolved parameter values
*/
public function resolveMethodParameters(
ClassName $className,
string $methodName,
array $overrides = []
): array {
$parameters = [];
$methodParameters = $this->reflectionProvider->getMethodParameters($className, $methodName);
foreach ($methodParameters as $param) {
$parameters[] = $this->resolveParameter($className, $methodName, $param, $overrides);
}
return $parameters;
}
/**
* Löst einen einzelnen Parameter auf
*/
private function resolveParameter(
ClassName $className,
string $methodName,
WrappedReflectionParameter $param,
array $overrides
): mixed {
$paramName = $param->getName();
// 1. Override-Werte haben höchste Priorität
if (array_key_exists($paramName, $overrides)) {
return $overrides[$paramName];
}
// 2. Attribute-basierte Injection
$attributeValue = $this->resolveFromAttributes($param, $className, $methodName);
if ($attributeValue !== null) {
return $attributeValue;
}
// 3. Type-basierte Injection vom Container
$typeValue = $this->resolveFromType($param);
if ($typeValue !== null) {
return $typeValue;
}
// 4. Default-Werte
if ($param->hasDefaultValue()) {
return $param->getDefaultValue();
}
// 5. Nullable Parameter
if ($param->allowsNull()) {
return null;
}
// Kann nicht aufgelöst werden
throw new \InvalidArgumentException(
"Cannot resolve parameter '{$paramName}' for method '{$methodName}' on class '{$className->getFullyQualified()}'"
);
}
/**
* Versucht Parameter über Attributes aufzulösen
*
* Nutzt direkt die native Reflection API für maximale Einfachheit
*
* Unterstützte Attributes:
* - #[LogChannel] - Injiziert Channel-spezifischen Logger
*/
private function resolveFromAttributes(
WrappedReflectionParameter $param,
ClassName $className,
string $methodName
): mixed {
// Direkt native Reflection API nutzen
$nativeParam = new \ReflectionParameter(
[$className->getFullyQualified(), $methodName],
$param->getPosition()
);
// #[LogChannel] Attribut
$logChannelAttrs = $nativeParam->getAttributes(LogChannel::class);
if (!empty($logChannelAttrs)) {
/** @var LogChannel $logChannelAttr */
$logChannelAttr = $logChannelAttrs[0]->newInstance();
return $this->resolveLogChannelAttribute($logChannelAttr);
}
// Weitere Attributes können hier hinzugefügt werden
// z.B. #[Inject], #[Autowire], #[Config], etc.
return null;
}
/**
* Löst #[LogChannel] Attribut auf
*
* Injiziert einen Channel-spezifischen Logger statt des Haupt-Loggers
*/
private function resolveLogChannelAttribute(LogChannel $attribute): mixed
{
// Haupt-Logger vom Container holen (SupportsChannels Interface)
$mainLogger = $this->container->get(SupportsChannels::class);
// Channel-Logger zurückgeben (Logger & HasChannel)
return $mainLogger->channel($attribute->channel);
}
/**
* Versucht Parameter über Type-Hint aufzulösen
*/
private function resolveFromType(WrappedReflectionParameter $param): mixed
{
$typeName = $param->getTypeName();
if ($typeName === null || $param->isBuiltin()) {
return null;
}
if ($this->container->has($typeName)) {
return $this->container->get($typeName);
}
return null;
}
}

View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI;
use App\Framework\Config\Environment;
use App\Framework\Config\EnvKey;
use App\Framework\Database\ConnectionInterface;
use App\Framework\DateTime\Clock;
use App\Framework\Vault\DatabaseVault;
use App\Framework\Vault\Vault;
use App\Framework\Vault\VaultAuditLogger;
/**
* Vault Service Initializer
*
* Placed in DI directory for guaranteed discovery
*/
final readonly class VaultServiceInitializer
{
public function __construct(
private Environment $environment,
private ConnectionInterface $connection,
private Clock $clock
) {
}
#[Initializer]
public function __invoke(): Vault
{
// Encryption Key aus Environment
$encodedKey = $this->environment->get(EnvKey::VAULT_ENCRYPTION_KEY);
if ($encodedKey === null) {
throw new \RuntimeException(
'VAULT_ENCRYPTION_KEY not set in environment. ' .
'Generate one with: php console.php vault:generate-key'
);
}
// Decode base64-encoded key
$encryptionKey = DatabaseVault::decodeKey($encodedKey);
// Audit Logger
$auditLogger = new VaultAuditLogger($this->connection);
// Client IP und User Agent für Audit Logging (CLI context = null)
$clientIp = null;
$userAgent = null;
// DatabaseVault instance
return new DatabaseVault(
connection: $this->connection,
encryptionKey: $encryptionKey,
auditLogger: $auditLogger,
clock: $this->clock,
clientIp: $clientIp,
userAgent: $userAgent
);
}
}