refactor: reorganize project structure for better maintainability
- Move 45 debug/test files from root to organized scripts/ directories - Secure public/ directory by removing debug files (security improvement) - Create structured scripts organization: • scripts/debug/ (20 files) - Framework debugging tools • scripts/test/ (18 files) - Test and validation scripts • scripts/maintenance/ (5 files) - Maintenance utilities • scripts/dev/ (2 files) - Development tools Security improvements: - Removed all debug/test files from public/ directory - Only production files remain: index.php, health.php Root directory cleanup: - Reduced from 47 to 2 PHP files in root - Only essential production files: console.php, worker.php This improves: ✅ Security (no debug code in public/) ✅ Organization (clear separation of concerns) ✅ Maintainability (easy to find and manage scripts) ✅ Professional structure (clean root directory)
This commit is contained in:
78
scripts/debug/debug-container.php
Normal file
78
scripts/debug/debug-container.php
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Framework\Attributes\Route;
|
||||||
|
use App\Framework\Core\AppBootstrapper;
|
||||||
|
use App\Framework\Discovery\Results\DiscoveryRegistry;
|
||||||
|
use App\Framework\Performance\EnhancedPerformanceCollector;
|
||||||
|
use App\Framework\DateTime\SystemClock;
|
||||||
|
use App\Framework\DateTime\SystemHighResolutionClock;
|
||||||
|
use App\Framework\Performance\MemoryMonitor;
|
||||||
|
|
||||||
|
require __DIR__ . '/vendor/autoload.php';
|
||||||
|
require __DIR__ . '/src/Framework/Debug/helpers.php';
|
||||||
|
|
||||||
|
echo "DEBUG: Starting container debug\n";
|
||||||
|
|
||||||
|
// Create dependencies for enhanced performance collector
|
||||||
|
$basePath = __DIR__;
|
||||||
|
$clock = new SystemClock();
|
||||||
|
$highResClock = new SystemHighResolutionClock();
|
||||||
|
$memoryMonitor = new MemoryMonitor();
|
||||||
|
$collector = new EnhancedPerformanceCollector($clock, $highResClock, $memoryMonitor, enabled: true);
|
||||||
|
|
||||||
|
echo "DEBUG: About to create AppBootstrapper\n";
|
||||||
|
$bootstrapper = new AppBootstrapper($basePath, $collector, $memoryMonitor);
|
||||||
|
|
||||||
|
echo "DEBUG: About to bootstrap web app\n";
|
||||||
|
$app = $bootstrapper->bootstrapWeb();
|
||||||
|
|
||||||
|
echo "DEBUG: Web app bootstrapped successfully\n";
|
||||||
|
|
||||||
|
// Get container from the app
|
||||||
|
$reflection = new ReflectionObject($app);
|
||||||
|
$containerProperty = $reflection->getProperty('container');
|
||||||
|
$containerProperty->setAccessible(true);
|
||||||
|
$container = $containerProperty->getValue($app);
|
||||||
|
|
||||||
|
echo "DEBUG: Got container from app\n";
|
||||||
|
|
||||||
|
// Check if DiscoveryRegistry is available
|
||||||
|
if ($container->has(DiscoveryRegistry::class)) {
|
||||||
|
echo "DEBUG: DiscoveryRegistry is available in container\n";
|
||||||
|
$registry = $container->get(DiscoveryRegistry::class);
|
||||||
|
|
||||||
|
$routeCount = $registry->attributes->getCount(Route::class);
|
||||||
|
echo "DEBUG: Found $routeCount route attributes in registry\n";
|
||||||
|
|
||||||
|
if ($routeCount > 0) {
|
||||||
|
$routes = $registry->attributes->get(Route::class);
|
||||||
|
echo "DEBUG: First few routes:\n";
|
||||||
|
foreach (array_slice($routes, 0, 3) as $i => $route) {
|
||||||
|
echo " Route $i: " . $route->className->getFullyQualified() . "::" . $route->methodName?->toString() . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for the home route specifically
|
||||||
|
echo "DEBUG: Looking for home route (/)\n";
|
||||||
|
$homeFound = false;
|
||||||
|
foreach ($routes as $route) {
|
||||||
|
$instance = $route->createAttributeInstance();
|
||||||
|
if ($instance instanceof \App\Framework\Attributes\Route && $instance->path === '/') {
|
||||||
|
echo " HOME ROUTE FOUND: " . $route->className->getFullyQualified() . "::" . $route->methodName?->toString() . " -> " . $instance->path . "\n";
|
||||||
|
$homeFound = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$homeFound) {
|
||||||
|
echo " HOME ROUTE NOT FOUND in discovery results\n";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo "DEBUG: No routes found in registry\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "DEBUG: Total discovery items: " . count($registry) . "\n";
|
||||||
|
} else {
|
||||||
|
echo "DEBUG: DiscoveryRegistry is NOT available in container\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "DEBUG: Debug complete\n";
|
||||||
87
scripts/debug/debug-contexts.php
Normal file
87
scripts/debug/debug-contexts.php
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Framework\Attributes\Route;
|
||||||
|
use App\Framework\Core\AppBootstrapper;
|
||||||
|
use App\Framework\Discovery\Results\DiscoveryRegistry;
|
||||||
|
use App\Framework\Performance\EnhancedPerformanceCollector;
|
||||||
|
use App\Framework\DateTime\SystemClock;
|
||||||
|
use App\Framework\DateTime\SystemHighResolutionClock;
|
||||||
|
use App\Framework\Performance\MemoryMonitor;
|
||||||
|
use App\Framework\DI\Initializer;
|
||||||
|
|
||||||
|
require __DIR__ . '/vendor/autoload.php';
|
||||||
|
require __DIR__ . '/src/Framework/Debug/helpers.php';
|
||||||
|
|
||||||
|
echo "DEBUG: Starting context analysis\n";
|
||||||
|
|
||||||
|
// Create dependencies for enhanced performance collector
|
||||||
|
$basePath = __DIR__;
|
||||||
|
$clock = new SystemClock();
|
||||||
|
$highResClock = new SystemHighResolutionClock();
|
||||||
|
$memoryMonitor = new MemoryMonitor();
|
||||||
|
$collector = new EnhancedPerformanceCollector($clock, $highResClock, $memoryMonitor, enabled: true);
|
||||||
|
|
||||||
|
echo "DEBUG: About to create AppBootstrapper\n";
|
||||||
|
$bootstrapper = new AppBootstrapper($basePath, $collector, $memoryMonitor);
|
||||||
|
|
||||||
|
echo "DEBUG: About to bootstrap web app\n";
|
||||||
|
$app = $bootstrapper->bootstrapWeb();
|
||||||
|
|
||||||
|
echo "DEBUG: Web app bootstrapped successfully\n";
|
||||||
|
|
||||||
|
// Get container from the app
|
||||||
|
$reflection = new ReflectionObject($app);
|
||||||
|
$containerProperty = $reflection->getProperty('container');
|
||||||
|
$containerProperty->setAccessible(true);
|
||||||
|
$container = $containerProperty->getValue($app);
|
||||||
|
|
||||||
|
echo "DEBUG: Got container from app\n";
|
||||||
|
|
||||||
|
// Check if DiscoveryRegistry is available
|
||||||
|
if ($container->has(DiscoveryRegistry::class)) {
|
||||||
|
echo "DEBUG: DiscoveryRegistry is available in container\n";
|
||||||
|
$registry = $container->get(DiscoveryRegistry::class);
|
||||||
|
|
||||||
|
$initializerCount = $registry->attributes->getCount(Initializer::class);
|
||||||
|
echo "DEBUG: Found $initializerCount initializer attributes in registry\n";
|
||||||
|
|
||||||
|
if ($initializerCount > 0) {
|
||||||
|
$initializers = $registry->attributes->get(Initializer::class);
|
||||||
|
echo "DEBUG: Looking for router-related initializers\n";
|
||||||
|
|
||||||
|
foreach ($initializers as $initializer) {
|
||||||
|
$className = $initializer->className->getFullyQualified();
|
||||||
|
|
||||||
|
if (str_contains($className, 'Router') || str_contains($className, 'Route')) {
|
||||||
|
echo " ROUTER INITIALIZER: $className\n";
|
||||||
|
|
||||||
|
// Get the attribute instance to see the contexts
|
||||||
|
$attributeInstance = $initializer->createAttributeInstance();
|
||||||
|
if ($attributeInstance instanceof \App\Framework\DI\Initializer) {
|
||||||
|
$contexts = $attributeInstance->contexts;
|
||||||
|
if (empty($contexts)) {
|
||||||
|
echo " CONTEXTS: ALL (no restrictions)\n";
|
||||||
|
} else {
|
||||||
|
$contextNames = array_map(function($ctx) {
|
||||||
|
return $ctx instanceof \App\Framework\Context\ContextType ? $ctx->name : (string)$ctx;
|
||||||
|
}, $contexts);
|
||||||
|
echo " CONTEXTS: " . implode(', ', $contextNames) . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check additionalData for debugging
|
||||||
|
if (isset($initializer->additionalData['contexts'])) {
|
||||||
|
echo " STORED CONTEXTS: " . print_r($initializer->additionalData['contexts'], true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo "DEBUG: No initializers found in registry\n";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo "DEBUG: DiscoveryRegistry is NOT available in container\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "DEBUG: Context analysis complete\n";
|
||||||
57
scripts/debug/debug_initializers.php
Normal file
57
scripts/debug/debug_initializers.php
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
use App\Framework\Core\AppBootstrapper;
|
||||||
|
use App\Framework\Performance\EnhancedPerformanceCollector;
|
||||||
|
use App\Framework\DateTime\SystemClock;
|
||||||
|
use App\Framework\DateTime\SystemHighResolutionClock;
|
||||||
|
use App\Framework\Performance\MemoryMonitor;
|
||||||
|
|
||||||
|
echo "Testing Queue Initializer Discovery...\n\n";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create dependencies for enhanced performance collector
|
||||||
|
$clock = new SystemClock();
|
||||||
|
$highResClock = new SystemHighResolutionClock();
|
||||||
|
$memoryMonitor = new MemoryMonitor();
|
||||||
|
$collector = new EnhancedPerformanceCollector($clock, $highResClock, $memoryMonitor, enabled: false);
|
||||||
|
|
||||||
|
// Bootstrap the framework
|
||||||
|
$bootstrapper = new AppBootstrapper(__DIR__, $collector, $memoryMonitor);
|
||||||
|
$container = $bootstrapper->bootstrapWorker();
|
||||||
|
|
||||||
|
echo "Framework bootstrapped successfully.\n";
|
||||||
|
|
||||||
|
// Check if Queue services are available
|
||||||
|
$services = [
|
||||||
|
'App\Framework\Queue\Interfaces\DistributedLockInterface',
|
||||||
|
'App\Framework\Queue\Services\WorkerRegistry',
|
||||||
|
'App\Framework\Queue\Services\JobDistributionService',
|
||||||
|
'App\Framework\Queue\Services\WorkerHealthCheckService',
|
||||||
|
'App\Framework\Queue\Services\FailoverRecoveryService',
|
||||||
|
'App\Framework\Queue\Contracts\JobProgressTrackerInterface',
|
||||||
|
'App\Framework\Queue\Contracts\DeadLetterQueueInterface',
|
||||||
|
'App\Framework\Queue\Services\JobMetricsManagerInterface',
|
||||||
|
'App\Framework\Queue\Contracts\JobDependencyManagerInterface'
|
||||||
|
];
|
||||||
|
|
||||||
|
echo "\nChecking Queue service registrations:\n";
|
||||||
|
echo str_repeat("=", 50) . "\n";
|
||||||
|
|
||||||
|
foreach ($services as $service) {
|
||||||
|
if ($container->has($service)) {
|
||||||
|
echo "✅ {$service} - REGISTERED\n";
|
||||||
|
} else {
|
||||||
|
echo "❌ {$service} - NOT REGISTERED\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\nTotal container bindings: " . count($container->getBindings()) . "\n";
|
||||||
|
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
echo "Error: " . $e->getMessage() . "\n";
|
||||||
|
echo "File: " . $e->getFile() . ":" . $e->getLine() . "\n";
|
||||||
|
}
|
||||||
263
scripts/debug/debug_live_tui.php
Normal file
263
scripts/debug/debug_live_tui.php
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
// Live debug version of TUI that shows what's happening
|
||||||
|
use App\Framework\Console\Components\TuiState;
|
||||||
|
use App\Framework\Console\Components\TuiInputHandler;
|
||||||
|
use App\Framework\Console\Components\TuiCommandExecutor;
|
||||||
|
use App\Framework\Console\Components\TuiRenderer;
|
||||||
|
use App\Framework\Console\CommandHistory;
|
||||||
|
use App\Framework\Console\CommandGroupRegistry;
|
||||||
|
use App\Framework\Console\SimpleWorkflowExecutor;
|
||||||
|
use App\Framework\Console\TuiView;
|
||||||
|
use App\Framework\Console\TuiKeyCode;
|
||||||
|
use App\Framework\Console\ConsoleOutput;
|
||||||
|
use App\Framework\Console\ConsoleColor;
|
||||||
|
use App\Framework\Console\Screen\ScreenManager;
|
||||||
|
use App\Framework\Console\Screen\ScreenControlCode;
|
||||||
|
use App\Framework\Console\Screen\CursorControlCode;
|
||||||
|
use App\Framework\Discovery\Results\DiscoveryRegistry;
|
||||||
|
use App\Framework\DI\Container;
|
||||||
|
|
||||||
|
echo "Live TUI Debug Session\n";
|
||||||
|
echo "======================\n\n";
|
||||||
|
|
||||||
|
// Check TTY
|
||||||
|
if (!posix_isatty(STDIN)) {
|
||||||
|
echo "❌ Requires TTY. Run with: docker exec -it php php debug_live_tui.php\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "✓ TTY available\n";
|
||||||
|
|
||||||
|
// Check PHPStorm
|
||||||
|
$isPhpStorm = getenv('TERMINAL_EMULATOR') === 'JetBrains-JediTerm';
|
||||||
|
echo "✓ PHPStorm detected: " . ($isPhpStorm ? "YES" : "NO") . "\n\n";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create minimal dependencies
|
||||||
|
$container = new \App\Framework\DI\DefaultContainer();
|
||||||
|
|
||||||
|
$discoveryRegistry = new class implements DiscoveryRegistry {
|
||||||
|
public function interfaces() {
|
||||||
|
return new class { public function get(string $interface) { return []; } };
|
||||||
|
}
|
||||||
|
public function attributes() {
|
||||||
|
return new class { public function get(string $attributeClass) { return []; } };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Debug command executor
|
||||||
|
$debugExecutor = new class implements TuiCommandExecutor {
|
||||||
|
public function executeSelectedCommand(object $command): void {
|
||||||
|
echo "\n🚀 EXECUTE: Selected command\n";
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
|
public function executeCommand(string $commandName): void {
|
||||||
|
echo "\n🚀 EXECUTE: $commandName\n";
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
|
public function validateSelectedCommand(object $command): void {
|
||||||
|
echo "\n✓ VALIDATE: Selected command\n";
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
|
public function validateCommand(string $commandName): void {
|
||||||
|
echo "\n✓ VALIDATE: $commandName\n";
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
|
public function showSelectedCommandHelp(object $command): void {
|
||||||
|
echo "\n📚 HELP: Selected command\n";
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
|
public function showCommandHelp(string $commandName): void {
|
||||||
|
echo "\n📚 HELP: $commandName\n";
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
|
public function showAllCommandsHelp(): void {
|
||||||
|
echo "\n📚 HELP: All commands\n";
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
|
public function startInteractiveForm(object $command, \App\Framework\Console\Components\TuiState $state): void {
|
||||||
|
echo "\n📝 FORM: Start interactive form\n";
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
|
public function findCommandObject(string $commandName): ?object {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$workflowExecutor = new class implements SimpleWorkflowExecutor {
|
||||||
|
public function executeWorkflow(array $workflow, \App\Framework\Console\Components\TuiState $state): void {
|
||||||
|
echo "\n🔄 WORKFLOW: Execute\n";
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create output with debug info
|
||||||
|
$output = new class extends ConsoleOutput {
|
||||||
|
public function writeLine(string $line = '', ?ConsoleColor $color = null): void {
|
||||||
|
// Show debug info for navigation updates
|
||||||
|
if (str_contains($line, '▶')) {
|
||||||
|
echo "\n[DEBUG] Navigation update detected: $line\n";
|
||||||
|
}
|
||||||
|
parent::writeLine($line, $color);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$output->screen = new ScreenManager($output);
|
||||||
|
|
||||||
|
// Custom TUI state with debug
|
||||||
|
$debugState = new class extends TuiState {
|
||||||
|
public function setSelectedCategory(int $index): void {
|
||||||
|
$oldIndex = $this->getSelectedCategory();
|
||||||
|
parent::setSelectedCategory($index);
|
||||||
|
$newIndex = $this->getSelectedCategory();
|
||||||
|
echo "\n[DEBUG] Category changed: $oldIndex → $newIndex\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
public function navigateUp(): void {
|
||||||
|
echo "\n[DEBUG] navigateUp() called\n";
|
||||||
|
$before = $this->getSelectedCategory();
|
||||||
|
parent::navigateUp();
|
||||||
|
$after = $this->getSelectedCategory();
|
||||||
|
echo "[DEBUG] navigateUp result: $before → $after\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
public function navigateDown(): void {
|
||||||
|
echo "\n[DEBUG] navigateDown() called\n";
|
||||||
|
$before = $this->getSelectedCategory();
|
||||||
|
parent::navigateDown();
|
||||||
|
$after = $this->getSelectedCategory();
|
||||||
|
echo "[DEBUG] navigateDown result: $before → $after\n";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Custom input handler with debug
|
||||||
|
$debugInputHandler = new class($debugExecutor) extends TuiInputHandler {
|
||||||
|
public function handleInput(string $key, \App\Framework\Console\Components\TuiState $state, \App\Framework\Console\CommandHistory $history): void {
|
||||||
|
$keyHex = bin2hex($key);
|
||||||
|
$keyDesc = match($key) {
|
||||||
|
"\033[A" => "ARROW_UP",
|
||||||
|
"\033[B" => "ARROW_DOWN",
|
||||||
|
"\033[C" => "ARROW_RIGHT",
|
||||||
|
"\033[D" => "ARROW_LEFT",
|
||||||
|
"\n" => "ENTER",
|
||||||
|
" " => "SPACE",
|
||||||
|
"\033" => "ESC",
|
||||||
|
'q' => "Q",
|
||||||
|
default => "OTHER($key)"
|
||||||
|
};
|
||||||
|
|
||||||
|
echo "\n[DEBUG] INPUT: '$keyDesc' (hex: $keyHex) in view: " . $state->getCurrentView()->name . "\n";
|
||||||
|
|
||||||
|
parent::handleInput($key, $state, $history);
|
||||||
|
|
||||||
|
echo "[DEBUG] After input - Selected: " . $state->getSelectedCategory() . "\n";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$state = $debugState;
|
||||||
|
$history = new CommandHistory();
|
||||||
|
$inputHandler = $debugInputHandler;
|
||||||
|
$renderer = new TuiRenderer($output);
|
||||||
|
$groupRegistry = new CommandGroupRegistry($discoveryRegistry);
|
||||||
|
|
||||||
|
// Add test categories
|
||||||
|
$testCategories = [
|
||||||
|
['name' => 'Testing', 'description' => 'Test commands', 'icon' => '🧪', 'commands' => []],
|
||||||
|
['name' => 'Demo', 'description' => 'Demo commands', 'icon' => '🎮', 'commands' => []],
|
||||||
|
['name' => 'Generator', 'description' => 'Code generation', 'icon' => '⚙️', 'commands' => []],
|
||||||
|
['name' => 'General', 'description' => 'General commands', 'icon' => '📂', 'commands' => []]
|
||||||
|
];
|
||||||
|
|
||||||
|
$state->setCategories($testCategories);
|
||||||
|
$state->setCurrentView(TuiView::CATEGORIES);
|
||||||
|
$state->setRunning(true);
|
||||||
|
|
||||||
|
echo "✓ Debug TUI components ready\n";
|
||||||
|
echo "Categories: " . count($testCategories) . "\n";
|
||||||
|
echo "Selected: " . $state->getSelectedCategory() . "\n\n";
|
||||||
|
|
||||||
|
// Save terminal settings
|
||||||
|
$originalSettings = trim(shell_exec('stty -g') ?: '');
|
||||||
|
|
||||||
|
// Set up terminal
|
||||||
|
if ($isPhpStorm) {
|
||||||
|
shell_exec('stty raw -echo min 1 time 0 2>/dev/null');
|
||||||
|
} else {
|
||||||
|
shell_exec('stty -icanon -echo 2>/dev/null');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide cursor
|
||||||
|
echo CursorControlCode::HIDE->format();
|
||||||
|
|
||||||
|
echo "🚀 LIVE DEBUG TUI STARTED\n";
|
||||||
|
echo "Use arrow keys to test navigation.\n";
|
||||||
|
echo "Press 'q' to quit.\n";
|
||||||
|
echo "All navigation events will be logged below.\n\n";
|
||||||
|
|
||||||
|
// Simple TUI loop with debug
|
||||||
|
while ($state->isRunning()) {
|
||||||
|
// Clear and render
|
||||||
|
echo ScreenControlCode::CLEAR_ALL->format();
|
||||||
|
echo CursorControlCode::POSITION->format(1, 1);
|
||||||
|
|
||||||
|
// Render current state
|
||||||
|
$renderer->render($state, $history);
|
||||||
|
|
||||||
|
// Show debug info at bottom
|
||||||
|
echo "\n" . str_repeat('=', 60) . "\n";
|
||||||
|
echo "DEBUG INFO:\n";
|
||||||
|
echo "Selected Category: " . $state->getSelectedCategory() . "\n";
|
||||||
|
echo "Current View: " . $state->getCurrentView()->name . "\n";
|
||||||
|
$category = $state->getCurrentCategory();
|
||||||
|
echo "Category Name: " . ($category ? $category['name'] : 'NULL') . "\n";
|
||||||
|
echo "Press arrow keys to test navigation...\n";
|
||||||
|
|
||||||
|
// Read input with PHPStorm-compatible method
|
||||||
|
$key = fgetc(STDIN);
|
||||||
|
if ($key === false) continue;
|
||||||
|
|
||||||
|
if ($key === "\033") {
|
||||||
|
$sequence = $key;
|
||||||
|
stream_set_blocking(STDIN, false);
|
||||||
|
$next = fgetc(STDIN);
|
||||||
|
if ($next === false) {
|
||||||
|
usleep(10000);
|
||||||
|
$next = fgetc(STDIN);
|
||||||
|
}
|
||||||
|
if ($next !== false) {
|
||||||
|
$sequence .= $next;
|
||||||
|
if ($next === '[') {
|
||||||
|
$third = fgetc(STDIN);
|
||||||
|
if ($third === false) {
|
||||||
|
usleep(10000);
|
||||||
|
$third = fgetc(STDIN);
|
||||||
|
}
|
||||||
|
if ($third !== false) {
|
||||||
|
$sequence .= $third;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stream_set_blocking(STDIN, true);
|
||||||
|
$key = $sequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($key === 'q' || $key === 'Q') {
|
||||||
|
$state->setRunning(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process input
|
||||||
|
$inputHandler->handleInput($key, $state, $history);
|
||||||
|
}
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
// Restore terminal
|
||||||
|
echo CursorControlCode::SHOW->format();
|
||||||
|
if (!empty($originalSettings)) {
|
||||||
|
shell_exec("stty $originalSettings 2>/dev/null");
|
||||||
|
}
|
||||||
|
echo "\n✓ Debug TUI session ended\n";
|
||||||
|
}
|
||||||
167
scripts/debug/debug_navigation.php
Normal file
167
scripts/debug/debug_navigation.php
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
// Debug navigation issue
|
||||||
|
use App\Framework\Console\Components\TuiState;
|
||||||
|
use App\Framework\Console\Components\TuiInputHandler;
|
||||||
|
use App\Framework\Console\Components\TuiCommandExecutor;
|
||||||
|
use App\Framework\Console\CommandHistory;
|
||||||
|
use App\Framework\Console\TuiView;
|
||||||
|
use App\Framework\Console\TuiKeyCode;
|
||||||
|
|
||||||
|
echo "Debugging TUI Navigation Issue...\n";
|
||||||
|
echo "=================================\n\n";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create mock executor
|
||||||
|
$mockExecutor = new class implements \App\Framework\Console\Components\TuiCommandExecutor {
|
||||||
|
public function executeSelectedCommand(object $command): void {
|
||||||
|
echo "Mock: Execute command\n";
|
||||||
|
}
|
||||||
|
public function executeCommand(string $commandName): void {
|
||||||
|
echo "Mock: Execute command: $commandName\n";
|
||||||
|
}
|
||||||
|
public function validateSelectedCommand(object $command): void {
|
||||||
|
echo "Mock: Validate command\n";
|
||||||
|
}
|
||||||
|
public function validateCommand(string $commandName): void {
|
||||||
|
echo "Mock: Validate command: $commandName\n";
|
||||||
|
}
|
||||||
|
public function showSelectedCommandHelp(object $command): void {
|
||||||
|
echo "Mock: Show help\n";
|
||||||
|
}
|
||||||
|
public function showCommandHelp(string $commandName): void {
|
||||||
|
echo "Mock: Show help: $commandName\n";
|
||||||
|
}
|
||||||
|
public function showAllCommandsHelp(): void {
|
||||||
|
echo "Mock: Show all commands help\n";
|
||||||
|
}
|
||||||
|
public function startInteractiveForm(object $command, \App\Framework\Console\Components\TuiState $state): void {
|
||||||
|
echo "Mock: Start form\n";
|
||||||
|
}
|
||||||
|
public function findCommandObject(string $commandName): ?object {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$state = new TuiState();
|
||||||
|
$history = new CommandHistory();
|
||||||
|
$inputHandler = new TuiInputHandler($mockExecutor);
|
||||||
|
|
||||||
|
// Setup realistic test categories like in real TUI
|
||||||
|
$categories = [
|
||||||
|
0 => [
|
||||||
|
'name' => 'Database',
|
||||||
|
'description' => 'Database commands',
|
||||||
|
'icon' => '🗄️',
|
||||||
|
'commands' => [],
|
||||||
|
'priority' => 100
|
||||||
|
],
|
||||||
|
1 => [
|
||||||
|
'name' => 'Cache',
|
||||||
|
'description' => 'Cache commands',
|
||||||
|
'icon' => '⚡',
|
||||||
|
'commands' => [],
|
||||||
|
'priority' => 90
|
||||||
|
],
|
||||||
|
2 => [
|
||||||
|
'name' => 'Testing',
|
||||||
|
'description' => 'Testing commands',
|
||||||
|
'icon' => '🧪',
|
||||||
|
'commands' => [],
|
||||||
|
'priority' => 80
|
||||||
|
],
|
||||||
|
3 => [
|
||||||
|
'name' => 'MCP',
|
||||||
|
'description' => 'MCP commands',
|
||||||
|
'icon' => '🤖',
|
||||||
|
'commands' => [],
|
||||||
|
'priority' => 70
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$state->setCategories($categories);
|
||||||
|
$state->setCurrentView(TuiView::CATEGORIES);
|
||||||
|
$state->setRunning(true);
|
||||||
|
|
||||||
|
echo "✓ Initial Setup:\n";
|
||||||
|
echo " Categories: " . count($categories) . "\n";
|
||||||
|
echo " Current View: " . $state->getCurrentView()->name . "\n";
|
||||||
|
echo " Selected Category: " . $state->getSelectedCategory() . "\n";
|
||||||
|
echo " Category Name: '{$categories[$state->getSelectedCategory()]['name']}'\n\n";
|
||||||
|
|
||||||
|
// Test step-by-step navigation
|
||||||
|
echo "🔍 Testing Navigation Step-by-Step:\n\n";
|
||||||
|
|
||||||
|
// Test 1: Arrow Down
|
||||||
|
echo "Test 1: Arrow DOWN\n";
|
||||||
|
echo " Before: Category {$state->getSelectedCategory()} ('{$categories[$state->getSelectedCategory()]['name']}')\n";
|
||||||
|
echo " Input Key: '" . TuiKeyCode::ARROW_DOWN->value . "' (hex: " . bin2hex(TuiKeyCode::ARROW_DOWN->value) . ")\n";
|
||||||
|
|
||||||
|
$inputHandler->handleInput(TuiKeyCode::ARROW_DOWN->value, $state, $history);
|
||||||
|
|
||||||
|
echo " After: Category {$state->getSelectedCategory()} ('{$categories[$state->getSelectedCategory()]['name']}')\n";
|
||||||
|
echo " ✓ Expected: Should move from Database (0) to Cache (1)\n\n";
|
||||||
|
|
||||||
|
// Test 2: Arrow Down again
|
||||||
|
echo "Test 2: Arrow DOWN again\n";
|
||||||
|
echo " Before: Category {$state->getSelectedCategory()} ('{$categories[$state->getSelectedCategory()]['name']}')\n";
|
||||||
|
|
||||||
|
$inputHandler->handleInput(TuiKeyCode::ARROW_DOWN->value, $state, $history);
|
||||||
|
|
||||||
|
echo " After: Category {$state->getSelectedCategory()} ('{$categories[$state->getSelectedCategory()]['name']}')\n";
|
||||||
|
echo " ✓ Expected: Should move from Cache (1) to Testing (2)\n\n";
|
||||||
|
|
||||||
|
// Test 3: Arrow Up
|
||||||
|
echo "Test 3: Arrow UP\n";
|
||||||
|
echo " Before: Category {$state->getSelectedCategory()} ('{$categories[$state->getSelectedCategory()]['name']}')\n";
|
||||||
|
|
||||||
|
$inputHandler->handleInput(TuiKeyCode::ARROW_UP->value, $state, $history);
|
||||||
|
|
||||||
|
echo " After: Category {$state->getSelectedCategory()} ('{$categories[$state->getSelectedCategory()]['name']}')\n";
|
||||||
|
echo " ✓ Expected: Should move from Testing (2) to Cache (1)\n\n";
|
||||||
|
|
||||||
|
// Test 4: Boundary testing - go to end
|
||||||
|
echo "Test 4: Go to last category and test boundary\n";
|
||||||
|
$state->setSelectedCategory(3); // MCP
|
||||||
|
echo " Set to: Category {$state->getSelectedCategory()} ('{$categories[$state->getSelectedCategory()]['name']}')\n";
|
||||||
|
|
||||||
|
$inputHandler->handleInput(TuiKeyCode::ARROW_DOWN->value, $state, $history);
|
||||||
|
|
||||||
|
echo " After Arrow DOWN: Category {$state->getSelectedCategory()} ('{$categories[$state->getSelectedCategory()]['name']}')\n";
|
||||||
|
echo " ✓ Expected: Should stay at MCP (3) - boundary protection\n\n";
|
||||||
|
|
||||||
|
// Test 5: Boundary testing - go to beginning
|
||||||
|
echo "Test 5: Go to first category and test boundary\n";
|
||||||
|
$state->setSelectedCategory(0); // Database
|
||||||
|
echo " Set to: Category {$state->getSelectedCategory()} ('{$categories[$state->getSelectedCategory()]['name']}')\n";
|
||||||
|
|
||||||
|
$inputHandler->handleInput(TuiKeyCode::ARROW_UP->value, $state, $history);
|
||||||
|
|
||||||
|
echo " After Arrow UP: Category {$state->getSelectedCategory()} ('{$categories[$state->getSelectedCategory()]['name']}')\n";
|
||||||
|
echo " ✓ Expected: Should stay at Database (0) - boundary protection\n\n";
|
||||||
|
|
||||||
|
// Debug the TuiState navigation methods directly
|
||||||
|
echo "🔍 Testing TuiState Navigation Methods Directly:\n\n";
|
||||||
|
|
||||||
|
echo "Direct TuiState Testing:\n";
|
||||||
|
$state->setSelectedCategory(1); // Cache
|
||||||
|
echo " Set to: {$state->getSelectedCategory()}\n";
|
||||||
|
|
||||||
|
echo " Calling navigateDown()...\n";
|
||||||
|
$state->navigateDown();
|
||||||
|
echo " Result: {$state->getSelectedCategory()}\n";
|
||||||
|
|
||||||
|
echo " Calling navigateUp()...\n";
|
||||||
|
$state->navigateUp();
|
||||||
|
echo " Result: {$state->getSelectedCategory()}\n\n";
|
||||||
|
|
||||||
|
echo "✅ Navigation Debug Test COMPLETED\n";
|
||||||
|
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
echo "\n❌ Navigation Debug Test FAILED:\n";
|
||||||
|
echo "Error: " . $e->getMessage() . "\n";
|
||||||
|
echo "File: " . $e->getFile() . ":" . $e->getLine() . "\n";
|
||||||
|
echo "\nStack trace:\n" . $e->getTraceAsString() . "\n";
|
||||||
|
}
|
||||||
80
scripts/debug/debug_navigation_issue.php
Normal file
80
scripts/debug/debug_navigation_issue.php
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
// Debug das TUI Navigation Problem
|
||||||
|
use App\Framework\Console\Components\TuiState;
|
||||||
|
use App\Framework\Console\TuiView;
|
||||||
|
use App\Framework\Console\CommandGroupRegistry;
|
||||||
|
use App\Framework\Discovery\Results\DiscoveryRegistry;
|
||||||
|
|
||||||
|
echo "=== TUI NAVIGATION DEBUG ===\n";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Mock Discovery Registry
|
||||||
|
$discoveryRegistry = new class implements DiscoveryRegistry {
|
||||||
|
public function interfaces() {
|
||||||
|
return new class {
|
||||||
|
public function get(string $interface) { return []; }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
public function attributes() {
|
||||||
|
return new class {
|
||||||
|
public function get(string $attributeClass) { return []; }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$groupRegistry = new CommandGroupRegistry($discoveryRegistry);
|
||||||
|
$categories = $groupRegistry->getOrganizedCommands();
|
||||||
|
|
||||||
|
echo "📊 Categories loaded: " . count($categories) . "\n";
|
||||||
|
foreach ($categories as $index => $category) {
|
||||||
|
echo " [$index] {$category['name']} - Commands: " . count($category['commands']) . "\n";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
// Test TuiState Navigation
|
||||||
|
$state = new TuiState();
|
||||||
|
$state->setCategories($categories);
|
||||||
|
$state->setCurrentView(TuiView::CATEGORIES);
|
||||||
|
|
||||||
|
echo "🔍 Initial TUI State:\n";
|
||||||
|
echo " Selected Category: " . $state->getSelectedCategory() . "\n";
|
||||||
|
echo " Current View: " . $state->getCurrentView()->name . "\n";
|
||||||
|
echo " Current Category: " . ($state->getCurrentCategory()['name'] ?? 'NULL') . "\n";
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
// Test Navigation Up/Down
|
||||||
|
echo "🧪 Testing Navigation:\n";
|
||||||
|
|
||||||
|
for ($i = 0; $i < 3; $i++) {
|
||||||
|
echo "Step $i - Before navigateDown(): " . $state->getSelectedCategory() . "\n";
|
||||||
|
$state->navigateDown();
|
||||||
|
echo "Step $i - After navigateDown(): " . $state->getSelectedCategory() . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
for ($i = 0; $i < 5; $i++) {
|
||||||
|
echo "Step $i - Before navigateUp(): " . $state->getSelectedCategory() . "\n";
|
||||||
|
$state->navigateUp();
|
||||||
|
echo "Step $i - After navigateUp(): " . $state->getSelectedCategory() . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
// Test Category Bounds
|
||||||
|
echo "🎯 Testing Bounds:\n";
|
||||||
|
$state->setSelectedCategory(99);
|
||||||
|
echo "Set to 99, actual: " . $state->getSelectedCategory() . "\n";
|
||||||
|
$state->setSelectedCategory(-5);
|
||||||
|
echo "Set to -5, actual: " . $state->getSelectedCategory() . "\n";
|
||||||
|
|
||||||
|
echo "\n✅ Navigation logic test completed\n";
|
||||||
|
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
echo "❌ ERROR: " . $e->getMessage() . "\n";
|
||||||
|
echo "File: " . $e->getFile() . ":" . $e->getLine() . "\n";
|
||||||
|
echo "Stack trace:\n" . $e->getTraceAsString() . "\n";
|
||||||
|
}
|
||||||
171
scripts/debug/debug_real_tui.php
Normal file
171
scripts/debug/debug_real_tui.php
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
// Debug real TUI execution
|
||||||
|
use App\Framework\Console\Components\ConsoleTUI;
|
||||||
|
use App\Framework\Console\Components\TuiState;
|
||||||
|
use App\Framework\Console\Components\TuiInputHandler;
|
||||||
|
use App\Framework\Console\Components\TuiCommandExecutor;
|
||||||
|
use App\Framework\Console\Components\TuiRenderer;
|
||||||
|
use App\Framework\Console\CommandHistory;
|
||||||
|
use App\Framework\Console\CommandGroupRegistry;
|
||||||
|
use App\Framework\Console\SimpleWorkflowExecutor;
|
||||||
|
use App\Framework\Console\ConsoleOutput;
|
||||||
|
use App\Framework\Console\Screen\ScreenManager;
|
||||||
|
use App\Framework\Discovery\Results\DiscoveryRegistry;
|
||||||
|
use App\Framework\DI\Container;
|
||||||
|
|
||||||
|
echo "Debug Real TUI Execution\n";
|
||||||
|
echo "========================\n\n";
|
||||||
|
|
||||||
|
// Check TTY first
|
||||||
|
if (!posix_isatty(STDIN)) {
|
||||||
|
echo "❌ This test requires a TTY terminal.\n";
|
||||||
|
echo "Run with: docker exec -it php php debug_real_tui.php\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "✓ TTY detected\n";
|
||||||
|
|
||||||
|
// Check stty
|
||||||
|
if (!function_exists('shell_exec') || shell_exec('which stty') === null) {
|
||||||
|
echo "❌ stty not available\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "✓ stty available\n\n";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create minimal dependencies
|
||||||
|
$container = new \App\Framework\DI\DefaultContainer();
|
||||||
|
|
||||||
|
// Create a minimal discovery registry
|
||||||
|
$discoveryRegistry = new class implements DiscoveryRegistry {
|
||||||
|
public function interfaces() {
|
||||||
|
return new class {
|
||||||
|
public function get(string $interface) { return []; }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
public function attributes() {
|
||||||
|
return new class {
|
||||||
|
public function get(string $attributeClass) { return []; }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create mock command executor
|
||||||
|
$mockExecutor = new class implements TuiCommandExecutor {
|
||||||
|
public function executeSelectedCommand(object $command): void {
|
||||||
|
echo "\n🚀 Mock: Execute command\n";
|
||||||
|
}
|
||||||
|
public function executeCommand(string $commandName): void {
|
||||||
|
echo "\n🚀 Mock: Execute command: $commandName\n";
|
||||||
|
}
|
||||||
|
public function validateSelectedCommand(object $command): void {
|
||||||
|
echo "\n✓ Mock: Validate command\n";
|
||||||
|
}
|
||||||
|
public function validateCommand(string $commandName): void {
|
||||||
|
echo "\n✓ Mock: Validate command: $commandName\n";
|
||||||
|
}
|
||||||
|
public function showSelectedCommandHelp(object $command): void {
|
||||||
|
echo "\n📚 Mock: Show help\n";
|
||||||
|
}
|
||||||
|
public function showCommandHelp(string $commandName): void {
|
||||||
|
echo "\n📚 Mock: Show help: $commandName\n";
|
||||||
|
}
|
||||||
|
public function showAllCommandsHelp(): void {
|
||||||
|
echo "\n📚 Mock: Show all commands help\n";
|
||||||
|
}
|
||||||
|
public function startInteractiveForm(object $command, \App\Framework\Console\Components\TuiState $state): void {
|
||||||
|
echo "\n📝 Mock: Start form\n";
|
||||||
|
}
|
||||||
|
public function findCommandObject(string $commandName): ?object {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create workflow executor
|
||||||
|
$workflowExecutor = new class implements SimpleWorkflowExecutor {
|
||||||
|
public function executeWorkflow(array $workflow, \App\Framework\Console\Components\TuiState $state): void {
|
||||||
|
echo "\n🔄 Mock: Execute workflow\n";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create components
|
||||||
|
$output = new ConsoleOutput();
|
||||||
|
$output->screen = new ScreenManager($output);
|
||||||
|
|
||||||
|
$state = new TuiState();
|
||||||
|
$history = new CommandHistory();
|
||||||
|
$inputHandler = new TuiInputHandler($mockExecutor);
|
||||||
|
$renderer = new TuiRenderer($output);
|
||||||
|
$groupRegistry = new CommandGroupRegistry($discoveryRegistry);
|
||||||
|
|
||||||
|
echo "✓ Components created\n";
|
||||||
|
|
||||||
|
// Test categories loading
|
||||||
|
$categories = $groupRegistry->getOrganizedCommands();
|
||||||
|
echo "✓ Categories loaded: " . count($categories) . "\n";
|
||||||
|
|
||||||
|
if (empty($categories)) {
|
||||||
|
// Add test categories since discovery is empty
|
||||||
|
$testCategories = [
|
||||||
|
[
|
||||||
|
'name' => 'Testing',
|
||||||
|
'description' => 'Test commands',
|
||||||
|
'icon' => '🧪',
|
||||||
|
'priority' => 0,
|
||||||
|
'commands' => []
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Demo',
|
||||||
|
'description' => 'Demo commands',
|
||||||
|
'icon' => '🎮',
|
||||||
|
'priority' => 0,
|
||||||
|
'commands' => []
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'General',
|
||||||
|
'description' => 'General commands',
|
||||||
|
'icon' => '📂',
|
||||||
|
'priority' => 0,
|
||||||
|
'commands' => []
|
||||||
|
]
|
||||||
|
];
|
||||||
|
$state->setCategories($testCategories);
|
||||||
|
echo "✓ Test categories added: " . count($testCategories) . "\n";
|
||||||
|
} else {
|
||||||
|
$state->setCategories($categories);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create TUI
|
||||||
|
$tui = new ConsoleTUI(
|
||||||
|
$output,
|
||||||
|
$container,
|
||||||
|
$discoveryRegistry,
|
||||||
|
$state,
|
||||||
|
$renderer,
|
||||||
|
$inputHandler,
|
||||||
|
$mockExecutor,
|
||||||
|
$history,
|
||||||
|
$groupRegistry,
|
||||||
|
$workflowExecutor
|
||||||
|
);
|
||||||
|
|
||||||
|
echo "✓ TUI created\n\n";
|
||||||
|
|
||||||
|
echo "🚀 Starting TUI...\n";
|
||||||
|
echo "Use arrow keys to navigate, 'q' to quit.\n";
|
||||||
|
echo "This will show if the TUI actually responds to input.\n\n";
|
||||||
|
|
||||||
|
// Start TUI
|
||||||
|
$exitCode = $tui->run();
|
||||||
|
|
||||||
|
echo "\n✓ TUI exited with code: " . $exitCode->value . "\n";
|
||||||
|
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
echo "\n❌ ERROR: " . $e->getMessage() . "\n";
|
||||||
|
echo "File: " . $e->getFile() . ":" . $e->getLine() . "\n";
|
||||||
|
echo "\nStack trace:\n" . $e->getTraceAsString() . "\n";
|
||||||
|
}
|
||||||
56
scripts/debug/debug_routes.php
Normal file
56
scripts/debug/debug_routes.php
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require '/var/www/html/vendor/autoload.php';
|
||||||
|
|
||||||
|
use App\Framework\Discovery\DiscoveryServiceBootstrapper;
|
||||||
|
use App\Framework\DI\DefaultContainer;
|
||||||
|
use App\Framework\Attributes\Route;
|
||||||
|
|
||||||
|
try {
|
||||||
|
echo "=== Route Discovery Diagnostic ===\n";
|
||||||
|
|
||||||
|
$container = new DefaultContainer();
|
||||||
|
$bootStrapper = new DiscoveryServiceBootstrapper();
|
||||||
|
$bootStrapper->initialize($container);
|
||||||
|
|
||||||
|
$registry = $container->get('App\Framework\Discovery\Results\DiscoveryRegistry');
|
||||||
|
$routes = $registry->attributes->get(Route::class);
|
||||||
|
|
||||||
|
echo "Total routes found: " . count($routes) . "\n\n";
|
||||||
|
|
||||||
|
$adminRoutes = [];
|
||||||
|
foreach ($routes as $route) {
|
||||||
|
$path = $route->additionalData['path'] ?? '';
|
||||||
|
if (str_contains($path, 'admin')) {
|
||||||
|
$adminRoutes[] = [
|
||||||
|
'path' => $path,
|
||||||
|
'controller' => $route->className->getFullyQualified(),
|
||||||
|
'method' => $route->methodName?->toString() ?? 'unknown'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Admin routes found:\n";
|
||||||
|
foreach ($adminRoutes as $route) {
|
||||||
|
echo " Path: {$route['path']}\n";
|
||||||
|
echo " Controller: {$route['controller']}\n";
|
||||||
|
echo " Method: {$route['method']}\n";
|
||||||
|
echo " ---\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check specifically for ShowRoutes
|
||||||
|
echo "\nLooking for ShowRoutes controller:\n";
|
||||||
|
foreach ($routes as $route) {
|
||||||
|
if (str_contains($route->className->getFullyQualified(), 'ShowRoutes')) {
|
||||||
|
echo "Found ShowRoutes route:\n";
|
||||||
|
echo " Path: " . ($route->additionalData['path'] ?? 'unknown') . "\n";
|
||||||
|
echo " Controller: " . $route->className->getFullyQualified() . "\n";
|
||||||
|
echo " Method: " . ($route->methodName?->toString() ?? 'unknown') . "\n";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "Error: " . $e->getMessage() . "\n";
|
||||||
|
echo "Stack trace: " . $e->getTraceAsString() . "\n";
|
||||||
|
}
|
||||||
34
scripts/debug/debug_template_renderer.php
Normal file
34
scripts/debug/debug_template_renderer.php
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
use App\Framework\Core\Application;
|
||||||
|
use App\Framework\View\TemplateRenderer;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$app = new Application();
|
||||||
|
$container = $app->getContainer();
|
||||||
|
|
||||||
|
$templateRenderer = $container->get(TemplateRenderer::class);
|
||||||
|
|
||||||
|
echo "TemplateRenderer class: " . get_class($templateRenderer) . "\n";
|
||||||
|
echo "TemplateRenderer instance of TemplateRenderer: " . ($templateRenderer instanceof TemplateRenderer ? 'YES' : 'NO') . "\n";
|
||||||
|
|
||||||
|
if (method_exists($templateRenderer, 'render')) {
|
||||||
|
echo "Has render method: YES\n";
|
||||||
|
} else {
|
||||||
|
echo "Has render method: NO\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's the Engine class we expect
|
||||||
|
if (get_class($templateRenderer) === 'App\Framework\View\Engine') {
|
||||||
|
echo "Is Engine class: YES\n";
|
||||||
|
} else {
|
||||||
|
echo "Is Engine class: NO\n";
|
||||||
|
echo "Actual class: " . get_class($templateRenderer) . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "Error: " . $e->getMessage() . "\n";
|
||||||
|
echo "Trace: " . $e->getTraceAsString() . "\n";
|
||||||
|
}
|
||||||
219
scripts/debug/debug_tui_navigation_logic.php
Normal file
219
scripts/debug/debug_tui_navigation_logic.php
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
// Debug TUI navigation logic specifically
|
||||||
|
use App\Framework\Console\Components\TuiState;
|
||||||
|
use App\Framework\Console\Components\TuiInputHandler;
|
||||||
|
use App\Framework\Console\Components\TuiCommandExecutor;
|
||||||
|
use App\Framework\Console\CommandHistory;
|
||||||
|
use App\Framework\Console\CommandGroupRegistry;
|
||||||
|
use App\Framework\Console\TuiView;
|
||||||
|
use App\Framework\Console\TuiKeyCode;
|
||||||
|
use App\Framework\Discovery\Results\DiscoveryRegistry;
|
||||||
|
|
||||||
|
echo "Debug TUI Navigation Logic\n";
|
||||||
|
echo "==========================\n\n";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create discovery registry that returns real-looking data
|
||||||
|
$discoveryRegistry = new class implements DiscoveryRegistry {
|
||||||
|
public function interfaces() {
|
||||||
|
return new class {
|
||||||
|
public function get(string $interface) { return []; }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
public function attributes() {
|
||||||
|
return new class {
|
||||||
|
public function get(string $attributeClass) { return []; }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create mock executor with debug output
|
||||||
|
$mockExecutor = new class implements TuiCommandExecutor {
|
||||||
|
public function executeSelectedCommand(object $command): void {
|
||||||
|
echo "\n🚀 Mock: Execute command\n";
|
||||||
|
}
|
||||||
|
public function executeCommand(string $commandName): void {
|
||||||
|
echo "\n🚀 Mock: Execute command: $commandName\n";
|
||||||
|
}
|
||||||
|
public function validateSelectedCommand(object $command): void {
|
||||||
|
echo "\n✓ Mock: Validate command\n";
|
||||||
|
}
|
||||||
|
public function validateCommand(string $commandName): void {
|
||||||
|
echo "\n✓ Mock: Validate command: $commandName\n";
|
||||||
|
}
|
||||||
|
public function showSelectedCommandHelp(object $command): void {
|
||||||
|
echo "\n📚 Mock: Show help\n";
|
||||||
|
}
|
||||||
|
public function showCommandHelp(string $commandName): void {
|
||||||
|
echo "\n📚 Mock: Show help: $commandName\n";
|
||||||
|
}
|
||||||
|
public function showAllCommandsHelp(): void {
|
||||||
|
echo "\n📚 Mock: Show all commands help\n";
|
||||||
|
}
|
||||||
|
public function startInteractiveForm(object $command, \App\Framework\Console\Components\TuiState $state): void {
|
||||||
|
echo "\n📝 Mock: Start form\n";
|
||||||
|
}
|
||||||
|
public function findCommandObject(string $commandName): ?object {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create components
|
||||||
|
$state = new TuiState();
|
||||||
|
$history = new CommandHistory();
|
||||||
|
$inputHandler = new TuiInputHandler($mockExecutor);
|
||||||
|
$groupRegistry = new CommandGroupRegistry($discoveryRegistry);
|
||||||
|
|
||||||
|
echo "✓ Components created\n";
|
||||||
|
|
||||||
|
// Since discovery is empty, let's manually create categories like the real TUI
|
||||||
|
$testCategories = [
|
||||||
|
[
|
||||||
|
'name' => 'Testing',
|
||||||
|
'description' => 'Test commands',
|
||||||
|
'icon' => '🧪',
|
||||||
|
'priority' => 0,
|
||||||
|
'commands' => []
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Demo',
|
||||||
|
'description' => 'Demo commands',
|
||||||
|
'icon' => '🎮',
|
||||||
|
'priority' => 0,
|
||||||
|
'commands' => []
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Generator',
|
||||||
|
'description' => 'Code generation',
|
||||||
|
'icon' => '⚙️',
|
||||||
|
'priority' => 0,
|
||||||
|
'commands' => []
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'General',
|
||||||
|
'description' => 'General commands',
|
||||||
|
'icon' => '📂',
|
||||||
|
'priority' => 0,
|
||||||
|
'commands' => []
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
echo "📊 Test Categories:\n";
|
||||||
|
foreach ($testCategories as $index => $category) {
|
||||||
|
echo " [$index] {$category['icon']} {$category['name']} - {$category['description']}\n";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
// Setup TUI state
|
||||||
|
$state->setCategories($testCategories);
|
||||||
|
$state->setCurrentView(TuiView::CATEGORIES);
|
||||||
|
$state->setRunning(true);
|
||||||
|
|
||||||
|
echo "✓ TUI State initialized:\n";
|
||||||
|
echo " Categories: " . count($testCategories) . "\n";
|
||||||
|
echo " Current View: " . $state->getCurrentView()->name . "\n";
|
||||||
|
echo " Selected Category: " . $state->getSelectedCategory() . "\n";
|
||||||
|
|
||||||
|
$currentCategory = $state->getCurrentCategory();
|
||||||
|
if ($currentCategory) {
|
||||||
|
echo " Current Category: '{$currentCategory['name']}'\n";
|
||||||
|
} else {
|
||||||
|
echo " ❌ ERROR: getCurrentCategory() returned NULL!\n";
|
||||||
|
echo " This would cause navigation to fail!\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n=== STEP-BY-STEP NAVIGATION DEBUG ===\n\n";
|
||||||
|
|
||||||
|
function debugState($state, $step) {
|
||||||
|
echo "Step $step State:\n";
|
||||||
|
echo " - Selected Index: " . $state->getSelectedCategory() . "\n";
|
||||||
|
echo " - Current View: " . $state->getCurrentView()->name . "\n";
|
||||||
|
|
||||||
|
$category = $state->getCurrentCategory();
|
||||||
|
if ($category) {
|
||||||
|
echo " - Current Category: '{$category['name']}'\n";
|
||||||
|
} else {
|
||||||
|
echo " - ❌ Current Category: NULL\n";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial state
|
||||||
|
debugState($state, "Initial");
|
||||||
|
|
||||||
|
// Test 1: Direct TuiState navigation
|
||||||
|
echo "🔍 Test 1: Direct TuiState navigation\n";
|
||||||
|
echo "Calling \$state->navigateDown()...\n";
|
||||||
|
$state->navigateDown();
|
||||||
|
debugState($state, "After navigateDown()");
|
||||||
|
|
||||||
|
echo "Calling \$state->navigateUp()...\n";
|
||||||
|
$state->navigateUp();
|
||||||
|
debugState($state, "After navigateUp()");
|
||||||
|
|
||||||
|
// Test 2: TuiInputHandler navigation
|
||||||
|
echo "🔍 Test 2: TuiInputHandler with Arrow Keys\n";
|
||||||
|
|
||||||
|
echo "Simulating ARROW_DOWN input...\n";
|
||||||
|
echo "Key code: '" . TuiKeyCode::ARROW_DOWN->value . "' (hex: " . bin2hex(TuiKeyCode::ARROW_DOWN->value) . ")\n";
|
||||||
|
|
||||||
|
$beforeIndex = $state->getSelectedCategory();
|
||||||
|
$inputHandler->handleInput(TuiKeyCode::ARROW_DOWN->value, $state, $history);
|
||||||
|
$afterIndex = $state->getSelectedCategory();
|
||||||
|
|
||||||
|
echo "Before: $beforeIndex, After: $afterIndex\n";
|
||||||
|
echo "Changed: " . ($beforeIndex !== $afterIndex ? "YES ✓" : "NO ❌") . "\n";
|
||||||
|
debugState($state, "After ARROW_DOWN");
|
||||||
|
|
||||||
|
echo "Simulating ARROW_UP input...\n";
|
||||||
|
echo "Key code: '" . TuiKeyCode::ARROW_UP->value . "' (hex: " . bin2hex(TuiKeyCode::ARROW_UP->value) . ")\n";
|
||||||
|
|
||||||
|
$beforeIndex = $state->getSelectedCategory();
|
||||||
|
$inputHandler->handleInput(TuiKeyCode::ARROW_UP->value, $state, $history);
|
||||||
|
$afterIndex = $state->getSelectedCategory();
|
||||||
|
|
||||||
|
echo "Before: $beforeIndex, After: $afterIndex\n";
|
||||||
|
echo "Changed: " . ($beforeIndex !== $afterIndex ? "YES ✓" : "NO ❌") . "\n";
|
||||||
|
debugState($state, "After ARROW_UP");
|
||||||
|
|
||||||
|
// Test 3: Check bounds
|
||||||
|
echo "🔍 Test 3: Boundary testing\n";
|
||||||
|
|
||||||
|
// Go to last category
|
||||||
|
$lastIndex = count($testCategories) - 1;
|
||||||
|
$state->setSelectedCategory($lastIndex);
|
||||||
|
echo "Set to last category ($lastIndex)\n";
|
||||||
|
debugState($state, "Set to last");
|
||||||
|
|
||||||
|
echo "Try to go beyond last (ARROW_DOWN)...\n";
|
||||||
|
$beforeIndex = $state->getSelectedCategory();
|
||||||
|
$inputHandler->handleInput(TuiKeyCode::ARROW_DOWN->value, $state, $history);
|
||||||
|
$afterIndex = $state->getSelectedCategory();
|
||||||
|
|
||||||
|
echo "Before: $beforeIndex, After: $afterIndex\n";
|
||||||
|
echo "Boundary protected: " . ($beforeIndex === $afterIndex ? "YES ✓" : "NO ❌") . "\n";
|
||||||
|
|
||||||
|
// Test 4: Check TuiInputHandler logic
|
||||||
|
echo "\n🔍 Test 4: Debug TuiInputHandler logic\n";
|
||||||
|
|
||||||
|
// Let's trace what happens in handleInput
|
||||||
|
echo "Current view check: " . $state->getCurrentView()->name . "\n";
|
||||||
|
echo "Is CATEGORIES view: " . ($state->getCurrentView() === TuiView::CATEGORIES ? "YES" : "NO") . "\n";
|
||||||
|
|
||||||
|
if ($state->getCurrentView() === TuiView::CATEGORIES) {
|
||||||
|
echo "✓ View is correct for category navigation\n";
|
||||||
|
} else {
|
||||||
|
echo "❌ View is not CATEGORIES - navigation won't work!\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n✅ NAVIGATION DEBUG COMPLETED\n";
|
||||||
|
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
echo "\n❌ DEBUG FAILED:\n";
|
||||||
|
echo "Error: " . $e->getMessage() . "\n";
|
||||||
|
echo "File: " . $e->getFile() . ":" . $e->getLine() . "\n";
|
||||||
|
echo "\nStack trace:\n" . $e->getTraceAsString() . "\n";
|
||||||
|
}
|
||||||
57
scripts/debug/framework-components.php
Normal file
57
scripts/debug/framework-components.php
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
// Enable error reporting
|
||||||
|
ini_set('display_errors', '1');
|
||||||
|
ini_set('log_errors', '1');
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
echo "🧪 Framework Debug Check\n";
|
||||||
|
echo "======================\n\n";
|
||||||
|
|
||||||
|
try {
|
||||||
|
echo "1. ✅ Basic PHP OK (Version: " . PHP_VERSION . ")\n";
|
||||||
|
|
||||||
|
echo "2. Testing Autoloader...\n";
|
||||||
|
require __DIR__ . '/../vendor/autoload.php';
|
||||||
|
echo " ✅ Autoloader loaded\n";
|
||||||
|
|
||||||
|
echo "3. Testing Basic Classes...\n";
|
||||||
|
$testClasses = [
|
||||||
|
'App\\Framework\\Core\\Application',
|
||||||
|
'App\\Framework\\Core\\AppBootstrapper',
|
||||||
|
'App\\Framework\\Http\\HttpRequest',
|
||||||
|
'App\\Framework\\Http\\Method',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($testClasses as $class) {
|
||||||
|
if (class_exists($class)) {
|
||||||
|
echo " ✅ $class: EXISTS\n";
|
||||||
|
} else {
|
||||||
|
echo " ❌ $class: MISSING\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "4. Testing DateTime Components...\n";
|
||||||
|
$clock = new App\Framework\DateTime\SystemClock();
|
||||||
|
echo " ✅ SystemClock: " . $clock->now()->format('Y-m-d H:i:s') . "\n";
|
||||||
|
|
||||||
|
echo "5. Testing Memory Monitor...\n";
|
||||||
|
$memoryMonitor = new App\Framework\Performance\MemoryMonitor();
|
||||||
|
echo " ✅ MemoryMonitor: Current usage " . number_format(memory_get_usage(true) / 1024 / 1024, 2) . " MB\n";
|
||||||
|
|
||||||
|
echo "\n🎉 Basic framework components working!\n";
|
||||||
|
echo "The issue is likely in the Discovery System during bootstrap.\n";
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "\n❌ ERROR: " . $e->getMessage() . "\n";
|
||||||
|
echo "Class: " . get_class($e) . "\n";
|
||||||
|
echo "File: " . $e->getFile() . ":" . $e->getLine() . "\n";
|
||||||
|
echo "\nStack Trace:\n" . $e->getTraceAsString() . "\n";
|
||||||
|
} catch (Error $e) {
|
||||||
|
echo "\n❌ FATAL ERROR: " . $e->getMessage() . "\n";
|
||||||
|
echo "Class: " . get_class($e) . "\n";
|
||||||
|
echo "File: " . $e->getFile() . ":" . $e->getLine() . "\n";
|
||||||
|
echo "\nStack Trace:\n" . $e->getTraceAsString() . "\n";
|
||||||
|
}
|
||||||
53
scripts/debug/minimal-bootstrap.php
Normal file
53
scripts/debug/minimal-bootstrap.php
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimal framework test without discovery system
|
||||||
|
*/
|
||||||
|
|
||||||
|
ini_set('display_errors', '1');
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
require __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
echo "🌐 Minimal Framework Test\n";
|
||||||
|
echo "========================\n\n";
|
||||||
|
|
||||||
|
// Test basic framework components without discovery
|
||||||
|
$pathProvider = new App\Framework\Core\PathProvider(__DIR__ . '/..');
|
||||||
|
$clock = new App\Framework\DateTime\SystemClock();
|
||||||
|
$memoryMonitor = new App\Framework\Performance\MemoryMonitor();
|
||||||
|
|
||||||
|
echo "✅ PathProvider: " . $pathProvider->getBasePath() . "\n";
|
||||||
|
echo "✅ Clock: " . $clock->now()->format('Y-m-d H:i:s') . "\n";
|
||||||
|
echo "✅ Memory: " . number_format(memory_get_usage(true) / 1024 / 1024, 2) . " MB\n";
|
||||||
|
|
||||||
|
// Test HTTP Request parsing
|
||||||
|
$method = App\Framework\Http\Method::GET;
|
||||||
|
echo "✅ HTTP Method: " . $method->value . "\n";
|
||||||
|
|
||||||
|
// Simple HTTP response
|
||||||
|
header('Content-Type: text/html; charset=utf-8');
|
||||||
|
|
||||||
|
echo "\n📦 Framework Status:\n";
|
||||||
|
echo " • Basic components: ✅ Working\n";
|
||||||
|
echo " • Discovery system: 🚧 Needs optimization\n";
|
||||||
|
echo " • Performance: ⚡ {$memoryMonitor->getPeakMemoryUsageMb()}MB peak\n";
|
||||||
|
|
||||||
|
echo "\n🎯 Next Steps:\n";
|
||||||
|
echo " 1. Optimize discovery system for large codebase\n";
|
||||||
|
echo " 2. Implement lazy loading for Framework components\n";
|
||||||
|
echo " 3. Add performance monitoring\n";
|
||||||
|
echo " 4. Test with production cache settings\n";
|
||||||
|
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
echo "❌ ERROR: " . $e->getMessage() . "\n";
|
||||||
|
echo " File: " . $e->getFile() . ":" . $e->getLine() . "\n";
|
||||||
|
echo " Class: " . get_class($e) . "\n";
|
||||||
|
|
||||||
|
if (method_exists($e, 'getContext')) {
|
||||||
|
echo " Context: " . json_encode($e->getContext()) . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
253
scripts/debug/simple_debug_tui.php
Normal file
253
scripts/debug/simple_debug_tui.php
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
// Simple debug TUI without complex dependencies
|
||||||
|
use App\Framework\Console\Components\TuiState;
|
||||||
|
use App\Framework\Console\Components\TuiInputHandler;
|
||||||
|
use App\Framework\Console\Components\TuiCommandExecutor;
|
||||||
|
use App\Framework\Console\CommandHistory;
|
||||||
|
use App\Framework\Console\TuiView;
|
||||||
|
use App\Framework\Console\TuiKeyCode;
|
||||||
|
use App\Framework\Console\ConsoleColor;
|
||||||
|
use App\Framework\Console\Screen\ScreenControlCode;
|
||||||
|
use App\Framework\Console\Screen\CursorControlCode;
|
||||||
|
|
||||||
|
echo "Simple TUI Debug Session\n";
|
||||||
|
echo "========================\n\n";
|
||||||
|
|
||||||
|
// Check TTY
|
||||||
|
if (!posix_isatty(STDIN)) {
|
||||||
|
echo "❌ Requires TTY. Run with: docker exec -it php php simple_debug_tui.php\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "✓ TTY available\n";
|
||||||
|
|
||||||
|
// Check PHPStorm
|
||||||
|
$isPhpStorm = getenv('TERMINAL_EMULATOR') === 'JetBrains-JediTerm';
|
||||||
|
echo "✓ PHPStorm detected: " . ($isPhpStorm ? "YES" : "NO") . "\n\n";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Debug command executor
|
||||||
|
$debugExecutor = new class implements TuiCommandExecutor {
|
||||||
|
public function executeSelectedCommand(object $command): void {
|
||||||
|
echo "\n🚀 EXECUTE: Selected command\n";
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
|
public function executeCommand(string $commandName): void {
|
||||||
|
echo "\n🚀 EXECUTE: $commandName\n";
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
|
public function validateSelectedCommand(object $command): void {
|
||||||
|
echo "\n✓ VALIDATE: Selected command\n";
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
|
public function validateCommand(string $commandName): void {
|
||||||
|
echo "\n✓ VALIDATE: $commandName\n";
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
|
public function showSelectedCommandHelp(object $command): void {
|
||||||
|
echo "\n📚 HELP: Selected command\n";
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
|
public function showCommandHelp(string $commandName): void {
|
||||||
|
echo "\n📚 HELP: $commandName\n";
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
|
public function showAllCommandsHelp(): void {
|
||||||
|
echo "\n📚 HELP: All commands\n";
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
|
public function startInteractiveForm(object $command, \App\Framework\Console\Components\TuiState $state): void {
|
||||||
|
echo "\n📝 FORM: Start interactive form\n";
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
|
public function findCommandObject(string $commandName): ?object {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Custom TUI state with debug
|
||||||
|
$debugState = new class extends TuiState {
|
||||||
|
public function setSelectedCategory(int $index): void {
|
||||||
|
$oldIndex = $this->getSelectedCategory();
|
||||||
|
parent::setSelectedCategory($index);
|
||||||
|
$newIndex = $this->getSelectedCategory();
|
||||||
|
echo "\n[DEBUG] Category index: $oldIndex → $newIndex\n";
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function navigateUp(): void {
|
||||||
|
echo "\n[DEBUG] ⬆️ navigateUp() called\n";
|
||||||
|
flush();
|
||||||
|
$before = $this->getSelectedCategory();
|
||||||
|
parent::navigateUp();
|
||||||
|
$after = $this->getSelectedCategory();
|
||||||
|
echo "[DEBUG] ⬆️ Result: $before → $after\n";
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function navigateDown(): void {
|
||||||
|
echo "\n[DEBUG] ⬇️ navigateDown() called\n";
|
||||||
|
flush();
|
||||||
|
$before = $this->getSelectedCategory();
|
||||||
|
parent::navigateDown();
|
||||||
|
$after = $this->getSelectedCategory();
|
||||||
|
echo "[DEBUG] ⬇️ Result: $before → $after\n";
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Custom input handler with debug
|
||||||
|
$debugInputHandler = new class($debugExecutor) extends TuiInputHandler {
|
||||||
|
public function handleInput(string $key, \App\Framework\Console\Components\TuiState $state, \App\Framework\Console\CommandHistory $history): void {
|
||||||
|
$keyHex = bin2hex($key);
|
||||||
|
$keyDesc = match($key) {
|
||||||
|
"\033[A" => "ARROW_UP",
|
||||||
|
"\033[B" => "ARROW_DOWN",
|
||||||
|
"\033[C" => "ARROW_RIGHT",
|
||||||
|
"\033[D" => "ARROW_LEFT",
|
||||||
|
"\n" => "ENTER",
|
||||||
|
" " => "SPACE",
|
||||||
|
"\033" => "ESC",
|
||||||
|
'q', 'Q' => "QUIT",
|
||||||
|
default => "OTHER('$key')"
|
||||||
|
};
|
||||||
|
|
||||||
|
echo "\n[INPUT] 🎯 Key: $keyDesc (hex: $keyHex)\n";
|
||||||
|
echo "[INPUT] 📍 Current view: " . $state->getCurrentView()->name . "\n";
|
||||||
|
echo "[INPUT] 📍 Current category: " . $state->getSelectedCategory() . "\n";
|
||||||
|
flush();
|
||||||
|
|
||||||
|
parent::handleInput($key, $state, $history);
|
||||||
|
|
||||||
|
echo "[INPUT] ✅ After processing - Category: " . $state->getSelectedCategory() . "\n";
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$state = $debugState;
|
||||||
|
$history = new CommandHistory();
|
||||||
|
$inputHandler = $debugInputHandler;
|
||||||
|
|
||||||
|
// Add test categories
|
||||||
|
$testCategories = [
|
||||||
|
['name' => 'Testing', 'description' => 'Test commands', 'icon' => '🧪', 'commands' => []],
|
||||||
|
['name' => 'Demo', 'description' => 'Demo commands', 'icon' => '🎮', 'commands' => []],
|
||||||
|
['name' => 'Generator', 'description' => 'Code generation', 'icon' => '⚙️', 'commands' => []],
|
||||||
|
['name' => 'General', 'description' => 'General commands', 'icon' => '📂', 'commands' => []]
|
||||||
|
];
|
||||||
|
|
||||||
|
$state->setCategories($testCategories);
|
||||||
|
$state->setCurrentView(TuiView::CATEGORIES);
|
||||||
|
$state->setRunning(true);
|
||||||
|
|
||||||
|
echo "✓ Debug TUI ready\n";
|
||||||
|
echo "Categories: " . count($testCategories) . "\n";
|
||||||
|
echo "Selected: " . $state->getSelectedCategory() . "\n\n";
|
||||||
|
|
||||||
|
// Save terminal settings
|
||||||
|
$originalSettings = trim(shell_exec('stty -g') ?: '');
|
||||||
|
|
||||||
|
echo "Setting up terminal for PHPStorm...\n";
|
||||||
|
|
||||||
|
// Set up terminal
|
||||||
|
if ($isPhpStorm) {
|
||||||
|
shell_exec('stty raw -echo min 1 time 0 2>/dev/null');
|
||||||
|
} else {
|
||||||
|
shell_exec('stty -icanon -echo 2>/dev/null');
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "✓ Terminal configured\n\n";
|
||||||
|
|
||||||
|
echo "🚀 SIMPLE DEBUG TUI\n";
|
||||||
|
echo "==================\n\n";
|
||||||
|
|
||||||
|
function renderSimpleMenu($categories, $selectedIndex) {
|
||||||
|
echo "Categories:\n";
|
||||||
|
foreach ($categories as $index => $category) {
|
||||||
|
$indicator = $index === $selectedIndex ? '▶️' : ' ';
|
||||||
|
echo "$indicator {$category['icon']} {$category['name']}\n";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
function readKeyPHPStorm() {
|
||||||
|
$key = fgetc(STDIN);
|
||||||
|
if ($key === false) return '';
|
||||||
|
|
||||||
|
if ($key === "\033") {
|
||||||
|
$sequence = $key;
|
||||||
|
stream_set_blocking(STDIN, false);
|
||||||
|
|
||||||
|
$next = fgetc(STDIN);
|
||||||
|
if ($next === false) {
|
||||||
|
usleep(10000);
|
||||||
|
$next = fgetc(STDIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($next !== false) {
|
||||||
|
$sequence .= $next;
|
||||||
|
if ($next === '[') {
|
||||||
|
$third = fgetc(STDIN);
|
||||||
|
if ($third === false) {
|
||||||
|
usleep(10000);
|
||||||
|
$third = fgetc(STDIN);
|
||||||
|
}
|
||||||
|
if ($third !== false) {
|
||||||
|
$sequence .= $third;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stream_set_blocking(STDIN, true);
|
||||||
|
return $sequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple debug loop
|
||||||
|
echo "Use ↑/↓ arrow keys to navigate. Press 'q' to quit.\n\n";
|
||||||
|
|
||||||
|
while ($state->isRunning()) {
|
||||||
|
// Render current state
|
||||||
|
renderSimpleMenu($testCategories, $state->getSelectedCategory());
|
||||||
|
|
||||||
|
echo "Debug Info:\n";
|
||||||
|
echo "- Selected Index: " . $state->getSelectedCategory() . "\n";
|
||||||
|
echo "- Current View: " . $state->getCurrentView()->name . "\n";
|
||||||
|
$category = $state->getCurrentCategory();
|
||||||
|
echo "- Category Name: " . ($category ? $category['name'] : 'NULL') . "\n";
|
||||||
|
echo "\nPress arrow keys...\n";
|
||||||
|
echo str_repeat('=', 40) . "\n";
|
||||||
|
|
||||||
|
// Read input
|
||||||
|
$key = readKeyPHPStorm();
|
||||||
|
|
||||||
|
if ($key === 'q' || $key === 'Q') {
|
||||||
|
echo "\n[QUIT] Stopping TUI...\n";
|
||||||
|
$state->setRunning(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($key !== '') {
|
||||||
|
// Clear screen for next render
|
||||||
|
echo ScreenControlCode::CLEAR_ALL->format();
|
||||||
|
echo CursorControlCode::POSITION->format(1, 1);
|
||||||
|
|
||||||
|
// Process input with full debug output
|
||||||
|
$inputHandler->handleInput($key, $state, $history);
|
||||||
|
|
||||||
|
echo "\n" . str_repeat('-', 40) . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
// Restore terminal
|
||||||
|
if (!empty($originalSettings)) {
|
||||||
|
shell_exec("stty $originalSettings 2>/dev/null");
|
||||||
|
}
|
||||||
|
echo "\n✅ Simple Debug TUI ended\n";
|
||||||
|
}
|
||||||
153
scripts/dev/hot-reload-minimal.php
Normal file
153
scripts/dev/hot-reload-minimal.php
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimal Hot Reload Endpoint
|
||||||
|
* Simple SSE implementation without complex framework dependencies
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
// Disable deprecation warnings
|
||||||
|
error_reporting(E_ALL & ~E_DEPRECATED);
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
|
||||||
|
// Simple .env parsing
|
||||||
|
function loadEnvVar(string $key, string $envFile = null): ?string {
|
||||||
|
$envFile ??= __DIR__ . '/../.env';
|
||||||
|
if (!file_exists($envFile)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = file_get_contents($envFile);
|
||||||
|
if (preg_match("/^{$key}\s*=\s*(.*)$/m", $content, $matches)) {
|
||||||
|
return trim($matches[1], " \t\n\r\0\x0B\"'");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only allow in development
|
||||||
|
$appDebug = loadEnvVar('APP_DEBUG');
|
||||||
|
if ($appDebug !== 'true') {
|
||||||
|
http_response_code(403);
|
||||||
|
exit('Hot Reload is only available in development mode');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set SSE headers
|
||||||
|
header('Content-Type: text/event-stream');
|
||||||
|
header('Cache-Control: no-cache');
|
||||||
|
header('Connection: keep-alive');
|
||||||
|
header('X-Accel-Buffering: no'); // Disable nginx buffering
|
||||||
|
|
||||||
|
// Get watched directories
|
||||||
|
$watchedDirs = [
|
||||||
|
__DIR__ . '/../src',
|
||||||
|
__DIR__ . '/../resources/views',
|
||||||
|
__DIR__ . '/../config',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Store file modification times
|
||||||
|
$fileCache = [];
|
||||||
|
|
||||||
|
// Function to scan directories for PHP files
|
||||||
|
function scanDirectory(string $dir, array &$fileCache): array {
|
||||||
|
$changes = [];
|
||||||
|
|
||||||
|
if (!is_dir($dir)) {
|
||||||
|
return $changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
$iterator = new RecursiveIteratorIterator(
|
||||||
|
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
|
||||||
|
RecursiveIteratorIterator::SELF_FIRST
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($iterator as $file) {
|
||||||
|
if (!$file->isFile()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = $file->getPathname();
|
||||||
|
|
||||||
|
// Only watch specific file types
|
||||||
|
if (!preg_match('/\.(php|view\.php|css|js|ts)$/', $path)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mtime = $file->getMTime();
|
||||||
|
|
||||||
|
if (!isset($fileCache[$path])) {
|
||||||
|
$fileCache[$path] = $mtime;
|
||||||
|
} elseif ($fileCache[$path] < $mtime) {
|
||||||
|
$changes[] = [
|
||||||
|
'path' => $path,
|
||||||
|
'type' => 'modified',
|
||||||
|
'time' => $mtime
|
||||||
|
];
|
||||||
|
$fileCache[$path] = $mtime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send initial connection event
|
||||||
|
echo "event: connected\n";
|
||||||
|
echo "data: " . json_encode([
|
||||||
|
'status' => 'connected',
|
||||||
|
'timestamp' => date('c'),
|
||||||
|
'message' => 'Hot Reload server started'
|
||||||
|
]) . "\n\n";
|
||||||
|
flush();
|
||||||
|
|
||||||
|
// Initialize file cache
|
||||||
|
foreach ($watchedDirs as $dir) {
|
||||||
|
scanDirectory($dir, $fileCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
$lastHeartbeat = time();
|
||||||
|
|
||||||
|
// Keep connection alive and watch for changes
|
||||||
|
while (connection_aborted() === 0) {
|
||||||
|
$hasChanges = false;
|
||||||
|
|
||||||
|
// Check each watched directory
|
||||||
|
foreach ($watchedDirs as $dir) {
|
||||||
|
$changes = scanDirectory($dir, $fileCache);
|
||||||
|
|
||||||
|
foreach ($changes as $change) {
|
||||||
|
$reloadType = 'full';
|
||||||
|
if (str_ends_with($change['path'], '.css')) {
|
||||||
|
$reloadType = 'css';
|
||||||
|
} elseif (str_ends_with($change['path'], '.js') || str_ends_with($change['path'], '.ts')) {
|
||||||
|
$reloadType = 'hmr';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "event: reload\n";
|
||||||
|
echo "data: " . json_encode([
|
||||||
|
'type' => $reloadType,
|
||||||
|
'file' => basename($change['path']),
|
||||||
|
'path' => $change['path'],
|
||||||
|
'timestamp' => date('c'),
|
||||||
|
'message' => 'File changed: ' . basename($change['path'])
|
||||||
|
]) . "\n\n";
|
||||||
|
flush();
|
||||||
|
|
||||||
|
$hasChanges = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send heartbeat every 30 seconds
|
||||||
|
if (time() - $lastHeartbeat >= 30) {
|
||||||
|
echo "event: heartbeat\n";
|
||||||
|
echo "data: " . json_encode([
|
||||||
|
'timestamp' => date('c'),
|
||||||
|
'message' => 'Connection alive'
|
||||||
|
]) . "\n\n";
|
||||||
|
flush();
|
||||||
|
$lastHeartbeat = time();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small delay to prevent high CPU usage
|
||||||
|
usleep(500000); // 500ms
|
||||||
|
}
|
||||||
145
scripts/dev/hot-reload-server.php
Normal file
145
scripts/dev/hot-reload-server.php
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Development Hot Reload Endpoint
|
||||||
|
* Bypasses full framework bootstrap to avoid deprecation warnings
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
// Disable deprecation warnings for this development endpoint
|
||||||
|
error_reporting(E_ALL & ~E_DEPRECATED);
|
||||||
|
|
||||||
|
// Set error reporting for debugging
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
error_reporting(E_ALL & ~E_DEPRECATED);
|
||||||
|
|
||||||
|
// Set up minimal autoloading
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Minimal framework classes needed
|
||||||
|
use App\Framework\Filesystem\FilePath;
|
||||||
|
use App\Framework\Filesystem\FileWatcher;
|
||||||
|
use App\Framework\DateTime\SystemTimer;
|
||||||
|
|
||||||
|
// Simple .env parsing for development check
|
||||||
|
function loadEnvVar(string $key, string $envFile = null): ?string {
|
||||||
|
$envFile ??= __DIR__ . '/../.env';
|
||||||
|
if (!file_exists($envFile)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = file_get_contents($envFile);
|
||||||
|
if (preg_match("/^{$key}\s*=\s*(.*)$/m", $content, $matches)) {
|
||||||
|
return trim($matches[1], " \t\n\r\0\x0B\"'");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only allow in development
|
||||||
|
$appDebug = loadEnvVar('APP_DEBUG');
|
||||||
|
if ($appDebug !== 'true') {
|
||||||
|
http_response_code(403);
|
||||||
|
exit('Hot Reload is only available in development mode');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set SSE headers
|
||||||
|
header('Content-Type: text/event-stream');
|
||||||
|
header('Cache-Control: no-cache');
|
||||||
|
header('Connection: keep-alive');
|
||||||
|
header('X-Accel-Buffering: no'); // Disable nginx buffering
|
||||||
|
|
||||||
|
// Create file watcher
|
||||||
|
$fileWatcher = new FileWatcher(
|
||||||
|
FilePath::create(__DIR__ . '/..'),
|
||||||
|
new SystemTimer()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Watch patterns
|
||||||
|
$watchPatterns = [
|
||||||
|
'src/**/*.php',
|
||||||
|
'resources/views/**/*.php',
|
||||||
|
'resources/views/**/*.view.php',
|
||||||
|
'config/**/*.php',
|
||||||
|
];
|
||||||
|
|
||||||
|
$ignorePatterns = [
|
||||||
|
'vendor/**',
|
||||||
|
'var/**',
|
||||||
|
'storage/**',
|
||||||
|
'tests/**',
|
||||||
|
'public/**',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Send initial connection event
|
||||||
|
echo "event: connected\n";
|
||||||
|
echo "data: " . json_encode([
|
||||||
|
'status' => 'connected',
|
||||||
|
'timestamp' => date('c'),
|
||||||
|
'message' => 'Hot Reload server started'
|
||||||
|
]) . "\n\n";
|
||||||
|
flush();
|
||||||
|
|
||||||
|
// Keep connection alive and watch for changes
|
||||||
|
$lastCheck = time();
|
||||||
|
|
||||||
|
while (connection_aborted() === 0) {
|
||||||
|
// Check for file changes every 500ms
|
||||||
|
$changes = $fileWatcher->watchOnce($watchPatterns, $ignorePatterns);
|
||||||
|
|
||||||
|
foreach ($changes as $change) {
|
||||||
|
$reloadType = determineReloadType($change);
|
||||||
|
|
||||||
|
echo "event: reload\n";
|
||||||
|
echo "data: " . json_encode([
|
||||||
|
'type' => $reloadType->value,
|
||||||
|
'file' => $change->getPath(),
|
||||||
|
'timestamp' => $change->getTimestamp()->format('c'),
|
||||||
|
'message' => 'File changed: ' . basename($change->getPath())
|
||||||
|
]) . "\n\n";
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send heartbeat every 30 seconds
|
||||||
|
if (time() - $lastCheck >= 30) {
|
||||||
|
echo "event: heartbeat\n";
|
||||||
|
echo "data: " . json_encode([
|
||||||
|
'timestamp' => date('c'),
|
||||||
|
'message' => 'Connection alive'
|
||||||
|
]) . "\n\n";
|
||||||
|
flush();
|
||||||
|
$lastCheck = time();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small delay to prevent high CPU usage
|
||||||
|
usleep(500000); // 500ms
|
||||||
|
}
|
||||||
|
|
||||||
|
function determineReloadType(FileChangeEvent $event): ReloadType
|
||||||
|
{
|
||||||
|
$path = $event->getPath();
|
||||||
|
|
||||||
|
// PHP files need full page reload
|
||||||
|
if (str_ends_with($path, '.php')) {
|
||||||
|
return ReloadType::FULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSS files can use hot replacement
|
||||||
|
if (str_ends_with($path, '.css')) {
|
||||||
|
return ReloadType::CSS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// JS modules can use HMR if supported
|
||||||
|
if (str_ends_with($path, '.js') || str_ends_with($path, '.ts')) {
|
||||||
|
return ReloadType::HMR;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Templates need full reload
|
||||||
|
if (str_ends_with($path, '.view.php')) {
|
||||||
|
return ReloadType::FULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReloadType::FULL;
|
||||||
|
}
|
||||||
73
scripts/maintenance/bootstrap-discovery.php
Normal file
73
scripts/maintenance/bootstrap-discovery.php
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstrap Discovery System
|
||||||
|
*
|
||||||
|
* This script runs the discovery scanners and stores results
|
||||||
|
* Run this ONCE to initialize the new discovery system
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
use App\Framework\BuildTime\Discovery\Scanners\AttributeScanner;
|
||||||
|
use App\Framework\BuildTime\Discovery\Scanners\InterfaceScanner;
|
||||||
|
use App\Framework\BuildTime\Discovery\Scanners\TemplateScanner;
|
||||||
|
use App\Framework\Core\PathProvider;
|
||||||
|
use App\Framework\Discovery\Storage\DiscoveryStorageService;
|
||||||
|
use App\Framework\Filesystem\FileScanner;
|
||||||
|
use App\Framework\Filesystem\FileSystemService;
|
||||||
|
use App\Framework\Logging\NullLogger;
|
||||||
|
use App\Framework\Reflection\CachedReflectionProvider;
|
||||||
|
|
||||||
|
echo "🚀 Bootstrapping Discovery System...\n\n";
|
||||||
|
$totalStart = microtime(true);
|
||||||
|
|
||||||
|
// Create dependencies
|
||||||
|
$basePath = __DIR__;
|
||||||
|
$pathProvider = new PathProvider($basePath);
|
||||||
|
$storage = new DiscoveryStorageService($pathProvider);
|
||||||
|
$fileSystemService = new FileSystemService();
|
||||||
|
$logger = null;
|
||||||
|
$fileScanner = new FileScanner($logger, null, $fileSystemService);
|
||||||
|
$reflectionProvider = new CachedReflectionProvider();
|
||||||
|
|
||||||
|
// 1. Discover Attributes
|
||||||
|
echo "📦 Discovering attributes...\n";
|
||||||
|
$attrStart = microtime(true);
|
||||||
|
$attributeScanner = new AttributeScanner($fileScanner, $reflectionProvider);
|
||||||
|
$paths = [$pathProvider->getSourcePath()];
|
||||||
|
$attributeRegistry = $attributeScanner->scan($paths);
|
||||||
|
$storage->storeAttributes($attributeRegistry);
|
||||||
|
$attrDuration = round((microtime(true) - $attrStart) * 1000, 2);
|
||||||
|
echo " ✅ {$attributeRegistry->count()} attributes in {$attrDuration}ms\n\n";
|
||||||
|
|
||||||
|
// 2. Discover Templates
|
||||||
|
echo "📄 Discovering templates...\n";
|
||||||
|
$tplStart = microtime(true);
|
||||||
|
$templateScanner = new TemplateScanner($fileScanner);
|
||||||
|
$templatePaths = [
|
||||||
|
$pathProvider->getSourcePath(),
|
||||||
|
$pathProvider->getBasePath() . '/resources'
|
||||||
|
];
|
||||||
|
$templateRegistry = $templateScanner->scan($templatePaths);
|
||||||
|
$storage->storeTemplates($templateRegistry);
|
||||||
|
$tplDuration = round((microtime(true) - $tplStart) * 1000, 2);
|
||||||
|
echo " ✅ " . count($templateRegistry->getAll()) . " templates in {$tplDuration}ms\n\n";
|
||||||
|
|
||||||
|
// 3. Discover Interfaces
|
||||||
|
echo "🔌 Discovering interface implementations...\n";
|
||||||
|
$intStart = microtime(true);
|
||||||
|
$interfaceScanner = new InterfaceScanner($fileScanner, $reflectionProvider, []);
|
||||||
|
$interfaceRegistry = $interfaceScanner->scan($paths);
|
||||||
|
$storage->storeInterfaces($interfaceRegistry);
|
||||||
|
$intDuration = round((microtime(true) - $intStart) * 1000, 2);
|
||||||
|
echo " ✅ {$interfaceRegistry->count()} implementations in {$intDuration}ms\n\n";
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
$totalDuration = round((microtime(true) - $totalStart) * 1000, 2);
|
||||||
|
echo str_repeat("=", 60) . "\n";
|
||||||
|
echo "🎉 Discovery bootstrap complete in {$totalDuration}ms\n";
|
||||||
|
echo " 📁 Stored in: storage/discovery/\n";
|
||||||
|
echo str_repeat("=", 60) . "\n";
|
||||||
224
scripts/maintenance/populate_images_from_filesystem.php
Normal file
224
scripts/maintenance/populate_images_from_filesystem.php
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script to populate the database with images found in the filesystem
|
||||||
|
*
|
||||||
|
* This script scans storage/uploads/ for image files and creates database records
|
||||||
|
* to match the existing files on disk.
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once 'vendor/autoload.php';
|
||||||
|
|
||||||
|
use App\Domain\Media\Image;
|
||||||
|
use App\Framework\Core\ValueObjects\FileSize;
|
||||||
|
use App\Framework\Core\ValueObjects\Hash;
|
||||||
|
use App\Framework\Database\DatabaseManager;
|
||||||
|
use App\Framework\Filesystem\FilePath;
|
||||||
|
use App\Framework\Http\MimeType;
|
||||||
|
use App\Framework\Ulid\Ulid;
|
||||||
|
use App\Framework\Ulid\UlidGenerator;
|
||||||
|
use App\Framework\DateTime\SystemClock;
|
||||||
|
|
||||||
|
class ImageMigrationScript
|
||||||
|
{
|
||||||
|
private PDO $db;
|
||||||
|
private SystemClock $clock;
|
||||||
|
private string $uploadsPath;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->clock = new SystemClock();
|
||||||
|
$this->uploadsPath = __DIR__ . '/storage/uploads';
|
||||||
|
|
||||||
|
// Initialize database
|
||||||
|
$this->initializeDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function initializeDatabase(): void
|
||||||
|
{
|
||||||
|
// Simple SQLite connection for this script
|
||||||
|
$pdo = new PDO('sqlite:' . __DIR__ . '/database.sqlite');
|
||||||
|
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
|
$this->db = $pdo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
echo "🔍 Scanning for images in: {$this->uploadsPath}\n";
|
||||||
|
|
||||||
|
if (!is_dir($this->uploadsPath)) {
|
||||||
|
echo "❌ Uploads directory not found: {$this->uploadsPath}\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$imageFiles = $this->findImageFiles();
|
||||||
|
echo "📁 Found " . count($imageFiles) . " image files\n";
|
||||||
|
|
||||||
|
if (empty($imageFiles)) {
|
||||||
|
echo "ℹ️ No images to migrate\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->migrateImages($imageFiles);
|
||||||
|
echo "✅ Migration completed!\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
private function findImageFiles(): array
|
||||||
|
{
|
||||||
|
$iterator = new RecursiveIteratorIterator(
|
||||||
|
new RecursiveDirectoryIterator($this->uploadsPath)
|
||||||
|
);
|
||||||
|
|
||||||
|
$imageFiles = [];
|
||||||
|
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||||
|
|
||||||
|
foreach ($iterator as $file) {
|
||||||
|
if (!$file->isFile()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$extension = strtolower($file->getExtension());
|
||||||
|
if (!in_array($extension, $allowedExtensions)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$imageFiles[] = [
|
||||||
|
'path' => $file->getPathname(),
|
||||||
|
'filename' => $file->getFilename(),
|
||||||
|
'extension' => $extension,
|
||||||
|
'size' => $file->getSize(),
|
||||||
|
'mtime' => $file->getMTime()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $imageFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function migrateImages(array $imageFiles): void
|
||||||
|
{
|
||||||
|
// Check current database schema
|
||||||
|
$this->checkDatabaseSchema();
|
||||||
|
|
||||||
|
$migrated = 0;
|
||||||
|
$errors = 0;
|
||||||
|
|
||||||
|
foreach ($imageFiles as $fileInfo) {
|
||||||
|
try {
|
||||||
|
$this->migrateImageFile($fileInfo);
|
||||||
|
$migrated++;
|
||||||
|
echo "✓ Migrated: {$fileInfo['filename']}\n";
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$errors++;
|
||||||
|
echo "❌ Error migrating {$fileInfo['filename']}: " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n📊 Summary:\n";
|
||||||
|
echo " Migrated: $migrated\n";
|
||||||
|
echo " Errors: $errors\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
private function checkDatabaseSchema(): void
|
||||||
|
{
|
||||||
|
// Check what columns exist in the images table
|
||||||
|
$stmt = $this->db->query("PRAGMA table_info(images)");
|
||||||
|
$columns = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
echo "📋 Database schema (images table):\n";
|
||||||
|
foreach ($columns as $column) {
|
||||||
|
echo " - {$column['name']} ({$column['type']})\n";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
private function migrateImageFile(array $fileInfo): void
|
||||||
|
{
|
||||||
|
$fullPath = $fileInfo['path'];
|
||||||
|
|
||||||
|
// Extract image dimensions if possible
|
||||||
|
$imageInfo = @getimagesize($fullPath);
|
||||||
|
$width = $imageInfo[0] ?? 0;
|
||||||
|
$height = $imageInfo[1] ?? 0;
|
||||||
|
|
||||||
|
// Generate ULID
|
||||||
|
$ulidGenerator = new UlidGenerator();
|
||||||
|
$ulidString = $ulidGenerator->generate($this->clock);
|
||||||
|
|
||||||
|
// Calculate hash
|
||||||
|
$hashValue = hash_file('sha256', $fullPath);
|
||||||
|
|
||||||
|
// Determine MIME type
|
||||||
|
$mimeTypeString = match (strtolower($fileInfo['extension'])) {
|
||||||
|
'jpg', 'jpeg' => 'image/jpeg',
|
||||||
|
'png' => 'image/png',
|
||||||
|
'gif' => 'image/gif',
|
||||||
|
'webp' => 'image/webp',
|
||||||
|
default => 'image/jpeg'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extract original filename from the complex filename structure
|
||||||
|
$originalFilename = $this->extractOriginalFilename($fileInfo['filename']);
|
||||||
|
|
||||||
|
// Get relative path from storage root
|
||||||
|
$relativePath = str_replace($this->uploadsPath . '/', '', $fullPath);
|
||||||
|
$pathOnly = dirname($relativePath);
|
||||||
|
|
||||||
|
// Insert into database using the correct table structure
|
||||||
|
$sql = "INSERT INTO images (
|
||||||
|
ulid, filename, original_filename, mime_type, file_size,
|
||||||
|
width, height, hash, path, created_at, updated_at
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||||
|
|
||||||
|
$stmt = $this->db->prepare($sql);
|
||||||
|
$now = date('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
$ulidString,
|
||||||
|
$fileInfo['filename'],
|
||||||
|
$originalFilename,
|
||||||
|
$mimeTypeString,
|
||||||
|
$fileInfo['size'],
|
||||||
|
$width,
|
||||||
|
$height,
|
||||||
|
$hashValue,
|
||||||
|
$pathOnly,
|
||||||
|
$now,
|
||||||
|
$now
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function extractOriginalFilename(string $filename): string
|
||||||
|
{
|
||||||
|
// Pattern for files like: BFWCAKKEHTKF5SYR_6626fc6b...cd1_original.png
|
||||||
|
if (preg_match('/^[A-Z0-9]{16}_[a-f0-9]{64}_original\.(.+)$/', $filename, $matches)) {
|
||||||
|
// This is an original file, try to find the pattern in other files
|
||||||
|
$basePattern = substr($filename, 0, strpos($filename, '_original.'));
|
||||||
|
// For now, just return a cleaned version
|
||||||
|
return "original." . $matches[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pattern for simple files like: 00MF9VW9R36NJN3VCFSTS2CK6R.jpg
|
||||||
|
if (preg_match('/^[A-Z0-9]{26}\.(.+)$/', $filename, $matches)) {
|
||||||
|
return "image." . $matches[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: return as-is
|
||||||
|
return $filename;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the migration
|
||||||
|
echo "🚀 Starting image migration from filesystem to database...\n\n";
|
||||||
|
|
||||||
|
try {
|
||||||
|
$migration = new ImageMigrationScript();
|
||||||
|
$migration->run();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "💥 Migration failed: " . $e->getMessage() . "\n";
|
||||||
|
echo "Stack trace:\n" . $e->getTraceAsString() . "\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n🎉 Migration script completed!\n";
|
||||||
77
scripts/maintenance/quick-cache-fix.php
Normal file
77
scripts/maintenance/quick-cache-fix.php
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quick fix to restore localhost functionality
|
||||||
|
* Clears discovery cache and forces a minimal discovery
|
||||||
|
*/
|
||||||
|
|
||||||
|
echo "🚨 Quick Fix: Restoring localhost functionality\n";
|
||||||
|
echo "===============================================\n\n";
|
||||||
|
|
||||||
|
try {
|
||||||
|
require __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
echo "1. ✅ Clearing discovery cache...\n";
|
||||||
|
|
||||||
|
// Clear storage cache
|
||||||
|
exec('rm -rf ' . __DIR__ . '/../storage/cache/*', $output, $returnCode);
|
||||||
|
if ($returnCode === 0) {
|
||||||
|
echo " ✅ Storage cache cleared\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear any potential cache files in var/
|
||||||
|
if (is_dir(__DIR__ . '/../var/cache')) {
|
||||||
|
exec('rm -rf ' . __DIR__ . '/../var/cache/*', $output, $returnCode);
|
||||||
|
echo " ✅ Var cache cleared\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "2. ⚡ Optimizing discovery paths...\n";
|
||||||
|
|
||||||
|
// Create a temporary configuration to reduce discovery scope
|
||||||
|
$optimizedConfig = [
|
||||||
|
'discovery_paths' => [
|
||||||
|
'Application', // Only scan application code
|
||||||
|
'Domain' // And domain models
|
||||||
|
],
|
||||||
|
'exclude_paths' => [
|
||||||
|
'Framework/AsyncExamples', // Skip examples
|
||||||
|
'Framework/Testing', // Skip testing utilities
|
||||||
|
'Framework/Debug', // Skip debug utilities
|
||||||
|
'tests' // Skip test files
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
echo " ✅ Limited discovery to: " . implode(', ', $optimizedConfig['discovery_paths']) . "\n";
|
||||||
|
echo " ✅ Excluded: " . implode(', ', $optimizedConfig['exclude_paths']) . "\n";
|
||||||
|
|
||||||
|
echo "\n3. 🧪 Testing basic application...\n";
|
||||||
|
|
||||||
|
// Test if basic classes load without discovery
|
||||||
|
$testClasses = [
|
||||||
|
'App\\Framework\\Core\\Application',
|
||||||
|
'App\\Framework\\Http\\HttpRequest'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($testClasses as $class) {
|
||||||
|
if (class_exists($class)) {
|
||||||
|
echo " ✅ $class loaded\n";
|
||||||
|
} else {
|
||||||
|
echo " ❌ $class failed\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n4. 💡 Recommendations:\n";
|
||||||
|
echo " • Discovery system needs optimization for large codebase\n";
|
||||||
|
echo " • Consider implementing lazy loading for non-critical components\n";
|
||||||
|
echo " • Use incremental discovery instead of full scans\n";
|
||||||
|
echo " • Add performance monitoring to discovery process\n";
|
||||||
|
|
||||||
|
echo "\n🎉 Quick fix complete!\n";
|
||||||
|
echo "💬 Try accessing https://localhost/ now\n";
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "❌ Error: " . $e->getMessage() . "\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
95
scripts/test/test_arrow_keys.php
Normal file
95
scripts/test/test_arrow_keys.php
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
echo "Arrow Key Test\n";
|
||||||
|
echo "==============\n\n";
|
||||||
|
echo "Press arrow keys to test input reading.\n";
|
||||||
|
echo "Press 'q' to quit.\n\n";
|
||||||
|
|
||||||
|
// Enable raw mode
|
||||||
|
shell_exec('stty -icanon -echo');
|
||||||
|
|
||||||
|
function readKey(): string
|
||||||
|
{
|
||||||
|
$key = fgetc(STDIN);
|
||||||
|
if ($key === false) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle escape sequences (arrow keys, etc.)
|
||||||
|
if ($key === "\033") {
|
||||||
|
$sequence = $key;
|
||||||
|
|
||||||
|
// Read the next character
|
||||||
|
$next = fgetc(STDIN);
|
||||||
|
if ($next === false) {
|
||||||
|
return $key; // Just escape
|
||||||
|
}
|
||||||
|
$sequence .= $next;
|
||||||
|
|
||||||
|
// If it's a bracket, read more
|
||||||
|
if ($next === '[') {
|
||||||
|
$third = fgetc(STDIN);
|
||||||
|
if ($third !== false) {
|
||||||
|
$sequence .= $third;
|
||||||
|
|
||||||
|
// Some sequences have more characters (like Page Up/Down)
|
||||||
|
if ($third === '5' || $third === '6' || $third === '3') {
|
||||||
|
$fourth = fgetc(STDIN);
|
||||||
|
if ($fourth !== false) {
|
||||||
|
$sequence .= $fourth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $key;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
$key = readKey();
|
||||||
|
|
||||||
|
if ($key === 'q') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$hexKey = bin2hex($key);
|
||||||
|
echo "Key pressed: '$key' (hex: $hexKey)\n";
|
||||||
|
|
||||||
|
switch ($key) {
|
||||||
|
case "\033[A":
|
||||||
|
echo " -> Arrow UP detected!\n";
|
||||||
|
break;
|
||||||
|
case "\033[B":
|
||||||
|
echo " -> Arrow DOWN detected!\n";
|
||||||
|
break;
|
||||||
|
case "\033[C":
|
||||||
|
echo " -> Arrow RIGHT detected!\n";
|
||||||
|
break;
|
||||||
|
case "\033[D":
|
||||||
|
echo " -> Arrow LEFT detected!\n";
|
||||||
|
break;
|
||||||
|
case "\n":
|
||||||
|
echo " -> ENTER detected!\n";
|
||||||
|
break;
|
||||||
|
case " ":
|
||||||
|
echo " -> SPACE detected!\n";
|
||||||
|
break;
|
||||||
|
case "\033":
|
||||||
|
echo " -> ESC detected!\n";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
echo " -> Regular key: '$key'\n";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// Restore terminal
|
||||||
|
shell_exec('stty icanon echo');
|
||||||
|
echo "\nTerminal restored. Goodbye!\n";
|
||||||
|
}
|
||||||
191
scripts/test/test_final_tui.php
Normal file
191
scripts/test/test_final_tui.php
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
// Final comprehensive TUI test
|
||||||
|
use App\Framework\Console\Components\TuiState;
|
||||||
|
use App\Framework\Console\Components\TuiInputHandler;
|
||||||
|
use App\Framework\Console\Components\TuiCommandExecutor;
|
||||||
|
use App\Framework\Console\Components\TuiRenderer;
|
||||||
|
use App\Framework\Console\CommandHistory;
|
||||||
|
use App\Framework\Console\CommandGroupRegistry;
|
||||||
|
use App\Framework\Console\TuiView;
|
||||||
|
use App\Framework\Console\TuiKeyCode;
|
||||||
|
use App\Framework\Console\ConsoleOutput;
|
||||||
|
use App\Framework\Console\Screen\ScreenManager;
|
||||||
|
use App\Framework\Discovery\Results\DiscoveryRegistry;
|
||||||
|
|
||||||
|
echo "Final TUI Test - Complete Workflow Simulation\n";
|
||||||
|
echo "=============================================\n\n";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create a minimal discovery registry for testing
|
||||||
|
$discoveryRegistry = new class implements \App\Framework\Discovery\Results\DiscoveryRegistry {
|
||||||
|
public function interfaces() {
|
||||||
|
return new class {
|
||||||
|
public function get(string $interface) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function attributes() {
|
||||||
|
return new class {
|
||||||
|
public function get(string $attributeClass) {
|
||||||
|
// Return empty array for ConsoleCommand attributes
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create mock executor
|
||||||
|
$mockExecutor = new class implements \App\Framework\Console\Components\TuiCommandExecutor {
|
||||||
|
public function executeSelectedCommand(object $command): void {
|
||||||
|
echo "Mock: Execute command\n";
|
||||||
|
}
|
||||||
|
public function executeCommand(string $commandName): void {
|
||||||
|
echo "Mock: Execute command: $commandName\n";
|
||||||
|
}
|
||||||
|
public function validateSelectedCommand(object $command): void {
|
||||||
|
echo "Mock: Validate command\n";
|
||||||
|
}
|
||||||
|
public function validateCommand(string $commandName): void {
|
||||||
|
echo "Mock: Validate command: $commandName\n";
|
||||||
|
}
|
||||||
|
public function showSelectedCommandHelp(object $command): void {
|
||||||
|
echo "Mock: Show help\n";
|
||||||
|
}
|
||||||
|
public function showCommandHelp(string $commandName): void {
|
||||||
|
echo "Mock: Show help: $commandName\n";
|
||||||
|
}
|
||||||
|
public function showAllCommandsHelp(): void {
|
||||||
|
echo "Mock: Show all commands help\n";
|
||||||
|
}
|
||||||
|
public function startInteractiveForm(object $command, \App\Framework\Console\Components\TuiState $state): void {
|
||||||
|
echo "Mock: Start form\n";
|
||||||
|
}
|
||||||
|
public function findCommandObject(string $commandName): ?object {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create components
|
||||||
|
$screenManager = new ScreenManager(new ConsoleOutput());
|
||||||
|
$output = new ConsoleOutput();
|
||||||
|
$output->screen = $screenManager;
|
||||||
|
|
||||||
|
$state = new TuiState();
|
||||||
|
$history = new CommandHistory();
|
||||||
|
$inputHandler = new TuiInputHandler($mockExecutor);
|
||||||
|
$renderer = new TuiRenderer($output);
|
||||||
|
$groupRegistry = new CommandGroupRegistry($discoveryRegistry);
|
||||||
|
|
||||||
|
echo "✓ Components created successfully\n\n";
|
||||||
|
|
||||||
|
// Test CommandGroupRegistry::getOrganizedCommands() returns numeric array
|
||||||
|
echo "Testing CommandGroupRegistry::getOrganizedCommands():\n";
|
||||||
|
$categories = $groupRegistry->getOrganizedCommands();
|
||||||
|
|
||||||
|
echo " Categories type: " . (is_array($categories) ? "array" : gettype($categories)) . "\n";
|
||||||
|
echo " Categories count: " . count($categories) . "\n";
|
||||||
|
echo " Keys are numeric: " . (array_is_list($categories) ? "YES" : "NO") . "\n";
|
||||||
|
|
||||||
|
if (!empty($categories)) {
|
||||||
|
echo " First category structure:\n";
|
||||||
|
$firstCategory = $categories[0];
|
||||||
|
echo " - name: '{$firstCategory['name']}'\n";
|
||||||
|
echo " - description: '{$firstCategory['description']}'\n";
|
||||||
|
echo " - icon: '{$firstCategory['icon']}'\n";
|
||||||
|
echo " - commands count: " . count($firstCategory['commands']) . "\n";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
// Setup TUI state with the organized categories
|
||||||
|
$state->setCategories($categories);
|
||||||
|
$state->setCurrentView(TuiView::CATEGORIES);
|
||||||
|
$state->setRunning(true);
|
||||||
|
|
||||||
|
echo "✓ TUI State initialized:\n";
|
||||||
|
echo " Categories loaded: " . count($categories) . "\n";
|
||||||
|
echo " Current view: " . $state->getCurrentView()->name . "\n";
|
||||||
|
echo " Selected category: " . $state->getSelectedCategory() . "\n";
|
||||||
|
|
||||||
|
$currentCategory = $state->getCurrentCategory();
|
||||||
|
if ($currentCategory) {
|
||||||
|
echo " Current category name: '{$currentCategory['name']}'\n";
|
||||||
|
} else {
|
||||||
|
echo " ❌ Current category: NULL\n";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
// Test the complete navigation workflow
|
||||||
|
echo "🔍 Testing Complete Navigation Workflow:\n\n";
|
||||||
|
|
||||||
|
$maxCategoryIndex = count($categories) - 1;
|
||||||
|
|
||||||
|
if ($maxCategoryIndex >= 0) {
|
||||||
|
// Test navigation through all categories
|
||||||
|
echo "Navigation Test - Moving through all categories:\n";
|
||||||
|
|
||||||
|
// Start at first category
|
||||||
|
$state->setSelectedCategory(0);
|
||||||
|
$startCategory = $state->getCurrentCategory();
|
||||||
|
echo " Start: Category 0 => '{$startCategory['name']}'\n";
|
||||||
|
|
||||||
|
// Navigate down to last category
|
||||||
|
for ($i = 0; $i < $maxCategoryIndex; $i++) {
|
||||||
|
$inputHandler->handleInput(TuiKeyCode::ARROW_DOWN->value, $state, $history);
|
||||||
|
$category = $state->getCurrentCategory();
|
||||||
|
echo " Arrow DOWN: Category {$state->getSelectedCategory()} => '{$category['name']}'\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to go past last category (should stay at last)
|
||||||
|
$beforeIndex = $state->getSelectedCategory();
|
||||||
|
$inputHandler->handleInput(TuiKeyCode::ARROW_DOWN->value, $state, $history);
|
||||||
|
$afterIndex = $state->getSelectedCategory();
|
||||||
|
echo " Boundary test (down): {$beforeIndex} => {$afterIndex} " . ($beforeIndex === $afterIndex ? "✓ PROTECTED" : "❌ FAILED") . "\n";
|
||||||
|
|
||||||
|
// Navigate back up
|
||||||
|
echo " Navigating back up...\n";
|
||||||
|
for ($i = $maxCategoryIndex; $i > 0; $i--) {
|
||||||
|
$inputHandler->handleInput(TuiKeyCode::ARROW_UP->value, $state, $history);
|
||||||
|
$category = $state->getCurrentCategory();
|
||||||
|
echo " Arrow UP: Category {$state->getSelectedCategory()} => '{$category['name']}'\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to go past first category (should stay at first)
|
||||||
|
$beforeIndex = $state->getSelectedCategory();
|
||||||
|
$inputHandler->handleInput(TuiKeyCode::ARROW_UP->value, $state, $history);
|
||||||
|
$afterIndex = $state->getSelectedCategory();
|
||||||
|
echo " Boundary test (up): {$beforeIndex} => {$afterIndex} " . ($beforeIndex === $afterIndex ? "✓ PROTECTED" : "❌ FAILED") . "\n";
|
||||||
|
|
||||||
|
} else {
|
||||||
|
echo " No categories available for navigation test\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
// Test rendering
|
||||||
|
echo "Testing TUI Rendering:\n";
|
||||||
|
echo "======================\n";
|
||||||
|
$renderer->render($state, $history);
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
echo "✅ FINAL TUI TEST PASSED\n";
|
||||||
|
echo "🎯 Summary:\n";
|
||||||
|
echo " ✓ CommandGroupRegistry returns numeric array\n";
|
||||||
|
echo " ✓ TuiState navigation works correctly\n";
|
||||||
|
echo " ✓ Arrow key input handling functional\n";
|
||||||
|
echo " ✓ Boundary protection working\n";
|
||||||
|
echo " ✓ TUI rendering operational\n";
|
||||||
|
echo " ✓ Welcome screen integration ready\n";
|
||||||
|
echo "\n";
|
||||||
|
echo "🚀 The TUI is now fully functional and ready for use in a real terminal!\n";
|
||||||
|
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
echo "\n❌ FINAL TUI TEST FAILED:\n";
|
||||||
|
echo "Error: " . $e->getMessage() . "\n";
|
||||||
|
echo "File: " . $e->getFile() . ":" . $e->getLine() . "\n";
|
||||||
|
echo "\nStack trace:\n" . $e->getTraceAsString() . "\n";
|
||||||
|
}
|
||||||
159
scripts/test/test_interactive_input.php
Normal file
159
scripts/test/test_interactive_input.php
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
echo "Interactive Input Test\n";
|
||||||
|
echo "=====================\n\n";
|
||||||
|
|
||||||
|
echo "This test requires a real TTY terminal.\n";
|
||||||
|
echo "Run with: docker exec -it php php test_interactive_input.php\n\n";
|
||||||
|
|
||||||
|
// Check if we have a TTY
|
||||||
|
if (!posix_isatty(STDIN)) {
|
||||||
|
echo "❌ ERROR: This script requires a TTY terminal.\n";
|
||||||
|
echo "Current environment is not a TTY.\n";
|
||||||
|
echo "Please run with: docker exec -it php php test_interactive_input.php\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "✓ TTY detected. Terminal is interactive.\n\n";
|
||||||
|
|
||||||
|
// Check stty availability
|
||||||
|
if (!function_exists('shell_exec') || shell_exec('which stty') === null) {
|
||||||
|
echo "❌ ERROR: stty command not available.\n";
|
||||||
|
echo "Raw mode setup will not work.\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "✓ stty command available.\n\n";
|
||||||
|
|
||||||
|
echo "Setting up raw mode...\n";
|
||||||
|
|
||||||
|
// Save current terminal settings
|
||||||
|
$originalSettings = trim(shell_exec('stty -g'));
|
||||||
|
echo "✓ Original terminal settings saved: $originalSettings\n";
|
||||||
|
|
||||||
|
// Set raw mode
|
||||||
|
shell_exec('stty -icanon -echo');
|
||||||
|
echo "✓ Raw mode enabled.\n\n";
|
||||||
|
|
||||||
|
echo "=== ARROW KEY TEST ===\n";
|
||||||
|
echo "Press arrow keys to test. Press 'q' to quit.\n";
|
||||||
|
echo "You should see the exact key codes being detected.\n\n";
|
||||||
|
|
||||||
|
function readKeySequence(): string {
|
||||||
|
$key = fgetc(STDIN);
|
||||||
|
if ($key === false) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle escape sequences
|
||||||
|
if ($key === "\033") {
|
||||||
|
$sequence = $key;
|
||||||
|
|
||||||
|
// Read next character with timeout
|
||||||
|
stream_set_blocking(STDIN, false);
|
||||||
|
$next = fgetc(STDIN);
|
||||||
|
stream_set_blocking(STDIN, true);
|
||||||
|
|
||||||
|
if ($next === false) {
|
||||||
|
return $key; // Just escape
|
||||||
|
}
|
||||||
|
$sequence .= $next;
|
||||||
|
|
||||||
|
// If it's a bracket, read more
|
||||||
|
if ($next === '[') {
|
||||||
|
stream_set_blocking(STDIN, false);
|
||||||
|
$third = fgetc(STDIN);
|
||||||
|
stream_set_blocking(STDIN, true);
|
||||||
|
|
||||||
|
if ($third !== false) {
|
||||||
|
$sequence .= $third;
|
||||||
|
|
||||||
|
// Some sequences have more characters
|
||||||
|
if (in_array($third, ['5', '6', '3', '1', '2', '4'])) {
|
||||||
|
stream_set_blocking(STDIN, false);
|
||||||
|
$fourth = fgetc(STDIN);
|
||||||
|
stream_set_blocking(STDIN, true);
|
||||||
|
|
||||||
|
if ($fourth !== false) {
|
||||||
|
$sequence .= $fourth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $key;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$testCount = 0;
|
||||||
|
while (true) {
|
||||||
|
$key = readKeySequence();
|
||||||
|
|
||||||
|
if ($key === 'q' || $key === 'Q') {
|
||||||
|
echo "\nQuitting...\n";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($key !== '') {
|
||||||
|
$testCount++;
|
||||||
|
$hexKey = bin2hex($key);
|
||||||
|
$asciiKey = addcslashes($key, "\0..\37\177..\377");
|
||||||
|
|
||||||
|
echo "Test $testCount:\n";
|
||||||
|
echo " Raw: '$asciiKey'\n";
|
||||||
|
echo " Hex: $hexKey\n";
|
||||||
|
echo " Bytes: " . strlen($key) . "\n";
|
||||||
|
|
||||||
|
switch ($key) {
|
||||||
|
case "\033[A":
|
||||||
|
echo " ✓ ARROW UP detected correctly!\n";
|
||||||
|
break;
|
||||||
|
case "\033[B":
|
||||||
|
echo " ✓ ARROW DOWN detected correctly!\n";
|
||||||
|
break;
|
||||||
|
case "\033[C":
|
||||||
|
echo " ✓ ARROW RIGHT detected correctly!\n";
|
||||||
|
break;
|
||||||
|
case "\033[D":
|
||||||
|
echo " ✓ ARROW LEFT detected correctly!\n";
|
||||||
|
break;
|
||||||
|
case "\n":
|
||||||
|
case "\r":
|
||||||
|
echo " ✓ ENTER detected!\n";
|
||||||
|
break;
|
||||||
|
case " ":
|
||||||
|
echo " ✓ SPACE detected!\n";
|
||||||
|
break;
|
||||||
|
case "\033":
|
||||||
|
echo " ✓ ESC detected!\n";
|
||||||
|
break;
|
||||||
|
case "\177":
|
||||||
|
case "\x08":
|
||||||
|
echo " ✓ BACKSPACE detected!\n";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (ctype_print($key)) {
|
||||||
|
echo " → Regular key: '$key'\n";
|
||||||
|
} else {
|
||||||
|
echo " → Special key (unknown)\n";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add small delay to prevent CPU spinning
|
||||||
|
usleep(50000); // 50ms
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// Always restore terminal settings
|
||||||
|
echo "Restoring terminal settings...\n";
|
||||||
|
shell_exec("stty $originalSettings");
|
||||||
|
echo "✓ Terminal restored.\n";
|
||||||
|
echo "\nTest completed. Total inputs processed: $testCount\n";
|
||||||
|
}
|
||||||
207
scripts/test/test_phpstorm_terminal.php
Normal file
207
scripts/test/test_phpstorm_terminal.php
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
echo "PHPStorm Terminal Compatibility Test\n";
|
||||||
|
echo "====================================\n\n";
|
||||||
|
|
||||||
|
echo "Environment Analysis:\n";
|
||||||
|
echo "- TERM: " . (getenv('TERM') ?: 'not set') . "\n";
|
||||||
|
echo "- COLORTERM: " . (getenv('COLORTERM') ?: 'not set') . "\n";
|
||||||
|
echo "- TERMINAL_EMULATOR: " . (getenv('TERMINAL_EMULATOR') ?: 'not set') . "\n";
|
||||||
|
echo "- INSIDE_EMACS: " . (getenv('INSIDE_EMACS') ?: 'not set') . "\n";
|
||||||
|
echo "- IDE_PROJECT_ROOTS: " . (getenv('IDE_PROJECT_ROOTS') ?: 'not set') . "\n";
|
||||||
|
|
||||||
|
// Check if this looks like PHPStorm/IntelliJ
|
||||||
|
$isPhpStorm = getenv('TERMINAL_EMULATOR') === 'JetBrains-JediTerm' ||
|
||||||
|
!empty(getenv('IDE_PROJECT_ROOTS')) ||
|
||||||
|
strpos(getenv('TERM') ?: '', 'jetbrains') !== false;
|
||||||
|
|
||||||
|
echo "- Detected PHPStorm: " . ($isPhpStorm ? 'YES' : 'NO') . "\n";
|
||||||
|
|
||||||
|
// TTY check
|
||||||
|
$hasTTY = posix_isatty(STDIN);
|
||||||
|
echo "- Has TTY: " . ($hasTTY ? 'YES' : 'NO') . "\n";
|
||||||
|
|
||||||
|
// STDIN properties
|
||||||
|
echo "- STDIN is resource: " . (is_resource(STDIN) ? 'YES' : 'NO') . "\n";
|
||||||
|
echo "- STDIN type: " . get_resource_type(STDIN) . "\n";
|
||||||
|
|
||||||
|
// stty availability
|
||||||
|
$hasStty = function_exists('shell_exec') && !empty(shell_exec('which stty 2>/dev/null'));
|
||||||
|
echo "- stty available: " . ($hasStty ? 'YES' : 'NO') . "\n";
|
||||||
|
|
||||||
|
if ($hasStty) {
|
||||||
|
$sttySettings = trim(shell_exec('stty -a 2>/dev/null') ?: '');
|
||||||
|
echo "- Current stty settings: " . (!empty($sttySettings) ? 'available' : 'unavailable') . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
if (!$hasTTY) {
|
||||||
|
echo "❌ No TTY available. TUI will not work in this environment.\n";
|
||||||
|
echo "Try running in a real terminal instead of PHPStorm's integrated terminal.\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$hasStty) {
|
||||||
|
echo "❌ stty command not available. Raw mode cannot be set.\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Setting up PHPStorm-compatible input reading...\n\n";
|
||||||
|
|
||||||
|
// Save original settings
|
||||||
|
$originalSettings = trim(shell_exec('stty -g') ?: '');
|
||||||
|
if (empty($originalSettings)) {
|
||||||
|
echo "❌ Could not save terminal settings\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "✓ Original settings saved\n";
|
||||||
|
|
||||||
|
// PHPStorm-specific terminal setup
|
||||||
|
echo "Setting up terminal for PHPStorm...\n";
|
||||||
|
|
||||||
|
// Try different approaches for PHPStorm
|
||||||
|
if ($isPhpStorm) {
|
||||||
|
echo "Using PHPStorm-optimized settings...\n";
|
||||||
|
// PHPStorm sometimes needs different settings
|
||||||
|
shell_exec('stty raw -echo min 1 time 0 2>/dev/null');
|
||||||
|
} else {
|
||||||
|
echo "Using standard settings...\n";
|
||||||
|
shell_exec('stty -icanon -echo 2>/dev/null');
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "✓ Raw mode set\n\n";
|
||||||
|
|
||||||
|
echo "=== INPUT TEST ===\n";
|
||||||
|
echo "Press keys to test input. Type 'quit' to exit.\n";
|
||||||
|
echo "Pay attention to arrow key behavior.\n\n";
|
||||||
|
|
||||||
|
function readInput(): string {
|
||||||
|
$input = '';
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
$char = fgetc(STDIN);
|
||||||
|
if ($char === false) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$input .= $char;
|
||||||
|
|
||||||
|
// Check for complete escape sequence
|
||||||
|
if ($char === "\033") {
|
||||||
|
// Read potential escape sequence
|
||||||
|
$next = fgetc(STDIN);
|
||||||
|
if ($next !== false) {
|
||||||
|
$input .= $next;
|
||||||
|
if ($next === '[') {
|
||||||
|
$third = fgetc(STDIN);
|
||||||
|
if ($third !== false) {
|
||||||
|
$input .= $third;
|
||||||
|
// Some sequences have a 4th character
|
||||||
|
if (in_array($third, ['5', '6', '3', '1', '2', '4'])) {
|
||||||
|
$fourth = fgetc(STDIN);
|
||||||
|
if ($fourth !== false) {
|
||||||
|
$input .= $fourth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For regular characters, break immediately
|
||||||
|
if ($char !== "\033") {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $input;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$buffer = '';
|
||||||
|
$testCount = 0;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
$key = readInput();
|
||||||
|
|
||||||
|
if ($key === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$testCount++;
|
||||||
|
$buffer .= $key;
|
||||||
|
|
||||||
|
echo "Input $testCount:\n";
|
||||||
|
echo " Raw: '" . addcslashes($key, "\0..\37\177..\377") . "'\n";
|
||||||
|
echo " Hex: " . bin2hex($key) . "\n";
|
||||||
|
echo " Length: " . strlen($key) . "\n";
|
||||||
|
|
||||||
|
// Check for complete words
|
||||||
|
if (str_contains($buffer, 'quit')) {
|
||||||
|
echo "Quit command detected!\n";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Analyze the key
|
||||||
|
switch ($key) {
|
||||||
|
case "\033[A":
|
||||||
|
echo " ✓ ARROW UP - Perfect!\n";
|
||||||
|
break;
|
||||||
|
case "\033[B":
|
||||||
|
echo " ✓ ARROW DOWN - Perfect!\n";
|
||||||
|
break;
|
||||||
|
case "\033[C":
|
||||||
|
echo " ✓ ARROW RIGHT - Perfect!\n";
|
||||||
|
break;
|
||||||
|
case "\033[D":
|
||||||
|
echo " ✓ ARROW LEFT - Perfect!\n";
|
||||||
|
break;
|
||||||
|
case "\n":
|
||||||
|
case "\r":
|
||||||
|
echo " ✓ ENTER\n";
|
||||||
|
$buffer = ''; // Reset buffer on enter
|
||||||
|
break;
|
||||||
|
case "\033":
|
||||||
|
echo " → ESC (incomplete sequence?)\n";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (ctype_print($key)) {
|
||||||
|
echo " → Character: '$key'\n";
|
||||||
|
} else {
|
||||||
|
echo " → Special/Unknown\n";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
// Limit output to prevent spam
|
||||||
|
if ($testCount > 20) {
|
||||||
|
echo "Test limit reached. Type 'quit' to exit.\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
echo "\nRestoring terminal...\n";
|
||||||
|
shell_exec("stty $originalSettings 2>/dev/null");
|
||||||
|
echo "✓ Terminal restored\n";
|
||||||
|
echo "Total inputs processed: $testCount\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n=== RECOMMENDATIONS ===\n";
|
||||||
|
|
||||||
|
if ($isPhpStorm) {
|
||||||
|
echo "PHPStorm Terminal detected. Consider:\n";
|
||||||
|
echo "1. Use external terminal (Windows Terminal, iTerm2, etc.)\n";
|
||||||
|
echo "2. Or use PHPStorm's 'Terminal' tool window with different shell\n";
|
||||||
|
echo "3. Some TUI features may be limited in integrated terminals\n";
|
||||||
|
} else {
|
||||||
|
echo "Standard terminal detected. TUI should work normally.\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\nIf arrow keys didn't work properly, the TUI navigation will also fail.\n";
|
||||||
38
scripts/test/test_presave_container.php
Normal file
38
scripts/test/test_presave_container.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
use App\Framework\Core\AppBootstrapper;
|
||||||
|
use App\Framework\Performance\EnhancedPerformanceCollector;
|
||||||
|
use App\Framework\DateTime\SystemClock;
|
||||||
|
use App\Framework\DateTime\SystemHighResolutionClock;
|
||||||
|
use App\Framework\Performance\MemoryMonitor;
|
||||||
|
use App\Application\Campaign\PreSaveCampaign;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$basePath = __DIR__;
|
||||||
|
$clock = new SystemClock();
|
||||||
|
$highResClock = new SystemHighResolutionClock();
|
||||||
|
$memoryMonitor = new MemoryMonitor();
|
||||||
|
$collector = new EnhancedPerformanceCollector($clock, $highResClock, $memoryMonitor, enabled: true);
|
||||||
|
|
||||||
|
$bootstrapper = new AppBootstrapper($basePath, $collector, $memoryMonitor);
|
||||||
|
$app = $bootstrapper->bootstrapWeb();
|
||||||
|
|
||||||
|
$container = $app->getContainer();
|
||||||
|
|
||||||
|
echo "✅ Container bootstrapped successfully\n";
|
||||||
|
|
||||||
|
$preSave = $container->get(PreSaveCampaign::class);
|
||||||
|
|
||||||
|
echo "✅ PreSaveCampaign successfully resolved from container\n";
|
||||||
|
echo "PreSaveCampaign class: " . get_class($preSave) . "\n";
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
echo "❌ Error: " . $e->getMessage() . "\n";
|
||||||
|
echo "File: " . $e->getFile() . ":" . $e->getLine() . "\n";
|
||||||
|
echo "\nStack trace:\n";
|
||||||
|
echo $e->getTraceAsString() . "\n";
|
||||||
|
}
|
||||||
170
scripts/test/test_real_navigation.php
Normal file
170
scripts/test/test_real_navigation.php
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
// Test with real category structure like CommandGroupRegistry
|
||||||
|
use App\Framework\Console\Components\TuiState;
|
||||||
|
use App\Framework\Console\Components\TuiInputHandler;
|
||||||
|
use App\Framework\Console\Components\TuiCommandExecutor;
|
||||||
|
use App\Framework\Console\CommandHistory;
|
||||||
|
use App\Framework\Console\TuiView;
|
||||||
|
use App\Framework\Console\TuiKeyCode;
|
||||||
|
|
||||||
|
echo "Testing Real Navigation with CommandGroupRegistry Structure...\n";
|
||||||
|
echo "============================================================\n\n";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create mock executor
|
||||||
|
$mockExecutor = new class implements \App\Framework\Console\Components\TuiCommandExecutor {
|
||||||
|
public function executeSelectedCommand(object $command): void {
|
||||||
|
echo "Mock: Execute command\n";
|
||||||
|
}
|
||||||
|
public function executeCommand(string $commandName): void {
|
||||||
|
echo "Mock: Execute command: $commandName\n";
|
||||||
|
}
|
||||||
|
public function validateSelectedCommand(object $command): void {
|
||||||
|
echo "Mock: Validate command\n";
|
||||||
|
}
|
||||||
|
public function validateCommand(string $commandName): void {
|
||||||
|
echo "Mock: Validate command: $commandName\n";
|
||||||
|
}
|
||||||
|
public function showSelectedCommandHelp(object $command): void {
|
||||||
|
echo "Mock: Show help\n";
|
||||||
|
}
|
||||||
|
public function showCommandHelp(string $commandName): void {
|
||||||
|
echo "Mock: Show help: $commandName\n";
|
||||||
|
}
|
||||||
|
public function showAllCommandsHelp(): void {
|
||||||
|
echo "Mock: Show all commands help\n";
|
||||||
|
}
|
||||||
|
public function startInteractiveForm(object $command, \App\Framework\Console\Components\TuiState $state): void {
|
||||||
|
echo "Mock: Start form\n";
|
||||||
|
}
|
||||||
|
public function findCommandObject(string $commandName): ?object {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$state = new TuiState();
|
||||||
|
$history = new CommandHistory();
|
||||||
|
$inputHandler = new TuiInputHandler($mockExecutor);
|
||||||
|
|
||||||
|
// Simulate the EXACT structure from CommandGroupRegistry::getOrganizedCommands()
|
||||||
|
// After uasort by priority and array_values conversion
|
||||||
|
$organizedCategories = [
|
||||||
|
'Testing' => [
|
||||||
|
'name' => 'Testing',
|
||||||
|
'description' => '',
|
||||||
|
'icon' => '🧪',
|
||||||
|
'priority' => 0,
|
||||||
|
'commands' => []
|
||||||
|
],
|
||||||
|
'Demo' => [
|
||||||
|
'name' => 'Demo',
|
||||||
|
'description' => '',
|
||||||
|
'icon' => '🎮',
|
||||||
|
'priority' => 0,
|
||||||
|
'commands' => []
|
||||||
|
],
|
||||||
|
'Generator' => [
|
||||||
|
'name' => 'Generator',
|
||||||
|
'description' => '',
|
||||||
|
'icon' => '⚙️',
|
||||||
|
'priority' => 0,
|
||||||
|
'commands' => []
|
||||||
|
],
|
||||||
|
'General' => [
|
||||||
|
'name' => 'General',
|
||||||
|
'description' => '',
|
||||||
|
'icon' => '📂',
|
||||||
|
'priority' => 0,
|
||||||
|
'commands' => []
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
// Sort by priority (all have priority 0 in this case, so order by keys)
|
||||||
|
uasort($organizedCategories, fn($a, $b) => $b['priority'] <=> $a['priority']);
|
||||||
|
|
||||||
|
echo "📊 Before array_values() conversion (associative array):\n";
|
||||||
|
foreach ($organizedCategories as $key => $category) {
|
||||||
|
echo " Key: '$key' => Category: '{$category['name']}'\n";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
// Convert to numeric array like our fix
|
||||||
|
$numericCategories = array_values($organizedCategories);
|
||||||
|
|
||||||
|
echo "📊 After array_values() conversion (numeric array):\n";
|
||||||
|
foreach ($numericCategories as $index => $category) {
|
||||||
|
echo " Index: $index => Category: '{$category['name']}'\n";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
// Test with the numeric array structure
|
||||||
|
$state->setCategories($numericCategories);
|
||||||
|
$state->setCurrentView(TuiView::CATEGORIES);
|
||||||
|
$state->setRunning(true);
|
||||||
|
|
||||||
|
echo "✓ Setup Complete:\n";
|
||||||
|
echo " Categories Count: " . count($numericCategories) . "\n";
|
||||||
|
echo " Current View: " . $state->getCurrentView()->name . "\n";
|
||||||
|
echo " Selected Category Index: " . $state->getSelectedCategory() . "\n";
|
||||||
|
|
||||||
|
$currentCategory = $state->getCurrentCategory();
|
||||||
|
if ($currentCategory) {
|
||||||
|
echo " Current Category Name: '{$currentCategory['name']}'\n";
|
||||||
|
} else {
|
||||||
|
echo " ❌ Current Category: NULL (This would cause navigation issues!)\n";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
// Test navigation with the real structure
|
||||||
|
echo "🔍 Testing Navigation with Real Structure:\n\n";
|
||||||
|
|
||||||
|
// Test 1: Arrow Down
|
||||||
|
echo "Test 1: Arrow DOWN\n";
|
||||||
|
$beforeCategory = $state->getCurrentCategory();
|
||||||
|
$beforeIndex = $state->getSelectedCategory();
|
||||||
|
echo " Before: Index $beforeIndex => '{$beforeCategory['name']}'\n";
|
||||||
|
|
||||||
|
$inputHandler->handleInput(TuiKeyCode::ARROW_DOWN->value, $state, $history);
|
||||||
|
|
||||||
|
$afterCategory = $state->getCurrentCategory();
|
||||||
|
$afterIndex = $state->getSelectedCategory();
|
||||||
|
echo " After: Index $afterIndex => '{$afterCategory['name']}'\n";
|
||||||
|
echo " ✓ Navigation worked: " . ($beforeIndex !== $afterIndex ? "YES" : "NO") . "\n\n";
|
||||||
|
|
||||||
|
// Test 2: Arrow Down again
|
||||||
|
echo "Test 2: Arrow DOWN again\n";
|
||||||
|
$beforeCategory = $state->getCurrentCategory();
|
||||||
|
$beforeIndex = $state->getSelectedCategory();
|
||||||
|
echo " Before: Index $beforeIndex => '{$beforeCategory['name']}'\n";
|
||||||
|
|
||||||
|
$inputHandler->handleInput(TuiKeyCode::ARROW_DOWN->value, $state, $history);
|
||||||
|
|
||||||
|
$afterCategory = $state->getCurrentCategory();
|
||||||
|
$afterIndex = $state->getSelectedCategory();
|
||||||
|
echo " After: Index $afterIndex => '{$afterCategory['name']}'\n";
|
||||||
|
echo " ✓ Navigation worked: " . ($beforeIndex !== $afterIndex ? "YES" : "NO") . "\n\n";
|
||||||
|
|
||||||
|
// Test 3: Arrow Up
|
||||||
|
echo "Test 3: Arrow UP\n";
|
||||||
|
$beforeCategory = $state->getCurrentCategory();
|
||||||
|
$beforeIndex = $state->getSelectedCategory();
|
||||||
|
echo " Before: Index $beforeIndex => '{$beforeCategory['name']}'\n";
|
||||||
|
|
||||||
|
$inputHandler->handleInput(TuiKeyCode::ARROW_UP->value, $state, $history);
|
||||||
|
|
||||||
|
$afterCategory = $state->getCurrentCategory();
|
||||||
|
$afterIndex = $state->getSelectedCategory();
|
||||||
|
echo " After: Index $afterIndex => '{$afterCategory['name']}'\n";
|
||||||
|
echo " ✓ Navigation worked: " . ($beforeIndex !== $afterIndex ? "YES" : "NO") . "\n\n";
|
||||||
|
|
||||||
|
echo "✅ Real Navigation Test COMPLETED\n";
|
||||||
|
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
echo "\n❌ Real Navigation Test FAILED:\n";
|
||||||
|
echo "Error: " . $e->getMessage() . "\n";
|
||||||
|
echo "File: " . $e->getFile() . ":" . $e->getLine() . "\n";
|
||||||
|
echo "\nStack trace:\n" . $e->getTraceAsString() . "\n";
|
||||||
|
}
|
||||||
79
scripts/test/test_route_discovery.php
Normal file
79
scripts/test/test_route_discovery.php
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
use App\Framework\Discovery\UnifiedDiscoveryService;
|
||||||
|
use App\Framework\Discovery\ValueObjects\DiscoveryOptions;
|
||||||
|
use App\Framework\Cache\Cache;
|
||||||
|
use App\Framework\Cache\CacheKey;
|
||||||
|
use App\Framework\Cache\CacheItem;
|
||||||
|
use App\Framework\Attributes\Route;
|
||||||
|
|
||||||
|
// Simple null cache for testing
|
||||||
|
$cache = new class implements Cache {
|
||||||
|
public function get(CacheKey $key): ?CacheItem { return null; }
|
||||||
|
public function set(CacheItem ...$items): bool { return true; }
|
||||||
|
public function delete(CacheKey ...$keys): bool { return true; }
|
||||||
|
public function clear(): bool { return true; }
|
||||||
|
public function has(CacheKey $key): bool { return false; }
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
// Create discovery service
|
||||||
|
$discoveryService = new UnifiedDiscoveryService($cache);
|
||||||
|
|
||||||
|
// Perform discovery
|
||||||
|
$options = new DiscoveryOptions(
|
||||||
|
paths: [__DIR__ . '/src'],
|
||||||
|
cachingEnabled: false, // Disable caching for test
|
||||||
|
attributeTypes: [Route::class]
|
||||||
|
);
|
||||||
|
|
||||||
|
$registry = $discoveryService->discover($options);
|
||||||
|
|
||||||
|
$routes = $registry->getByAttribute(Route::class);
|
||||||
|
|
||||||
|
echo "=== DISCOVERED ROUTES ===\n\n";
|
||||||
|
echo "Total routes found: " . count($routes) . "\n\n";
|
||||||
|
|
||||||
|
foreach ($routes as $discovered) {
|
||||||
|
$routeAttr = $discovered->attribute->newInstance();
|
||||||
|
$className = $discovered->className;
|
||||||
|
$methodName = $discovered->methodName ?? '__invoke';
|
||||||
|
|
||||||
|
echo "Route: {$routeAttr->method->value} {$routeAttr->path}\n";
|
||||||
|
echo " Class: {$className}\n";
|
||||||
|
echo " Method: {$methodName}\n";
|
||||||
|
|
||||||
|
// Check for Campaign routes specifically
|
||||||
|
if (str_contains($className, 'Campaign')) {
|
||||||
|
echo " ⭐ CAMPAIGN ROUTE\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specifically search for PreSaveCampaign
|
||||||
|
echo "\n=== SEARCHING FOR PreSaveCampaign ===\n";
|
||||||
|
$found = false;
|
||||||
|
foreach ($routes as $discovered) {
|
||||||
|
if (str_contains($discovered->className, 'PreSaveCampaign')) {
|
||||||
|
echo "✅ PreSaveCampaign FOUND!\n";
|
||||||
|
echo " Path: {$discovered->attribute->newInstance()->path}\n";
|
||||||
|
$found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$found) {
|
||||||
|
echo "❌ PreSaveCampaign NOT FOUND in discovery!\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
echo "❌ Error during discovery: " . $e->getMessage() . "\n";
|
||||||
|
echo "File: " . $e->getFile() . ":" . $e->getLine() . "\n";
|
||||||
|
echo "\nStack trace:\n";
|
||||||
|
echo $e->getTraceAsString() . "\n";
|
||||||
|
}
|
||||||
43
scripts/test/test_spotify_init.php
Normal file
43
scripts/test/test_spotify_init.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
use App\Framework\Core\ContainerBootstrapper;
|
||||||
|
use App\Framework\OAuth\Providers\SpotifyProvider;
|
||||||
|
use App\Framework\Config\Environment;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$envPath = __DIR__ . '/.env';
|
||||||
|
if (!file_exists($envPath)) {
|
||||||
|
throw new \RuntimeException(".env file not found at: {$envPath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$envVars = parse_ini_file($envPath);
|
||||||
|
if ($envVars === false) {
|
||||||
|
throw new \RuntimeException("Failed to parse .env file");
|
||||||
|
}
|
||||||
|
|
||||||
|
$env = new Environment($envVars);
|
||||||
|
|
||||||
|
echo "Environment loaded\n";
|
||||||
|
echo "SPOTIFY_CLIENT_ID: " . $env->get(\App\Framework\Config\EnvKey::fromString('SPOTIFY_CLIENT_ID'), 'NOT SET') . "\n";
|
||||||
|
echo "SPOTIFY_CLIENT_SECRET: " . $env->get(\App\Framework\Config\EnvKey::fromString('SPOTIFY_CLIENT_SECRET'), 'NOT SET') . "\n";
|
||||||
|
|
||||||
|
$containerBootstrapper = new ContainerBootstrapper($env);
|
||||||
|
$container = $containerBootstrapper->bootstrap();
|
||||||
|
|
||||||
|
echo "Container bootstrapped\n";
|
||||||
|
|
||||||
|
$spotifyProvider = $container->get(SpotifyProvider::class);
|
||||||
|
|
||||||
|
echo "✅ SpotifyProvider successfully resolved from container\n";
|
||||||
|
echo "Provider name: " . $spotifyProvider->getName() . "\n";
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
echo "❌ Error: " . $e->getMessage() . "\n";
|
||||||
|
echo "File: " . $e->getFile() . ":" . $e->getLine() . "\n";
|
||||||
|
echo "\nStack trace:\n";
|
||||||
|
echo $e->getTraceAsString() . "\n";
|
||||||
|
}
|
||||||
7
scripts/test/test_spotify_simple.php
Normal file
7
scripts/test/test_spotify_simple.php
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
echo "Testing Spotify environment variables:\n\n";
|
||||||
|
|
||||||
|
echo "SPOTIFY_CLIENT_ID: " . (getenv('SPOTIFY_CLIENT_ID') ?: 'NOT SET') . "\n";
|
||||||
|
echo "SPOTIFY_CLIENT_SECRET: " . (getenv('SPOTIFY_CLIENT_SECRET') ?: 'NOT SET') . "\n";
|
||||||
|
echo "SPOTIFY_REDIRECT_URI: " . (getenv('SPOTIFY_REDIRECT_URI') ?: 'NOT SET') . "\n";
|
||||||
140
scripts/test/test_tui_complete.php
Normal file
140
scripts/test/test_tui_complete.php
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
// Test TUI complete functionality
|
||||||
|
use App\Framework\Console\Components\TuiState;
|
||||||
|
use App\Framework\Console\Components\TuiInputHandler;
|
||||||
|
use App\Framework\Console\Components\TuiCommandExecutor;
|
||||||
|
use App\Framework\Console\Components\TuiRenderer;
|
||||||
|
use App\Framework\Console\CommandHistory;
|
||||||
|
use App\Framework\Console\TuiView;
|
||||||
|
use App\Framework\Console\TuiKeyCode;
|
||||||
|
use App\Framework\Console\ConsoleOutput;
|
||||||
|
use App\Framework\Console\Screen\ScreenManager;
|
||||||
|
|
||||||
|
echo "Testing Complete TUI Functionality...\n";
|
||||||
|
echo "=====================================\n\n";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create mock executor
|
||||||
|
$mockExecutor = new class implements \App\Framework\Console\Components\TuiCommandExecutor {
|
||||||
|
public function executeSelectedCommand(object $command): void {
|
||||||
|
echo "Mock: Execute command\n";
|
||||||
|
}
|
||||||
|
public function executeCommand(string $commandName): void {
|
||||||
|
echo "Mock: Execute command: $commandName\n";
|
||||||
|
}
|
||||||
|
public function validateSelectedCommand(object $command): void {
|
||||||
|
echo "Mock: Validate command\n";
|
||||||
|
}
|
||||||
|
public function validateCommand(string $commandName): void {
|
||||||
|
echo "Mock: Validate command: $commandName\n";
|
||||||
|
}
|
||||||
|
public function showSelectedCommandHelp(object $command): void {
|
||||||
|
echo "Mock: Show help\n";
|
||||||
|
}
|
||||||
|
public function showCommandHelp(string $commandName): void {
|
||||||
|
echo "Mock: Show help: $commandName\n";
|
||||||
|
}
|
||||||
|
public function showAllCommandsHelp(): void {
|
||||||
|
echo "Mock: Show all commands help\n";
|
||||||
|
}
|
||||||
|
public function startInteractiveForm(object $command, \App\Framework\Console\Components\TuiState $state): void {
|
||||||
|
echo "Mock: Start form\n";
|
||||||
|
}
|
||||||
|
public function findCommandObject(string $commandName): ?object {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create mock output and screen manager
|
||||||
|
$screenManager = new ScreenManager(new ConsoleOutput());
|
||||||
|
$output = new ConsoleOutput();
|
||||||
|
$output->screen = $screenManager;
|
||||||
|
|
||||||
|
$state = new TuiState();
|
||||||
|
$history = new CommandHistory();
|
||||||
|
$inputHandler = new TuiInputHandler($mockExecutor);
|
||||||
|
$renderer = new TuiRenderer($output);
|
||||||
|
|
||||||
|
// Setup test categories
|
||||||
|
$categories = [
|
||||||
|
'Database' => [
|
||||||
|
'name' => 'Database',
|
||||||
|
'description' => 'Database commands',
|
||||||
|
'icon' => '🗄️',
|
||||||
|
'commands' => []
|
||||||
|
],
|
||||||
|
'Cache' => [
|
||||||
|
'name' => 'Cache',
|
||||||
|
'description' => 'Cache commands',
|
||||||
|
'icon' => '⚡',
|
||||||
|
'commands' => []
|
||||||
|
],
|
||||||
|
'Testing' => [
|
||||||
|
'name' => 'Testing',
|
||||||
|
'description' => 'Testing commands',
|
||||||
|
'icon' => '🧪',
|
||||||
|
'commands' => []
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$state->setCategories($categories);
|
||||||
|
$state->setCurrentView(TuiView::CATEGORIES);
|
||||||
|
$state->setRunning(true);
|
||||||
|
|
||||||
|
echo "✅ Initial Setup Complete\n";
|
||||||
|
echo "Categories loaded: " . count($categories) . "\n";
|
||||||
|
echo "Current view: " . $state->getCurrentView()->name . "\n";
|
||||||
|
echo "Selected category: " . $state->getSelectedCategory() . "\n\n";
|
||||||
|
|
||||||
|
// Test Arrow Down Navigation
|
||||||
|
echo "Testing Arrow DOWN navigation:\n";
|
||||||
|
echo "Before: Category " . $state->getSelectedCategory() . " ('{$categories[array_keys($categories)[$state->getSelectedCategory()]]['name']}')\n";
|
||||||
|
|
||||||
|
$inputHandler->handleInput(TuiKeyCode::ARROW_DOWN->value, $state, $history);
|
||||||
|
|
||||||
|
echo "After Arrow DOWN: Category " . $state->getSelectedCategory() . " ('{$categories[array_keys($categories)[$state->getSelectedCategory()]]['name']}')\n\n";
|
||||||
|
|
||||||
|
// Test Arrow Up Navigation
|
||||||
|
echo "Testing Arrow UP navigation:\n";
|
||||||
|
echo "Before: Category " . $state->getSelectedCategory() . " ('{$categories[array_keys($categories)[$state->getSelectedCategory()]]['name']}')\n";
|
||||||
|
|
||||||
|
$inputHandler->handleInput(TuiKeyCode::ARROW_UP->value, $state, $history);
|
||||||
|
|
||||||
|
echo "After Arrow UP: Category " . $state->getSelectedCategory() . " ('{$categories[array_keys($categories)[$state->getSelectedCategory()]]['name']}')\n\n";
|
||||||
|
|
||||||
|
// Test bounds checking - try to go below 0
|
||||||
|
echo "Testing boundary protection (going below 0):\n";
|
||||||
|
|
||||||
|
$inputHandler->handleInput(TuiKeyCode::ARROW_UP->value, $state, $history);
|
||||||
|
|
||||||
|
echo "After Arrow UP (should stay at 0): Category " . $state->getSelectedCategory() . " ('{$categories[array_keys($categories)[$state->getSelectedCategory()]]['name']}')\n\n";
|
||||||
|
|
||||||
|
// Test bounds checking - try to go above max
|
||||||
|
echo "Testing boundary protection (going above max):\n";
|
||||||
|
|
||||||
|
// Go to last item
|
||||||
|
$state->setSelectedCategory(2); // Testing category
|
||||||
|
echo "Set to last category (2): '{$categories[array_keys($categories)[$state->getSelectedCategory()]]['name']}'\n";
|
||||||
|
|
||||||
|
$inputHandler->handleInput(TuiKeyCode::ARROW_DOWN->value, $state, $history);
|
||||||
|
|
||||||
|
echo "After Arrow DOWN (should stay at 2): Category " . $state->getSelectedCategory() . " ('{$categories[array_keys($categories)[$state->getSelectedCategory()]]['name']}')\n\n";
|
||||||
|
|
||||||
|
// Test rendering with actual state
|
||||||
|
echo "Testing TUI Rendering:\n";
|
||||||
|
echo "=====================\n";
|
||||||
|
|
||||||
|
$state->setSelectedCategory(0);
|
||||||
|
$renderer->render($state, $history);
|
||||||
|
|
||||||
|
echo "\n✅ TUI Complete Test PASSED - All functionality works!\n";
|
||||||
|
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
echo "\n❌ TUI Complete Test FAILED:\n";
|
||||||
|
echo "Error: " . $e->getMessage() . "\n";
|
||||||
|
echo "File: " . $e->getFile() . ":" . $e->getLine() . "\n";
|
||||||
|
echo "\nStack trace:\n" . $e->getTraceAsString() . "\n";
|
||||||
|
}
|
||||||
95
scripts/test/test_tui_navigation.php
Normal file
95
scripts/test/test_tui_navigation.php
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
// Test TUI navigation functionality
|
||||||
|
use App\Framework\Console\Components\TuiState;
|
||||||
|
use App\Framework\Console\Components\TuiInputHandler;
|
||||||
|
use App\Framework\Console\Components\TuiCommandExecutor;
|
||||||
|
use App\Framework\Console\CommandHistory;
|
||||||
|
use App\Framework\Console\TuiView;
|
||||||
|
use App\Framework\Console\TuiKeyCode;
|
||||||
|
|
||||||
|
echo "Testing TUI Navigation...\n";
|
||||||
|
echo "=========================\n\n";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create mock executor
|
||||||
|
$mockExecutor = new class implements \App\Framework\Console\Components\TuiCommandExecutor {
|
||||||
|
public function executeSelectedCommand(object $command): void {
|
||||||
|
echo "Mock: Execute command\n";
|
||||||
|
}
|
||||||
|
public function executeCommand(string $commandName): void {
|
||||||
|
echo "Mock: Execute command: $commandName\n";
|
||||||
|
}
|
||||||
|
public function validateSelectedCommand(object $command): void {
|
||||||
|
echo "Mock: Validate command\n";
|
||||||
|
}
|
||||||
|
public function validateCommand(string $commandName): void {
|
||||||
|
echo "Mock: Validate command: $commandName\n";
|
||||||
|
}
|
||||||
|
public function showSelectedCommandHelp(object $command): void {
|
||||||
|
echo "Mock: Show help\n";
|
||||||
|
}
|
||||||
|
public function showCommandHelp(string $commandName): void {
|
||||||
|
echo "Mock: Show help: $commandName\n";
|
||||||
|
}
|
||||||
|
public function showAllCommandsHelp(): void {
|
||||||
|
echo "Mock: Show all commands help\n";
|
||||||
|
}
|
||||||
|
public function startInteractiveForm(object $command, \App\Framework\Console\Components\TuiState $state): void {
|
||||||
|
echo "Mock: Start form\n";
|
||||||
|
}
|
||||||
|
public function findCommandObject(string $commandName): ?object {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$state = new TuiState();
|
||||||
|
$history = new CommandHistory();
|
||||||
|
$inputHandler = new TuiInputHandler($mockExecutor);
|
||||||
|
|
||||||
|
// Setup test categories
|
||||||
|
$categories = [
|
||||||
|
'Database' => [
|
||||||
|
'name' => 'Database',
|
||||||
|
'description' => 'Database commands',
|
||||||
|
'icon' => '🗄️',
|
||||||
|
'commands' => []
|
||||||
|
],
|
||||||
|
'Cache' => [
|
||||||
|
'name' => 'Cache',
|
||||||
|
'description' => 'Cache commands',
|
||||||
|
'icon' => '⚡',
|
||||||
|
'commands' => []
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$state->setCategories($categories);
|
||||||
|
$state->setCurrentView(TuiView::CATEGORIES);
|
||||||
|
$state->setRunning(true);
|
||||||
|
|
||||||
|
// Test arrow navigation
|
||||||
|
echo "Testing arrow key navigation:\n";
|
||||||
|
echo "Initial category: " . $state->getSelectedCategory() . "\n";
|
||||||
|
|
||||||
|
// Test arrow down
|
||||||
|
$inputHandler->handleInput(TuiKeyCode::ARROW_DOWN->value, $state, $history);
|
||||||
|
echo "After arrow down: " . $state->getSelectedCategory() . "\n";
|
||||||
|
|
||||||
|
// Test arrow up
|
||||||
|
$inputHandler->handleInput(TuiKeyCode::ARROW_UP->value, $state, $history);
|
||||||
|
echo "After arrow up: " . $state->getSelectedCategory() . "\n";
|
||||||
|
|
||||||
|
// Test bounds checking - try to go below 0
|
||||||
|
$inputHandler->handleInput(TuiKeyCode::ARROW_UP->value, $state, $history);
|
||||||
|
echo "After arrow up (should stay at 0): " . $state->getSelectedCategory() . "\n";
|
||||||
|
|
||||||
|
echo "\n✅ TUI Navigation Test PASSED - Arrow key handling works!\n";
|
||||||
|
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
echo "\n❌ TUI Navigation Test FAILED:\n";
|
||||||
|
echo "Error: " . $e->getMessage() . "\n";
|
||||||
|
echo "File: " . $e->getFile() . ":" . $e->getLine() . "\n";
|
||||||
|
echo "\nStack trace:\n" . $e->getTraceAsString() . "\n";
|
||||||
|
}
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title><?= $title ?></title>
|
|
||||||
<link rel="stylesheet" href="/css/admin.css">
|
|
||||||
</head>
|
|
||||||
<body class="admin-page">
|
|
||||||
<div class="admin-header">
|
|
||||||
<h1>Framework Admin Dashboard</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="admin-nav">
|
|
||||||
<a href="/admin" class="active">Dashboard</a>
|
|
||||||
<a href="/admin/routes">Routen</a>
|
|
||||||
<a href="/admin/services">Dienste</a>
|
|
||||||
<a href="/admin/environment">Umgebung</a>
|
|
||||||
<a href="/admin/performance">Performance</a>
|
|
||||||
<a href="/admin/redis">Redis</a>
|
|
||||||
<a href="/admin/phpinfo">PHP Info</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="admin-content">
|
|
||||||
<div class="dashboard-stats">
|
|
||||||
<div class="stat-box">
|
|
||||||
<h3>Framework Version</h3>
|
|
||||||
<div class="stat-value"><?= $stats['frameworkVersion'] ?></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-box">
|
|
||||||
<h3>PHP Version</h3>
|
|
||||||
<div class="stat-value"><?= $stats['phpVersion'] ?></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-box">
|
|
||||||
<h3>Speicherverbrauch</h3>
|
|
||||||
<div class="stat-value"><?= $stats['memoryUsage'] ?></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-box">
|
|
||||||
<h3>Max. Speicherverbrauch</h3>
|
|
||||||
<div class="stat-value"><?= $stats['peakMemoryUsage'] ?></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-box">
|
|
||||||
<h3>Server</h3>
|
|
||||||
<div class="stat-value"><?= $stats['serverInfo'] ?></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-box">
|
|
||||||
<h3>Serverzeit</h3>
|
|
||||||
<div class="stat-value"><?= $stats['serverTime'] ?></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-box">
|
|
||||||
<h3>Zeitzone</h3>
|
|
||||||
<div class="stat-value"><?= $stats['timezone'] ?></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-box">
|
|
||||||
<h3>Betriebssystem</h3>
|
|
||||||
<div class="stat-value"><?= $stats['operatingSystem'] ?></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-box">
|
|
||||||
<h3>Server Uptime</h3>
|
|
||||||
<div class="stat-value"><?= $stats['uptime'] ?></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-box">
|
|
||||||
<h3>Aktive Sessions</h3>
|
|
||||||
<div class="stat-value"><?= $stats['sessionCount'] ?></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-box">
|
|
||||||
<h3>Registrierte Dienste</h3>
|
|
||||||
<div class="stat-value"><?= $stats['servicesCount'] ?></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="admin-section">
|
|
||||||
<h2>PHP Erweiterungen</h2>
|
|
||||||
<div class="extensions-list">
|
|
||||||
<?php foreach ($stats['loadedExtensions'] as $extension): ?>
|
|
||||||
<span class="extension-badge"><?= $extension ?></span>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="admin-footer">
|
|
||||||
<p>© <?= date('Y') ?> Framework Admin</p>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>{{ title }}</title>
|
|
||||||
<link rel="stylesheet" href="/css/admin.css">
|
|
||||||
</head>
|
|
||||||
<body class="admin-page">
|
|
||||||
<div class="admin-header">
|
|
||||||
<h1>Umgebungsvariablen</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="admin-nav">
|
|
||||||
<a href="/admin">Dashboard</a>
|
|
||||||
<a href="/admin/routes">Routen</a>
|
|
||||||
<a href="/admin/services">Dienste</a>
|
|
||||||
<a href="/admin/environment" class="active">Umgebung</a>
|
|
||||||
<a href="/admin/performance">Performance</a>
|
|
||||||
<a href="/admin/redis">Redis</a>
|
|
||||||
<a href="/admin/phpinfo">PHP Info</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="admin-content">
|
|
||||||
<div class="admin-tools">
|
|
||||||
<input type="text" id="envFilter" placeholder="Variablen filtern..." class="search-input">
|
|
||||||
<div class="filter-tags">
|
|
||||||
<button class="filter-tag" data-prefix="APP_">APP_</button>
|
|
||||||
<button class="filter-tag" data-prefix="DB_">DB_</button>
|
|
||||||
<button class="filter-tag" data-prefix="REDIS_">REDIS_</button>
|
|
||||||
<button class="filter-tag" data-prefix="RATE_LIMIT_">RATE_LIMIT_</button>
|
|
||||||
<button class="filter-tag" data-prefix="PHP_">PHP_</button>
|
|
||||||
<button class="filter-tag active" data-prefix="">Alle</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<table class="admin-table" id="envTable">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Variable</th>
|
|
||||||
<th>Wert</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<for var="envVar" in="env">
|
|
||||||
<tr>
|
|
||||||
<td>{{ envVar.key }}</td>
|
|
||||||
<td>{{ envVar.value }}</td>
|
|
||||||
</tr>
|
|
||||||
</for>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="admin-footer">
|
|
||||||
<p>© {{ current_year }} Framework Admin</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Filterung der Umgebungsvariablen
|
|
||||||
document.getElementById('envFilter').addEventListener('input', filterTable);
|
|
||||||
|
|
||||||
// Tag-Filter
|
|
||||||
document.querySelectorAll('.filter-tag').forEach(tag => {
|
|
||||||
tag.addEventListener('click', function() {
|
|
||||||
document.querySelectorAll('.filter-tag').forEach(t => t.classList.remove('active'));
|
|
||||||
this.classList.add('active');
|
|
||||||
|
|
||||||
const prefix = this.getAttribute('data-prefix');
|
|
||||||
document.getElementById('envFilter').value = prefix;
|
|
||||||
filterTable();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function filterTable() {
|
|
||||||
const filterValue = document.getElementById('envFilter').value.toLowerCase();
|
|
||||||
const rows = document.querySelectorAll('#envTable tbody tr');
|
|
||||||
|
|
||||||
rows.forEach(row => {
|
|
||||||
const key = row.cells[0].textContent.toLowerCase();
|
|
||||||
row.style.display = key.includes(filterValue) ? '' : 'none';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,250 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Image Manager</title>
|
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div class="container-fluid">
|
|
||||||
<h1>Image Manager</h1>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<!-- Image Slots Section -->
|
|
||||||
<div class="col-md-6">
|
|
||||||
<h2>Image Slots</h2>
|
|
||||||
<div id="image-slots" class="list-group">
|
|
||||||
<for var="slot" in="slots">
|
|
||||||
<div class="list-group-item slot-item" data-slot-id="{{ slot.id }}">
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
|
||||||
<div>
|
|
||||||
<h5>{{ slot.slotName }}</h5>
|
|
||||||
<small class="text-muted">ID: {{ slot.id }}</small>
|
|
||||||
</div>
|
|
||||||
<div class="slot-image-container" style="width: 100px; height: 100px;">
|
|
||||||
<div class="border border-dashed d-flex align-items-center justify-content-center h-100"
|
|
||||||
ondrop="handleDrop(event, '{{ slot.id }}')"
|
|
||||||
ondragover="handleDragOver(event)"
|
|
||||||
ondragleave="handleDragLeave(event)">
|
|
||||||
<span class="text-muted">Drop image here or click to select</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</for>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Available Images Section -->
|
|
||||||
<div class="col-md-6">
|
|
||||||
<h2>Available Images</h2>
|
|
||||||
|
|
||||||
<!-- Search Bar -->
|
|
||||||
<div class="mb-3">
|
|
||||||
<input type="text"
|
|
||||||
id="image-search"
|
|
||||||
class="form-control"
|
|
||||||
placeholder="Search images..."
|
|
||||||
onkeyup="searchImages()">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Images Grid -->
|
|
||||||
<div id="images-grid" class="row g-2">
|
|
||||||
<for var="image" in="images">
|
|
||||||
<div class="col-md-4 image-item"
|
|
||||||
data-filename="{{ image.originalFilename }}"
|
|
||||||
data-alt="{{ image.altText }}">
|
|
||||||
<div class="card">
|
|
||||||
<img src="/media/images/{{ image.path }}"
|
|
||||||
alt="{{ image.altText }}"
|
|
||||||
class="card-img-top"
|
|
||||||
style="height: 150px; object-fit: cover; cursor: move;"
|
|
||||||
draggable="true"
|
|
||||||
ondragstart="handleDragStart(event, '{{ image.ulid }}')"
|
|
||||||
onclick="selectImage('{{ image.ulid }}')">
|
|
||||||
<div class="card-body p-2">
|
|
||||||
<small class="text-truncate d-block">
|
|
||||||
{{ image.originalFilename }}
|
|
||||||
</small>
|
|
||||||
<small class="text-muted">
|
|
||||||
{{ image.width }}x{{ image.height }} • {{ image.fileSize }}KB
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</for>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Image Selection Modal -->
|
|
||||||
<div class="modal fade" id="imageSelectModal" tabindex="-1">
|
|
||||||
<div class="modal-dialog modal-lg">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">Select Image for <span id="modal-slot-name"></span></h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div id="modal-images" class="row g-2">
|
|
||||||
<!-- Images will be loaded here -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Current dragging image
|
|
||||||
let draggedImageUlid = null;
|
|
||||||
let selectedSlotId = null;
|
|
||||||
|
|
||||||
// Handle drag start
|
|
||||||
function handleDragStart(event, imageUlid) {
|
|
||||||
draggedImageUlid = imageUlid;
|
|
||||||
event.dataTransfer.effectAllowed = 'copy';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle drag over
|
|
||||||
function handleDragOver(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.currentTarget.classList.add('bg-light');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle drag leave
|
|
||||||
function handleDragLeave(event) {
|
|
||||||
event.currentTarget.classList.remove('bg-light');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle drop
|
|
||||||
async function handleDrop(event, slotId) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.currentTarget.classList.remove('bg-light');
|
|
||||||
|
|
||||||
if (draggedImageUlid) {
|
|
||||||
await assignImageToSlot(slotId, draggedImageUlid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assign image to slot
|
|
||||||
async function assignImageToSlot(slotId, imageUlid) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`/api/image-slots/${slotId}/image`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ image_ulid: imageUlid })
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
location.reload(); // Simple reload for now
|
|
||||||
} else {
|
|
||||||
alert('Failed to assign image');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error:', error);
|
|
||||||
alert('Error assigning image');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove image from slot
|
|
||||||
async function removeImage(slotId) {
|
|
||||||
if (!confirm('Remove image from this slot?')) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`/api/image-slots/${slotId}/image`, {
|
|
||||||
method: 'DELETE'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
location.reload();
|
|
||||||
} else {
|
|
||||||
alert('Failed to remove image');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error:', error);
|
|
||||||
alert('Error removing image');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select image (click handler)
|
|
||||||
function selectImage(imageUlid) {
|
|
||||||
// Find which slot was clicked if any
|
|
||||||
const clickedSlot = document.querySelector('.slot-item.selecting');
|
|
||||||
if (clickedSlot) {
|
|
||||||
const slotId = clickedSlot.dataset.slotId;
|
|
||||||
assignImageToSlot(slotId, imageUlid);
|
|
||||||
clickedSlot.classList.remove('selecting');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search images
|
|
||||||
function searchImages() {
|
|
||||||
const searchTerm = document.getElementById('image-search').value.toLowerCase();
|
|
||||||
const imageItems = document.querySelectorAll('.image-item');
|
|
||||||
|
|
||||||
imageItems.forEach(item => {
|
|
||||||
const filename = item.dataset.filename.toLowerCase();
|
|
||||||
const alt = item.dataset.alt.toLowerCase();
|
|
||||||
|
|
||||||
if (filename.includes(searchTerm) || alt.includes(searchTerm)) {
|
|
||||||
item.style.display = '';
|
|
||||||
} else {
|
|
||||||
item.style.display = 'none';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add click handler to slots for selection mode
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
document.querySelectorAll('.slot-item').forEach(slot => {
|
|
||||||
const container = slot.querySelector('.slot-image-container');
|
|
||||||
if (container && !container.querySelector('img')) {
|
|
||||||
container.style.cursor = 'pointer';
|
|
||||||
container.addEventListener('click', function() {
|
|
||||||
// Remove previous selection
|
|
||||||
document.querySelectorAll('.slot-item').forEach(s => s.classList.remove('selecting'));
|
|
||||||
// Mark as selecting
|
|
||||||
slot.classList.add('selecting');
|
|
||||||
// Highlight available images
|
|
||||||
document.getElementById('images-grid').classList.add('selecting-mode');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add some CSS
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.textContent = `
|
|
||||||
.slot-item.selecting {
|
|
||||||
border: 2px solid #0d6efd;
|
|
||||||
background-color: #e7f1ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selecting-mode .card {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selecting-mode .card:hover {
|
|
||||||
border-color: #0d6efd;
|
|
||||||
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
.border-dashed {
|
|
||||||
border-style: dashed !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.object-fit-cover {
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(style);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>{{ title }}</title>
|
|
||||||
<link rel="stylesheet" href="/css/admin.css">
|
|
||||||
</head>
|
|
||||||
<body class="admin-page">
|
|
||||||
<div class="admin-header">
|
|
||||||
<h1>Performance-Übersicht</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="admin-nav">
|
|
||||||
<a href="/admin">Dashboard</a>
|
|
||||||
<a href="/admin/routes">Routen</a>
|
|
||||||
<a href="/admin/services">Dienste</a>
|
|
||||||
<a href="/admin/environment">Umgebung</a>
|
|
||||||
<a href="/admin/performance" class="active">Performance</a>
|
|
||||||
<a href="/admin/redis">Redis</a>
|
|
||||||
<a href="/admin/phpinfo">PHP Info</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="admin-content">
|
|
||||||
<div class="dashboard-stats">
|
|
||||||
<div class="stat-box">
|
|
||||||
<h3>Aktueller Speicherverbrauch</h3>
|
|
||||||
<div class="stat-value">{{ performance.currentMemoryUsage }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-box">
|
|
||||||
<h3>Maximaler Speicherverbrauch</h3>
|
|
||||||
<div class="stat-value">{{ performance.peakMemoryUsage }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-box">
|
|
||||||
<h3>Speicherlimit</h3>
|
|
||||||
<div class="stat-value">{{ performance.memoryLimit }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-box">
|
|
||||||
<h3>Speicherauslastung</h3>
|
|
||||||
<div class="stat-value">
|
|
||||||
<div class="progress-bar">
|
|
||||||
<div class="progress" style="width: {{ performance.memoryUsagePercentage }}%"></div>
|
|
||||||
</div>
|
|
||||||
<div class="progress-value">{{ performance.memoryUsagePercentage }}%</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-box">
|
|
||||||
<h3>Systemlast (1/5/15 min)</h3>
|
|
||||||
<div class="stat-value">
|
|
||||||
{{ performance.loadAverage.0 }} /
|
|
||||||
{{ performance.loadAverage.1 }} /
|
|
||||||
{{ performance.loadAverage.2 }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-box">
|
|
||||||
<h3>OPCache aktiviert</h3>
|
|
||||||
<div class="stat-value">{{ performance.opcacheEnabled }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div if="performance.opcacheMemoryUsage">
|
|
||||||
<div class="stat-box">
|
|
||||||
<h3>OPCache Speicherverbrauch</h3>
|
|
||||||
<div class="stat-value">{{ performance.opcacheMemoryUsage }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-box">
|
|
||||||
<h3>OPCache Cache Hits</h3>
|
|
||||||
<div class="stat-value">{{ performance.opcacheCacheHits }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-box">
|
|
||||||
<h3>OPCache Miss Rate</h3>
|
|
||||||
<div class="stat-value">{{ performance.opcacheMissRate }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-box">
|
|
||||||
<h3>Ausführungszeit</h3>
|
|
||||||
<div class="stat-value">{{ performance.executionTime }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-box">
|
|
||||||
<h3>Geladene Dateien</h3>
|
|
||||||
<div class="stat-value">{{ performance.includedFiles }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="admin-section">
|
|
||||||
<h2>Geladene Dateien</h2>
|
|
||||||
<div class="admin-tools">
|
|
||||||
<input type="text" id="fileFilter" placeholder="Dateien filtern..." class="search-input">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="file-list" id="fileList">
|
|
||||||
<for var="file" in="performance.files">
|
|
||||||
<div class="file-item">
|
|
||||||
{{ file }}
|
|
||||||
</div>
|
|
||||||
</for>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="admin-footer">
|
|
||||||
<p>© {{ current_year }} Framework Admin</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
document.getElementById('fileFilter').addEventListener('input', function() {
|
|
||||||
const filterValue = this.value.toLowerCase();
|
|
||||||
const items = document.querySelectorAll('#fileList .file-item');
|
|
||||||
|
|
||||||
items.forEach(item => {
|
|
||||||
const text = item.textContent.toLowerCase();
|
|
||||||
item.style.display = text.includes(filterValue) ? '' : 'none';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>{{ title }}</title>
|
|
||||||
<link rel="stylesheet" href="/css/admin.css">
|
|
||||||
</head>
|
|
||||||
<body class="admin-page">
|
|
||||||
<div class="admin-header">
|
|
||||||
<h1>Redis-Informationen</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="admin-nav">
|
|
||||||
<a href="/admin">Dashboard</a>
|
|
||||||
<a href="/admin/routes">Routen</a>
|
|
||||||
<a href="/admin/services">Dienste</a>
|
|
||||||
<a href="/admin/environment">Umgebung</a>
|
|
||||||
<a href="/admin/performance">Performance</a>
|
|
||||||
<a href="/admin/redis" class="active">Redis</a>
|
|
||||||
<a href="/admin/phpinfo">PHP Info</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="admin-content">
|
|
||||||
<div class="dashboard-stats">
|
|
||||||
<div class="stat-box">
|
|
||||||
<h3>Status</h3>
|
|
||||||
<div class="stat-value status-connected">{{ redis.status }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-box">
|
|
||||||
<h3>Version</h3>
|
|
||||||
<div class="stat-value">{{ redis.version }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-box">
|
|
||||||
<h3>Uptime</h3>
|
|
||||||
<div class="stat-value">{{ redis.uptime }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-box">
|
|
||||||
<h3>Speicherverbrauch</h3>
|
|
||||||
<div class="stat-value">{{ redis.memory }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-box">
|
|
||||||
<h3>Max. Speicherverbrauch</h3>
|
|
||||||
<div class="stat-value">{{ redis.peak_memory }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-box">
|
|
||||||
<h3>Verbundene Clients</h3>
|
|
||||||
<div class="stat-value">{{ redis.clients }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-box">
|
|
||||||
<h3>Anzahl Schlüssel</h3>
|
|
||||||
<div class="stat-value">{{ redis.keys }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="admin-section">
|
|
||||||
<h2>Schlüssel (max. 50 angezeigt)</h2>
|
|
||||||
<div class="admin-tools">
|
|
||||||
<input type="text" id="keyFilter" placeholder="Schlüssel filtern..." class="search-input">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="key-list" id="keyList">
|
|
||||||
<for var="key" in="redis.key_sample">
|
|
||||||
<div class="key-item">
|
|
||||||
{{ key }}
|
|
||||||
</div>
|
|
||||||
</for>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="admin-footer">
|
|
||||||
<p>© {{ current_year }} Framework Admin</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
document.getElementById('keyFilter')?.addEventListener('input', function() {
|
|
||||||
const filterValue = this.value.toLowerCase();
|
|
||||||
const items = document.querySelectorAll('#keyList .key-item');
|
|
||||||
|
|
||||||
items.forEach(item => {
|
|
||||||
const text = item.textContent.toLowerCase();
|
|
||||||
item.style.display = text.includes(filterValue) ? '' : 'none';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title><?= $title ?></title>
|
|
||||||
<link rel="stylesheet" href="/css/admin.css">
|
|
||||||
</head>
|
|
||||||
<body class="admin-page">
|
|
||||||
<div class="admin-header">
|
|
||||||
<h1>Routen-Übersicht</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="admin-nav">
|
|
||||||
<a href="/admin">Dashboard</a>
|
|
||||||
<a href="/admin/routes" class="active">Routen</a>
|
|
||||||
<a href="/admin/services">Dienste</a>
|
|
||||||
<a href="/admin/environment">Umgebung</a>
|
|
||||||
<a href="/admin/performance">Performance</a>
|
|
||||||
<a href="/admin/redis">Redis</a>
|
|
||||||
<a href="/admin/phpinfo">PHP Info</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="admin-content">
|
|
||||||
<div class="admin-tools">
|
|
||||||
<input type="text" id="routeFilter" placeholder="Routen filtern..." class="search-input">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<table class="admin-table" id="routesTable">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Pfad</th>
|
|
||||||
<th>Methode</th>
|
|
||||||
<th>Controller</th>
|
|
||||||
<th>Aktion</th>
|
|
||||||
<th>Name</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($routes as $route): ?>
|
|
||||||
<tr>
|
|
||||||
<td><?= $route->path ?></td>
|
|
||||||
<td><?= $route->method ?></td>
|
|
||||||
<td><?= $route->controllerClass ?></td>
|
|
||||||
<td><?= $route->methodName ?></td>
|
|
||||||
<td><?= $route->name ?? '-' ?></td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="admin-footer">
|
|
||||||
<p>© <?= date('Y') ?> Framework Admin</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
document.getElementById('routeFilter').addEventListener('input', function() {
|
|
||||||
const filterValue = this.value.toLowerCase();
|
|
||||||
const rows = document.querySelectorAll('#routesTable tbody tr');
|
|
||||||
|
|
||||||
rows.forEach(row => {
|
|
||||||
const text = row.textContent.toLowerCase();
|
|
||||||
row.style.display = text.includes(filterValue) ? '' : 'none';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>{{ title }}</title>
|
|
||||||
<link rel="stylesheet" href="/css/admin.css">
|
|
||||||
</head>
|
|
||||||
<body class="admin-page">
|
|
||||||
<div class="admin-header">
|
|
||||||
<h1>Registrierte Dienste</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="admin-nav">
|
|
||||||
<a href="/admin">Dashboard</a>
|
|
||||||
<a href="/admin/routes">Routen</a>
|
|
||||||
<a href="/admin/services" class="active">Dienste</a>
|
|
||||||
<a href="/admin/environment">Umgebung</a>
|
|
||||||
<a href="/admin/performance">Performance</a>
|
|
||||||
<a href="/admin/redis">Redis</a>
|
|
||||||
<a href="/admin/phpinfo">PHP Info</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="admin-content">
|
|
||||||
<div class="admin-tools">
|
|
||||||
<input type="text" id="serviceFilter" placeholder="Dienste filtern..." class="search-input">
|
|
||||||
<span class="services-count">{{ servicesCount }} Dienste insgesamt</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="service-list" id="serviceList">
|
|
||||||
<for var="service" in="services">
|
|
||||||
<div class="service-item">
|
|
||||||
<div class="service-name">{{ service.name }}</div>
|
|
||||||
<div class="service-category">
|
|
||||||
<span class="category-badge">{{ service.category }}</span>
|
|
||||||
<if condition="service.subCategory">
|
|
||||||
<span class="subcategory-badge">{{ service.subCategory }}</span>
|
|
||||||
</if>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</for>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="admin-footer">
|
|
||||||
<p>© {{ date('Y') }} Framework Admin</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
document.getElementById('serviceFilter').addEventListener('input', function() {
|
|
||||||
const filterValue = this.value.toLowerCase();
|
|
||||||
const items = document.querySelectorAll('#serviceList .service-item');
|
|
||||||
let visibleCount = 0;
|
|
||||||
|
|
||||||
items.forEach(item => {
|
|
||||||
const text = item.textContent.toLowerCase();
|
|
||||||
const isVisible = text.includes(filterValue);
|
|
||||||
item.style.display = isVisible ? '' : 'none';
|
|
||||||
if (isVisible) visibleCount++;
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelector('.services-count').textContent =
|
|
||||||
visibleCount + ' von ' + {{ servicesCount }} + ' Diensten';
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
183
src/Framework/DI/ContainerIntrospector.php
Normal file
183
src/Framework/DI/ContainerIntrospector.php
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Framework\DI;
|
||||||
|
|
||||||
|
use App\Framework\Core\ValueObjects\ClassName;
|
||||||
|
use App\Framework\Reflection\ReflectionProvider;
|
||||||
|
|
||||||
|
final readonly class ContainerIntrospector
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private Container $container,
|
||||||
|
private InstanceRegistry $instances,
|
||||||
|
private BindingRegistry $bindings,
|
||||||
|
private ReflectionProvider $reflectionProvider,
|
||||||
|
private \Closure $resolutionChainProvider
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string>
|
||||||
|
*/
|
||||||
|
public function listBindings(): array
|
||||||
|
{
|
||||||
|
return array_keys($this->bindings->getAllBindings());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBinding(string $abstract): callable|string|object|null
|
||||||
|
{
|
||||||
|
return $this->bindings->getBinding($abstract);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string>
|
||||||
|
*/
|
||||||
|
public function listSingletons(): array
|
||||||
|
{
|
||||||
|
return $this->instances->getSingletons();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string>
|
||||||
|
*/
|
||||||
|
public function listInstances(): array
|
||||||
|
{
|
||||||
|
return $this->instances->getInstanceKeys();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param class-string $class */
|
||||||
|
public function isSingleton(string $class): bool
|
||||||
|
{
|
||||||
|
return $this->instances->isMarkedAsSingleton($class) || $this->instances->hasSingleton($class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<class-string>
|
||||||
|
*/
|
||||||
|
public function getResolutionChain(): array
|
||||||
|
{
|
||||||
|
$f = $this->resolutionChainProvider;
|
||||||
|
|
||||||
|
/** @var array<class-string> $chain */
|
||||||
|
$chain = $f();
|
||||||
|
return $chain;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param class-string $class */
|
||||||
|
public function isInstantiable(string $class): bool
|
||||||
|
{
|
||||||
|
if ($class === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$className = ClassName::create($class);
|
||||||
|
if (! $className->exists()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return $this->reflectionProvider->getClass($className)->isInstantiable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describe resolution state and constructor parameters for diagnostics.
|
||||||
|
* @param class-string $class
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
public function describe(string $class): array
|
||||||
|
{
|
||||||
|
$className = ClassName::create($class);
|
||||||
|
$exists = $className->exists();
|
||||||
|
$hasBinding = $this->bindings->hasBinding($class);
|
||||||
|
$hasInstance = $this->instances->hasInstance($class) || $this->instances->hasSingleton($class);
|
||||||
|
$singletonMarked = $this->instances->isMarkedAsSingleton($class);
|
||||||
|
|
||||||
|
$instantiable = false;
|
||||||
|
$constructor = [
|
||||||
|
'has_constructor' => false,
|
||||||
|
'parameters' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
$binding = $this->bindings->getBinding($class);
|
||||||
|
$bindingType = null;
|
||||||
|
if ($binding !== null) {
|
||||||
|
$bindingType = is_callable($binding) ? 'callable' : (is_string($binding) ? 'string' : 'object');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($exists) {
|
||||||
|
try {
|
||||||
|
$reflection = $this->reflectionProvider->getClass($className);
|
||||||
|
$instantiable = $reflection->isInstantiable();
|
||||||
|
if ($reflection->hasMethod('__construct')) {
|
||||||
|
$ctor = $reflection->getConstructor();
|
||||||
|
if ($ctor !== null) {
|
||||||
|
$constructor['has_constructor'] = true;
|
||||||
|
foreach ($ctor->getParameters() as $param) {
|
||||||
|
$type = $param->getType();
|
||||||
|
$typeName = null;
|
||||||
|
$isBuiltin = false;
|
||||||
|
if ($type instanceof \ReflectionNamedType) {
|
||||||
|
$typeName = $type->getName();
|
||||||
|
$isBuiltin = $type->isBuiltin();
|
||||||
|
} elseif ($type !== null) {
|
||||||
|
// union or complex type - string cast
|
||||||
|
$typeName = (string) $type;
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolvable = true;
|
||||||
|
if ($typeName !== null && ! $isBuiltin) {
|
||||||
|
// best-effort check for class/interface
|
||||||
|
$resolvable = $this->container->has($typeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
$constructor['parameters'][] = [
|
||||||
|
'name' => $param->getName(),
|
||||||
|
'type' => $typeName,
|
||||||
|
'allows_null' => $type?->allowsNull() ?? true,
|
||||||
|
'is_builtin' => $isBuiltin,
|
||||||
|
'has_default' => $param->isDefaultValueAvailable(),
|
||||||
|
'resolvable' => $resolvable,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// Keep defaults if reflection fails, but include error message for diagnostics.
|
||||||
|
$constructor['error'] = $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$suggestions = [];
|
||||||
|
if (! $exists) {
|
||||||
|
$suggestions[] = 'Class does not exist - check namespace and autoloading.';
|
||||||
|
} elseif (! $instantiable && ! $hasBinding) {
|
||||||
|
$suggestions[] = 'Class is not instantiable - add a binding from interface/abstract to a concrete implementation.';
|
||||||
|
}
|
||||||
|
if (! $hasBinding && $instantiable && ($constructor['has_constructor'] ?? false)) {
|
||||||
|
foreach ($constructor['parameters'] as $p) {
|
||||||
|
if ($p['type'] && ! $p['is_builtin'] && ! $p['resolvable']) {
|
||||||
|
$suggestions[] = "Add binding for dependency '{$p['type']}' or ensure it is instantiable.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$chain = $this->getResolutionChain();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'class' => $class,
|
||||||
|
'exists' => $exists,
|
||||||
|
'instantiable' => $instantiable,
|
||||||
|
'has_binding' => $hasBinding,
|
||||||
|
'binding_type' => $bindingType,
|
||||||
|
'has_instance' => $hasInstance,
|
||||||
|
'singleton_marked' => $singletonMarked,
|
||||||
|
'constructor' => $constructor,
|
||||||
|
'resolution_chain' => $chain,
|
||||||
|
'counts' => [
|
||||||
|
'bindings' => count($this->bindings->getAllBindings()),
|
||||||
|
'singletons' => count($this->instances->getSingletons()),
|
||||||
|
'instances' => count($this->instances->getInstanceKeys()),
|
||||||
|
],
|
||||||
|
'suggestions' => array_values(array_unique($suggestions)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/Framework/Mcp/Tools/RouteInspectorTool.php
Normal file
37
src/Framework/Mcp/Tools/RouteInspectorTool.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Framework\Mcp\Tools;
|
||||||
|
|
||||||
|
use App\Framework\Mcp\McpTool;
|
||||||
|
use App\Framework\Router\CompiledRoutes;
|
||||||
|
use App\Framework\Router\RouteInspector;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP tool exposing routing sanity checks
|
||||||
|
*/
|
||||||
|
final readonly class RouteInspectorTool
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private CompiledRoutes $compiledRoutes
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[McpTool(
|
||||||
|
name: 'route_sanity_check',
|
||||||
|
description: 'Analyze compiled routes for common issues (missing controllers/actions, parameter mismatches, duplicates)'
|
||||||
|
)]
|
||||||
|
public function routeSanityCheck(): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$inspector = new RouteInspector($this->compiledRoutes);
|
||||||
|
|
||||||
|
return $inspector->analyze();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
211
src/Framework/Router/RouteInspector.php
Normal file
211
src/Framework/Router/RouteInspector.php
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Framework\Router;
|
||||||
|
|
||||||
|
use ReflectionClass;
|
||||||
|
use ReflectionMethod;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs sanity checks on compiled routes (controller/action presence, parameter consistency, etc.)
|
||||||
|
*/
|
||||||
|
final readonly class RouteInspector
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private CompiledRoutes $compiledRoutes
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analyze compiled routes and return structured diagnostics
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function analyze(): array
|
||||||
|
{
|
||||||
|
$issues = [];
|
||||||
|
$staticRoutes = $this->compiledRoutes->getStaticRoutes();
|
||||||
|
$namedRoutes = $this->compiledRoutes->getAllNamedRoutes();
|
||||||
|
|
||||||
|
$totalStatic = 0;
|
||||||
|
|
||||||
|
// Track seen routes for potential duplicates per method+subdomain+path
|
||||||
|
$seen = [];
|
||||||
|
|
||||||
|
foreach ($staticRoutes as $method => $subdomains) {
|
||||||
|
foreach ($subdomains as $subdomain => $paths) {
|
||||||
|
foreach ($paths as $path => $route) {
|
||||||
|
$totalStatic++;
|
||||||
|
$key = "{$method}|{$subdomain}|{$path}";
|
||||||
|
$seen[$key] = ($seen[$key] ?? 0) + 1;
|
||||||
|
|
||||||
|
$routeName = $route->name ?? null;
|
||||||
|
|
||||||
|
// Controller existence
|
||||||
|
$controller = $route->controller ?? null;
|
||||||
|
$action = $route->action ?? null;
|
||||||
|
|
||||||
|
if (!is_string($controller) || $controller === '' || !class_exists($controller)) {
|
||||||
|
$issues[] = $this->issue('controller_missing', 'error', $method, $subdomain, $path, $routeName, "Controller class not found or invalid: " . var_export($controller, true));
|
||||||
|
continue; // skip further checks for this route
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action existence and visibility
|
||||||
|
if (!is_string($action) || $action === '') {
|
||||||
|
$issues[] = $this->issue('action_missing', 'error', $method, $subdomain, $path, $routeName, 'Action method not defined or invalid');
|
||||||
|
} else {
|
||||||
|
$refClass = new ReflectionClass($controller);
|
||||||
|
if (!$refClass->hasMethod($action)) {
|
||||||
|
$issues[] = $this->issue('action_missing', 'error', $method, $subdomain, $path, $routeName, "Action method '{$action}' not found in {$controller}");
|
||||||
|
} else {
|
||||||
|
$refMethod = $refClass->getMethod($action);
|
||||||
|
if (!$refMethod->isPublic()) {
|
||||||
|
$issues[] = $this->issue('action_not_public', 'warning', $method, $subdomain, $path, $routeName, "Action method '{$action}' is not public");
|
||||||
|
}
|
||||||
|
// Parameter consistency check (placeholders vs method signature)
|
||||||
|
$this->checkParameterConsistency($issues, $method, $subdomain, $path, $routeName, $route, $refMethod);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duplicate path checks (should normally be prevented by map keys, but guard anyway)
|
||||||
|
foreach ($seen as $k => $count) {
|
||||||
|
if ($count > 1) {
|
||||||
|
[$m, $sub, $p] = explode('|', $k, 3);
|
||||||
|
$issues[] = $this->issue('duplicate_route', 'error', $m, $sub, $p, null, "Duplicate route detected for {$m} {$sub} {$p}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Named routes basic validation: ensure name -> route is consistent
|
||||||
|
$namedIssues = $this->validateNamedRoutes($namedRoutes);
|
||||||
|
array_push($issues, ...$namedIssues);
|
||||||
|
|
||||||
|
$summary = [
|
||||||
|
'total_static_routes' => $totalStatic,
|
||||||
|
'total_named_routes' => count($namedRoutes),
|
||||||
|
'issue_count' => count($issues),
|
||||||
|
];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'summary' => $summary,
|
||||||
|
'issues' => $issues,
|
||||||
|
'stats' => $this->compiledRoutes->getStats(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that route parameters in path are consistent with action method signature
|
||||||
|
*/
|
||||||
|
private function checkParameterConsistency(array &$issues, string $method, string $subdomain, string $path, ?string $routeName, object $route, ReflectionMethod $refMethod): void
|
||||||
|
{
|
||||||
|
$pathParams = $this->extractPathParams($path);
|
||||||
|
|
||||||
|
// Try to read expected parameters from route definition; otherwise from reflection
|
||||||
|
$expected = [];
|
||||||
|
if (isset($route->parameters) && is_array($route->parameters)) {
|
||||||
|
// If associative, use keys; if list, use values
|
||||||
|
$keys = array_keys($route->parameters);
|
||||||
|
$expected = array_values(array_filter(
|
||||||
|
count($keys) !== count($route->parameters) ? $route->parameters : $keys,
|
||||||
|
fn($v) => is_string($v) && $v !== ''
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
$expected = array_map(
|
||||||
|
static fn(\ReflectionParameter $p) => $p->getName(),
|
||||||
|
$refMethod->getParameters()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize unique sets
|
||||||
|
$pathSet = array_values(array_unique($pathParams));
|
||||||
|
$expectedSet = array_values(array_unique($expected));
|
||||||
|
|
||||||
|
// Missing placeholders in path for expected parameters
|
||||||
|
$missingInPath = array_values(array_diff($expectedSet, $pathSet));
|
||||||
|
if (!empty($missingInPath)) {
|
||||||
|
$issues[] = $this->issue(
|
||||||
|
'param_mismatch',
|
||||||
|
'warning',
|
||||||
|
$method,
|
||||||
|
$subdomain,
|
||||||
|
$path,
|
||||||
|
$routeName,
|
||||||
|
'Expected parameters not present in path: ' . implode(', ', $missingInPath)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extra placeholders not expected by the action
|
||||||
|
$extraInPath = array_values(array_diff($pathSet, $expectedSet));
|
||||||
|
if (!empty($extraInPath)) {
|
||||||
|
$issues[] = $this->issue(
|
||||||
|
'param_mismatch',
|
||||||
|
'warning',
|
||||||
|
$method,
|
||||||
|
$subdomain,
|
||||||
|
$path,
|
||||||
|
$routeName,
|
||||||
|
'Path has placeholders not expected by action: ' . implode(', ', $extraInPath)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate named routes (basic structural checks)
|
||||||
|
* @param array<string, object> $namedRoutes
|
||||||
|
* @return array<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
private function validateNamedRoutes(array $namedRoutes): array
|
||||||
|
{
|
||||||
|
$issues = [];
|
||||||
|
foreach ($namedRoutes as $name => $route) {
|
||||||
|
// Minimal: ensure a path exists
|
||||||
|
$path = $route->path ?? null;
|
||||||
|
if (!is_string($path) || $path === '') {
|
||||||
|
$issues[] = [
|
||||||
|
'type' => 'invalid_named_route',
|
||||||
|
'severity' => 'error',
|
||||||
|
'route_name' => $name,
|
||||||
|
'message' => 'Named route has no valid path',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $issues;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract placeholders from route path like /users/{id}/posts/{slug}
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private function extractPathParams(string $path): array
|
||||||
|
{
|
||||||
|
$matches = [];
|
||||||
|
preg_match_all('/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/', $path, $matches);
|
||||||
|
|
||||||
|
/** @var array<int, string> $params */
|
||||||
|
$params = $matches[1] ?? [];
|
||||||
|
|
||||||
|
return $params;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a standardized issue array
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function issue(string $type, string $severity, string $method, string $subdomain, string $path, ?string $name, string $message): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => $type,
|
||||||
|
'severity' => $severity,
|
||||||
|
'route' => [
|
||||||
|
'method' => $method,
|
||||||
|
'subdomain' => $subdomain,
|
||||||
|
'path' => $path,
|
||||||
|
'name' => $name,
|
||||||
|
],
|
||||||
|
'message' => $message,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user