Files
michaelschiemer/src/Framework/Console/Commands/InitializersCheckCommand.php
2025-11-24 21:28:25 +01:00

276 lines
11 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\Console\Commands;
use App\Framework\Console\CommandGroup;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleCommand;
use App\Framework\Console\ConsoleInput;
use App\Framework\Console\ConsoleOutputInterface;
use App\Framework\Console\ExitCode;
use App\Framework\Core\ValueObjects\ReturnTypeValue;
use App\Framework\DI\Exceptions\InitializerCycleException;
use App\Framework\DI\Initializer;
use App\Framework\DI\InitializerDependencyAnalyzer;
use App\Framework\DI\InitializerDependencyGraph;
use App\Framework\Discovery\Results\DiscoveryRegistry;
use App\Framework\Discovery\ValueObjects\DiscoveredAttribute;
use App\Framework\Reflection\ReflectionService;
#[CommandGroup(
name: 'Framework',
description: 'Framework diagnostics and health checks',
icon: '🔧',
priority: 90
)]
final readonly class InitializersCheckCommand
{
public function __construct(
private DiscoveryRegistry $discoveryRegistry,
private ReflectionService $reflectionService,
private InitializerDependencyAnalyzer $dependencyAnalyzer,
private ConsoleOutputInterface $output
) {
}
#[ConsoleCommand('initializers:check', 'Check all initializers for problems (dependencies, cycles, etc.)')]
public function check(ConsoleInput $input, ConsoleOutputInterface $output): ExitCode
{
$jsonOutput = $input->hasOption('json');
$output->writeLine('Checking initializers...', ConsoleColor::BRIGHT_CYAN);
$output->newLine();
$initializerResults = $this->discoveryRegistry->attributes->get(Initializer::class);
$totalInitializers = count($initializerResults);
if ($totalInitializers === 0) {
$output->writeLine('⚠️ No initializers found in discovery registry.', ConsoleColor::YELLOW);
return ExitCode::FAILURE;
}
$output->writeLine("Found {$totalInitializers} initializer(s)", ConsoleColor::WHITE);
$output->newLine();
// Build dependency graph
$dependencyGraph = new InitializerDependencyGraph(
$this->reflectionService,
$this->dependencyAnalyzer
);
$problems = [];
$warnings = [];
$info = [];
// Phase 1: Analyze each initializer
foreach ($initializerResults as $discoveredAttribute) {
$initializer = $discoveredAttribute->createAttributeInstance();
if ($initializer === null) {
$problems[] = [
'type' => 'error',
'message' => "Failed to instantiate Initializer attribute",
'class' => (string) $discoveredAttribute->className,
'method' => (string) ($discoveredAttribute->methodName ?? 'unknown'),
];
continue;
}
$methodName = $discoveredAttribute->methodName ?? \App\Framework\Core\ValueObjects\MethodName::invoke();
$returnTypeString = $discoveredAttribute->additionalData['return'] ?? null;
try {
$returnType = ReturnTypeValue::fromString($returnTypeString, $discoveredAttribute->className);
// Skip setup initializers (void return)
if ($returnType->hasNoReturn()) {
continue;
}
$concreteReturnType = $returnType->isSelf()
? $returnType->toClassName()
: $returnType->toClassName();
// Analyze dependencies
$analysis = $this->dependencyAnalyzer->analyze($discoveredAttribute->className->getFullyQualified());
// Check for missing dependencies
$missingDeps = [];
foreach (array_merge($analysis['constructorDeps'], $analysis['containerGetDeps']) as $dep) {
// Skip Container itself
if (\App\Framework\DI\InitializerDependencyAnalyzer::isContainerClass($dep)) {
continue;
}
// Check if dependency has an initializer or is a concrete class
if (!class_exists($dep) && !interface_exists($dep)) {
$missingDeps[] = $dep;
}
}
if (!empty($missingDeps)) {
$warnings[] = [
'type' => 'warning',
'message' => "Potential missing dependencies",
'class' => (string) $discoveredAttribute->className,
'method' => (string) $methodName,
'return_type' => $concreteReturnType->getFullyQualified(),
'missing_dependencies' => $missingDeps,
];
}
// Add to graph
// Extrahiere explizite Dependencies und Priority aus additionalData
$explicitDependencies = $discoveredAttribute->additionalData['dependencies'] ?? null;
$priority = (int) ($discoveredAttribute->additionalData['priority'] ?? 0);
$dependencyGraph->addInitializer(
$concreteReturnType->getFullyQualified(),
$discoveredAttribute->className,
$methodName,
$explicitDependencies,
$priority
);
$info[] = [
'type' => 'info',
'class' => (string) $discoveredAttribute->className,
'method' => (string) $methodName,
'return_type' => $concreteReturnType->getFullyQualified(),
'dependencies' => array_merge($analysis['constructorDeps'], $analysis['containerGetDeps']),
];
} catch (\Throwable $e) {
$problems[] = [
'type' => 'error',
'message' => "Failed to analyze initializer: {$e->getMessage()}",
'class' => (string) $discoveredAttribute->className,
'method' => (string) $methodName,
'exception' => get_class($e),
];
}
}
// Phase 2: Check for cycles
try {
$executionOrder = $dependencyGraph->getExecutionOrder();
} catch (InitializerCycleException $e) {
$cycles = $e->getCycles();
$paths = $e->getDependencyPaths();
foreach ($cycles as $index => $cycle) {
$path = $paths[$index] ?? $cycle;
$problems[] = [
'type' => 'error',
'message' => 'Circular dependency detected',
'cycle' => $cycle,
'dependency_path' => $path,
];
}
}
// Phase 3: Output results
if ($jsonOutput) {
$this->outputJson($output, [
'total' => $totalInitializers,
'problems' => $problems,
'warnings' => $warnings,
'info' => $info,
]);
} else {
$this->outputHumanReadable($output, $problems, $warnings, $info, $dependencyGraph);
}
// Return exit code based on problems
if (!empty($problems)) {
return ExitCode::FAILURE;
}
if (!empty($warnings)) {
return ExitCode::PARTIAL_SUCCESS;
}
$output->writeLine('✅ All initializers are valid', ConsoleColor::GREEN);
return ExitCode::SUCCESS;
}
private function outputHumanReadable(
ConsoleOutputInterface $output,
array $problems,
array $warnings,
array $info,
InitializerDependencyGraph $graph
): void {
// Show problems
if (!empty($problems)) {
$output->writeLine('❌ Problems:', ConsoleColor::BRIGHT_RED);
foreach ($problems as $problem) {
$output->writeLine("{$problem['message']}", ConsoleColor::RED);
if (isset($problem['class'])) {
$output->writeLine(" Class: {$problem['class']}", ConsoleColor::GRAY);
}
if (isset($problem['cycle'])) {
$cycleStr = implode(' → ', $problem['cycle']) . ' → ' . ($problem['cycle'][0] ?? '');
$output->writeLine(" Cycle: {$cycleStr}", ConsoleColor::GRAY);
}
if (isset($problem['dependency_path'])) {
$pathStr = implode(' → ', $problem['dependency_path']);
$output->writeLine(" Path: {$pathStr}", ConsoleColor::GRAY);
}
}
$output->newLine();
}
// Show warnings
if (!empty($warnings)) {
$output->writeLine('⚠️ Warnings:', ConsoleColor::BRIGHT_YELLOW);
foreach ($warnings as $warning) {
$output->writeLine("{$warning['message']}", ConsoleColor::YELLOW);
$output->writeLine(" Class: {$warning['class']}::{$warning['method']}", ConsoleColor::GRAY);
if (isset($warning['missing_dependencies'])) {
$depsStr = implode(', ', $warning['missing_dependencies']);
$output->writeLine(" Missing: {$depsStr}", ConsoleColor::GRAY);
}
}
$output->newLine();
}
// Show summary
$output->writeLine('📊 Summary:', ConsoleColor::BRIGHT_CYAN);
$output->writeLine(" Total initializers: " . count($info), ConsoleColor::WHITE);
$output->writeLine(" Problems: " . count($problems), ConsoleColor::RED);
$output->writeLine(" Warnings: " . count($warnings), ConsoleColor::YELLOW);
// Show dependency graph visualization
$output->newLine();
$output->writeLine('🔗 Dependency Graph:', ConsoleColor::BRIGHT_CYAN);
try {
$executionOrder = $graph->getExecutionOrder();
foreach ($executionOrder as $index => $returnType) {
$node = $graph->getNode($returnType);
if ($node === null) {
continue;
}
$deps = empty($node->dependencies)
? 'no dependencies'
: 'depends on: ' . implode(', ', $node->dependencies);
$output->writeLine(
sprintf(' %d. %s', $index + 1, $node->toString()),
ConsoleColor::WHITE
);
}
} catch (InitializerCycleException $e) {
$output->writeLine(' ⚠️ Cannot show graph due to circular dependencies', ConsoleColor::YELLOW);
}
}
private function outputJson(ConsoleOutputInterface $output, array $data): void
{
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
$output->writeLine($json);
}
}