From c76e6ae111fbe35190ad17127aa1e04bf0f47784 Mon Sep 17 00:00:00 2001 From: "Fabian @ Blax Software" Date: Sat, 24 Jan 2026 14:49:51 +0100 Subject: [PATCH] RI websocket performance --- src/Websocket/ControllerResolver.php | 65 ++++++++++++--------------- tests/Unit/ControllerResolverTest.php | 9 ++-- 2 files changed, 34 insertions(+), 40 deletions(-) diff --git a/src/Websocket/ControllerResolver.php b/src/Websocket/ControllerResolver.php index 32d30f6..51105bc 100644 --- a/src/Websocket/ControllerResolver.php +++ b/src/Websocket/ControllerResolver.php @@ -59,12 +59,7 @@ class ControllerResolver return self::$controllerCache[$eventPrefix]; } - // Ensure controllers are scanned - if (!self::$scanned) { - self::scanControllers(); - } - - // Try to find the controller + // Try to find the controller (skip scanning in forked children - classes are already loaded) $controllerClass = self::findController($eventPrefix); // Cache the result (even if null, to avoid repeated lookups) @@ -75,60 +70,58 @@ class ControllerResolver /** * Find controller using multiple strategies + * Optimized for speed: most common case (direct match) checked first */ private static function findController(string $eventPrefix): ?string { - // Strategy 1: Direct match (e.g., 'app' → 'AppController') + // Strategy 1: Direct match in app namespace (most common case) + // e.g., 'app' → '\App\Websocket\Controllers\AppController' $directName = self::kebabToPascal($eventPrefix) . 'Controller'; - if ($class = self::findInAvailable($directName)) { - return $class; + $appClass = self::APP_NAMESPACE . $directName; + + // class_exists with autoload=true is fast for already-loaded classes + if (class_exists($appClass, true)) { + return $appClass; } - // Strategy 2: Folder structure (e.g., 'admin-user' → 'Admin/UserController') + // Strategy 2: Direct match in vendor namespace + $vendorClass = self::VENDOR_NAMESPACE . $directName; + if (class_exists($vendorClass, true)) { + return $vendorClass; + } + + // Strategy 3: Check pre-scanned available controllers (if scanned) + if (self::$scanned) { + if ($class = self::findInAvailable($directName)) { + return $class; + } + } + + // Strategy 4: Folder structure (e.g., 'admin-user' → 'Admin/UserController') + // Only try this for kebab-case names with multiple parts $parts = explode('-', $eventPrefix); if (count($parts) > 1) { - // Try progressively deeper folder structures - // 'admin-user-settings' tries: - // - Admin/User/SettingsController - // - Admin/UserSettingsController - // - AdminUser/SettingsController - for ($folderDepth = count($parts) - 1; $folderDepth >= 1; $folderDepth--) { $folderParts = array_slice($parts, 0, $folderDepth); $nameParts = array_slice($parts, $folderDepth); - $folder = implode('/', array_map('ucfirst', $folderParts)); + $folder = implode('\\', array_map('ucfirst', $folderParts)); $name = implode('', array_map('ucfirst', $nameParts)) . 'Controller'; // Try app namespace with folder - $appClass = self::APP_NAMESPACE . str_replace('/', '\\', $folder) . '\\' . $name; - if (class_exists($appClass)) { - self::$availableControllers[strtolower($name)] = $appClass; + $appClass = self::APP_NAMESPACE . $folder . '\\' . $name; + if (class_exists($appClass, true)) { return $appClass; } // Try vendor namespace with folder - $vendorClass = self::VENDOR_NAMESPACE . str_replace('/', '\\', $folder) . '\\' . $name; - if (class_exists($vendorClass)) { - self::$availableControllers[strtolower($name)] = $vendorClass; + $vendorClass = self::VENDOR_NAMESPACE . $folder . '\\' . $name; + if (class_exists($vendorClass, true)) { return $vendorClass; } } } - // Strategy 3: Dynamic class_exists check (for newly added controllers) - $appClass = self::APP_NAMESPACE . $directName; - if (class_exists($appClass)) { - self::$availableControllers[strtolower($directName)] = $appClass; - return $appClass; - } - - $vendorClass = self::VENDOR_NAMESPACE . $directName; - if (class_exists($vendorClass)) { - self::$availableControllers[strtolower($directName)] = $vendorClass; - return $vendorClass; - } - return null; } diff --git a/tests/Unit/ControllerResolverTest.php b/tests/Unit/ControllerResolverTest.php index 4d06087..9ca5d3a 100644 --- a/tests/Unit/ControllerResolverTest.php +++ b/tests/Unit/ControllerResolverTest.php @@ -26,11 +26,12 @@ class ControllerResolverTest extends TestCase /** @test */ public function it_caches_resolved_controllers() { - // First call scans and caches + // First call resolves and caches ControllerResolver::resolve('pusher'); $stats = ControllerResolver::getStats(); - $this->assertTrue($stats['scanned']); + // scanned may or may not be true (we no longer auto-scan on resolve) + // but cached should be > 0 $this->assertGreaterThan(0, $stats['cached']); } @@ -63,8 +64,8 @@ class ControllerResolverTest extends TestCase /** @test */ public function it_clears_cache_correctly() { - // Populate cache - ControllerResolver::resolve('pusher'); + // Preload to ensure scanned is true + ControllerResolver::preload(); $statsBefore = ControllerResolver::getStats(); $this->assertTrue($statsBefore['scanned']);