Files
michaelschiemer/src/Framework/Console/CommandRegistry.php
Michael Schiemer fc3d7e6357 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.
2025-10-25 19:18:37 +02:00

320 lines
12 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\Console;
use App\Framework\Console\Performance\ConsolePerformanceCollector;
use App\Framework\Console\Progress\ProgressMiddleware;
use App\Framework\DI\Container;
use App\Framework\Discovery\Results\DiscoveryRegistry;
use App\Framework\Discovery\ValueObjects\DiscoveredAttribute;
use App\Framework\Exception\Core\ConsoleErrorCode;
use App\Framework\Exception\FrameworkException;
use App\Framework\Logging\Logger;
use App\Framework\Logging\ValueObjects\LogContext;
use App\Framework\Performance\Contracts\PerformanceCollectorInterface;
use ReflectionException;
use ReflectionMethod;
use Throwable;
/**
* Registry für Console Commands mit Discovery Integration
*/
final readonly class CommandRegistry
{
private CommandList $commandList;
/** @var array<string, DiscoveredAttribute> */
private array $discoveredAttributes;
private CommandParameterResolver $parameterResolver;
private ?ConsolePerformanceCollector $performanceCollector;
private ProgressMiddleware $progressMiddleware;
public function __construct(
private Container $container,
DiscoveryRegistry $discoveryRegistry
) {
$this->parameterResolver = new CommandParameterResolver(new MethodSignatureAnalyzer());
$this->performanceCollector = $this->createPerformanceCollector();
$this->progressMiddleware = new ProgressMiddleware();
$this->discoverCommands($discoveryRegistry);
}
public function getCommandList(): CommandList
{
return $this->commandList;
}
public function getDiscoveredAttribute(string $commandName): DiscoveredAttribute
{
if (! isset($this->discoveredAttributes[$commandName])) {
throw FrameworkException::create(
ConsoleErrorCode::COMMAND_NOT_FOUND,
"No discovered attribute found for command '{$commandName}'"
)->withData(['command_name' => $commandName]);
}
return $this->discoveredAttributes[$commandName];
}
/**
* @param array<int, string> $arguments
*/
public function executeCommand(string $commandName, array $arguments, ConsoleOutputInterface $output): ExitCode
{
if ($this->performanceCollector) {
return $this->performanceCollector->measureCommand(
$commandName,
fn () => $this->doExecuteCommand($commandName, $arguments, $output),
$arguments,
$output
);
}
return $this->doExecuteCommand($commandName, $arguments, $output);
}
private function doExecuteCommand(string $commandName, array $arguments, ConsoleOutputInterface $output): ExitCode
{
$command = $this->commandList->get($commandName);
$discoveredAttribute = $this->getDiscoveredAttribute($commandName);
try {
// Get execution context from discovered attribute
$className = $discoveredAttribute->className->getFullyQualified();
$methodName = $discoveredAttribute->methodName?->toString() ?? '__invoke';
// Performance tracking: Container resolution
$containerStart = microtime(true);
$instance = $this->container->get($className);
$containerDuration = (microtime(true) - $containerStart) * 1000;
if ($this->performanceCollector) {
$this->performanceCollector->recordContainerResolutionTime($commandName, $className, $containerDuration);
}
// Validate command structure
if (! is_object($instance) || ! method_exists($instance, $methodName)) {
throw FrameworkException::create(
ConsoleErrorCode::INVALID_COMMAND_STRUCTURE,
"Invalid command configuration for '{$commandName}'"
)->withData([
'command_name' => $commandName,
'class_name' => $className,
'method_name' => $methodName,
]);
}
// Get reflection method for parameter resolution
$reflectionMethod = new ReflectionMethod($className, $methodName);
// Performance tracking: Validation time
$validationStart = microtime(true);
$this->parameterResolver->validateMethodSignature($reflectionMethod);
$validationDuration = (microtime(true) - $validationStart) * 1000;
if ($this->performanceCollector) {
$this->performanceCollector->recordValidationTime($commandName, $validationDuration);
}
// Execute command with automatic parameter resolution
$result = $this->executeCommandWithReflection($instance, $reflectionMethod, $arguments, $output);
return $this->normalizeCommandResult($result);
} catch (Throwable $e) {
throw FrameworkException::create(
ConsoleErrorCode::EXECUTION_FAILED,
"Failed to execute command '{$commandName}': {$e->getMessage()}"
)->withData([
'command_name' => $commandName,
'error_message' => $e->getMessage(),
'error_type' => get_class($e),
]);
}
}
private function createPerformanceCollector(): ?ConsolePerformanceCollector
{
try {
if ($this->container->has(PerformanceCollectorInterface::class)) {
$performanceCollector = $this->container->get(PerformanceCollectorInterface::class);
return new ConsolePerformanceCollector($performanceCollector);
}
} catch (Throwable $e) {
// Performance monitoring is optional - don't fail if unavailable
}
return null;
}
private function discoverCommands(DiscoveryRegistry $discoveryRegistry): void
{
$commands = [];
$discoveredAttributes = [];
/** @var DiscoveredAttribute $discoveredAttribute */
foreach ($discoveryRegistry->attributes->get(ConsoleCommand::class) as $discoveredAttribute) {
try {
$registeredCommand = $this->registerDiscoveredCommand($discoveredAttribute);
$commands[] = $registeredCommand;
$discoveredAttributes[$registeredCommand->name] = $discoveredAttribute;
} catch (Throwable $e) {
// Log warning but continue with other commands
if ($this->container->has(Logger::class)) {
$logger = $this->container->get(Logger::class);
$logger->warning('Failed to register command', LogContext::withData([
'class_name' => $discoveredAttribute->className->getFullyQualified(),
'error' => $e->getMessage(),
'error_class' => get_class($e),
'component' => 'CommandRegistry',
]));
}
}
}
$this->commandList = new CommandList(...$commands);
$this->discoveredAttributes = $discoveredAttributes;
}
private function registerDiscoveredCommand(DiscoveredAttribute $discoveredAttribute): ConsoleCommand
{
// Validate discovered attribute
if (! $discoveredAttribute->className) {
throw new \InvalidArgumentException('Missing class name in discovered attribute');
}
$className = $discoveredAttribute->className->getFullyQualified();
// Validate class exists
if (! class_exists($className)) {
throw new \InvalidArgumentException("Command class {$className} does not exist");
}
/** @var ConsoleCommand $command */
$command = $discoveredAttribute->createAttributeInstance();
// Validate command name
if (empty(trim($command->name))) {
throw new \InvalidArgumentException("Command name cannot be empty for class {$className}");
}
$methodName = $discoveredAttribute->methodName?->toString() ?? '__invoke';
try {
// Validate method exists and is callable
$reflection = new ReflectionMethod($className, $methodName);
if (! $reflection->isPublic()) {
throw new \InvalidArgumentException("Command method {$className}::{$methodName} must be public");
}
// Validate that instance can be created from container
$this->container->get($className);
return $command;
} catch (ReflectionException $e) {
throw new \InvalidArgumentException("Invalid command method {$className}::{$methodName}: {$e->getMessage()}");
} catch (Throwable $e) {
throw new \RuntimeException("Failed to instantiate command class {$className}: {$e->getMessage()}");
}
}
private function normalizeCommandResult($result): ExitCode
{
if ($result instanceof ExitCode) {
return $result;
}
if (is_int($result)) {
try {
return ExitCode::from($result);
} catch (\ValueError) {
return ExitCode::GENERAL_ERROR;
}
}
if (is_bool($result)) {
return $result ? ExitCode::SUCCESS : ExitCode::GENERAL_ERROR;
}
return ExitCode::SUCCESS;
}
/**
* Execute command with automatic parameter resolution
*/
private function executeCommandWithReflection(
object $instance,
ReflectionMethod $method,
array $arguments,
ConsoleOutputInterface $output
): ExitCode {
try {
// Create the actual command execution callback
$commandExecutor = function (ConsoleInput $input, ConsoleOutputInterface $progressAwareOutput) use ($instance, $method, $arguments) {
// Performance tracking: Parameter resolution
$parameterStart = microtime(true);
// Resolve parameters with framework injection support
$resolvedParams = $this->parameterResolver->resolveParameters(
$method,
$arguments,
$input, // Inject ConsoleInput
$progressAwareOutput // Inject ConsoleOutput
);
$parameterDuration = (microtime(true) - $parameterStart) * 1000;
if ($this->performanceCollector) {
$this->performanceCollector->recordParameterResolutionTime($method->getDeclaringClass()->getName(), $parameterDuration);
}
$result = $method->invokeArgs($instance, $resolvedParams);
return $this->normalizeCommandResult($result);
};
// Wrap execution with ProgressMiddleware
$input = new ConsoleInput($arguments, $output);
return $this->progressMiddleware->handle($input, $output, $commandExecutor, $method, $instance);
} catch (\ArgumentCountError $e) {
throw new \InvalidArgumentException(
"Invalid number of arguments for command. " . $e->getMessage()
);
} catch (\TypeError $e) {
throw new \InvalidArgumentException(
"Type error in command execution: " . $e->getMessage()
);
}
}
/**
* Generate help for a specific command
*/
public function generateCommandHelp(string $commandName): string
{
$discoveredAttribute = $this->getDiscoveredAttribute($commandName);
$className = $discoveredAttribute->className->getFullyQualified();
$methodName = $discoveredAttribute->methodName?->toString() ?? '__invoke';
try {
$reflectionMethod = new ReflectionMethod($className, $methodName);
return $this->parameterResolver->generateMethodHelp($reflectionMethod, $commandName);
} catch (ReflectionException $e) {
return "Command: {$commandName}\nError generating help: {$e->getMessage()}";
}
}
}