From a31eea763319909b7478e517c38183d7173cc82d Mon Sep 17 00:00:00 2001 From: Arif Hoque Date: Wed, 4 Mar 2026 12:25:48 +0600 Subject: [PATCH] Improve Dynamic Controller Registration --- src/Phaseolies/Support/Router.php | 53 +++-- .../InteractsWithDynamicControllerBinding.php | 181 ++++++++++++++++++ 2 files changed, 217 insertions(+), 17 deletions(-) create mode 100644 src/Phaseolies/Support/Router/InteractsWithDynamicControllerBinding.php diff --git a/src/Phaseolies/Support/Router.php b/src/Phaseolies/Support/Router.php index 82c09515..860fff2c 100644 --- a/src/Phaseolies/Support/Router.php +++ b/src/Phaseolies/Support/Router.php @@ -9,6 +9,7 @@ use Phaseolies\Utilities\Attributes\Bind; use Phaseolies\Support\Router\InteractsWithCurrentRouter; use Phaseolies\Support\Router\InteractsWithBundleRouter; +use Phaseolies\Support\Router\InteractsWithDynamicControllerBinding; use Phaseolies\Middleware\Contracts\Middleware as ContractsMiddleware; use Phaseolies\Http\Validation\Contracts\ValidatesWhenResolved; use Phaseolies\Http\Response; @@ -20,7 +21,7 @@ class Router extends Kernel { - use InteractsWithBundleRouter, InteractsWithCurrentRouter; + use InteractsWithBundleRouter, InteractsWithCurrentRouter, InteractsWithDynamicControllerBinding; /** * Holds the registered routes. @@ -288,36 +289,54 @@ public function registerAttributeRoutes(): void } /** - * Get all controller classes from app directory + * Get all controller classes using Composer autodiscovery * * @return array */ protected function getControllerClasses(): array { - $controllerPath = base_path('app/Http/Controllers'); $controllers = []; + $composerLoader = $this->getComposerClassLoader(); - if (!is_dir($controllerPath)) { - return $controllers; - } - - $iterator = new \RecursiveIteratorIterator( - new \RecursiveDirectoryIterator($controllerPath) - ); + $prefixes = $composerLoader->getPrefixesPsr4(); - foreach ($iterator as $file) { - if ($file->isDir() || $file->getExtension() !== 'php') { + foreach ($prefixes as $namespace => $paths) { + if ( + !str_starts_with($namespace, 'App\\') && + !str_starts_with($namespace, 'Modules\\') + ) { continue; } - $relativePath = str_replace($controllerPath . DIRECTORY_SEPARATOR, '', $file->getPathname()); - $relativePath = str_replace(DIRECTORY_SEPARATOR, '\\', $relativePath); - $className = 'App\\Http\\Controllers\\' . str_replace('.php', '', $relativePath); - if (class_exists($className)) { - $controllers[] = $className; + foreach ($paths as $path) { + + if (!is_dir($path)) { + continue; + } + + $files = $this->findPhpFiles($path); + + foreach ($files as $file) { + + $class = $this->convertFileToClassName($file, $path, $namespace); + + if (!$class || !class_exists($class)) { + continue; + } + + if ($this->isControllerClass($class)) { + $controllers[] = $class; + } + } } } + $controllers = array_unique($controllers); + + if (empty($controllers)) { + return $this->scanDefaultControllerDirectory(); + } + return $controllers; } diff --git a/src/Phaseolies/Support/Router/InteractsWithDynamicControllerBinding.php b/src/Phaseolies/Support/Router/InteractsWithDynamicControllerBinding.php new file mode 100644 index 00000000..e20ac9e4 --- /dev/null +++ b/src/Phaseolies/Support/Router/InteractsWithDynamicControllerBinding.php @@ -0,0 +1,181 @@ +isFile() && $file->getExtension() === 'php') { + $files[] = $file->getPathname(); + } + } + } catch (\Exception $e) { + } + + return $files; + } + + /** + * Convert file path to fully qualified class name + * + * @param string $filePath + * @param string $basePath + * @param string $namespace + * @return string|null + */ + protected function convertFileToClassName(string $filePath, string $basePath, string $namespace): ?string + { + // Normalize paths + $filePath = str_replace('\\', '/', realpath($filePath)); + $basePath = str_replace('\\', '/', realpath($basePath)); + + if (!str_starts_with($filePath, $basePath)) { + return null; + } + + // Get relative path from base + $relativePath = substr($filePath, strlen($basePath)); + $relativePath = ltrim($relativePath, '/'); + + // Remove .php extension + $relativePath = substr($relativePath, 0, -4); + + // Convert path to namespace + $classPath = str_replace('/', '\\', $relativePath); + + // Combine with namespace prefix + $className = rtrim($namespace, '\\') . '\\' . $classPath; + + return $className; + } + + /** + * Check if a class is a controller + * + * @param string $class + * @return bool + */ + protected function isControllerClass(string $class): bool + { + try { + $reflection = new \ReflectionClass($class); + + if ( + $reflection->isAbstract() || + $reflection->isInterface() || + $reflection->isTrait() + ) { + return false; + } + + if (str_ends_with($reflection->getShortName(), 'Controller')) { + return true; + } + + foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { + if (!empty($method->getAttributes( + \Phaseolies\Utilities\Attributes\Route::class + ))) { + return true; + } + } + + if ($reflection->isSubclassOf(\App\Http\Controllers\Controller::class)) { + return true; + } + + return false; + } catch (\Throwable $e) { + return false; + } + } + + /** + * Fallback: scan default controller directory + * + * @return array + */ + protected function scanDefaultControllerDirectory(): array + { + $controllerPath = base_path('app/Http/Controllers'); + $controllers = []; + + if (!is_dir($controllerPath)) { + return $controllers; + } + + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($controllerPath, \RecursiveDirectoryIterator::SKIP_DOTS) + ); + + foreach ($iterator as $file) { + if ($file->isDir() || $file->getExtension() !== 'php') { + continue; + } + + $relativePath = str_replace($controllerPath . DIRECTORY_SEPARATOR, '', $file->getPathname()); + $relativePath = str_replace(DIRECTORY_SEPARATOR, '\\', $relativePath); + $className = 'App\\Http\\Controllers\\' . str_replace('.php', '', $relativePath); + + if (class_exists($className)) { + $controllers[] = $className; + } + } + + return $controllers; + } +}