- 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.
320 lines
12 KiB
PHP
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()}";
|
|
}
|
|
}
|
|
}
|