diff --git a/src/router/src/DispatcherFactory.php b/src/router/src/DispatcherFactory.php index e666eaec9..70c840333 100644 --- a/src/router/src/DispatcherFactory.php +++ b/src/router/src/DispatcherFactory.php @@ -16,18 +16,20 @@ class DispatcherFactory extends BaseDispatcherFactory public function __construct(protected ContainerInterface $container) { - $this->routes = $container->get(RouteFileCollector::class) - ->getRouteFiles(); $this->initAnnotationRoute(AnnotationCollector::list()); } - public function initRoutes() + public function initRoutes(): void { $this->initialized = true; MiddlewareManager::$container = []; - foreach ($this->routes as $route) { + // Fetch route files at initialization time + // Ensures routes added via loadRoutesFrom() in service providers are included + $routes = $this->container->get(RouteFileCollector::class)->getRouteFiles(); + + foreach ($routes as $route) { if (file_exists($route)) { require $route; } diff --git a/tests/Router/DispatcherFactoryTest.php b/tests/Router/DispatcherFactoryTest.php index 73278f189..a81a61e83 100644 --- a/tests/Router/DispatcherFactoryTest.php +++ b/tests/Router/DispatcherFactoryTest.php @@ -22,6 +22,15 @@ */ class DispatcherFactoryTest extends TestCase { + public static function setUpBeforeClass(): void + { + parent::setUpBeforeClass(); + + if (! defined('BASE_PATH')) { + define('BASE_PATH', dirname(__DIR__, 2)); + } + } + public function testGetRouter() { if (! defined('BASE_PATH')) { @@ -66,6 +75,51 @@ public function testInitConfigRoute() $dispatcherFactory->initRoutes('http'); } + /** + * Test that routes added to RouteFileCollector AFTER DispatcherFactory + * construction are still loaded when initRoutes() is called. + * + * This simulates the loadRoutesFrom() pattern where service providers + * add route files during boot(), after DispatcherFactory may have been + * constructed. + */ + public function testRoutesAddedAfterConstructionAreLoaded() + { + if (! defined('BASE_PATH')) { + $this->markTestSkipped('skip it because DispatcherFactory in hyperf is dirty.'); + } + + /** @var MockInterface|RouteCollector */ + $routeCollector = Mockery::mock(RouteCollector::class); + + // Initial route from foo.php + $routeCollector->shouldReceive('get')->with('/foo', 'Handler::Foo')->once(); + + // Late-added route from late.php - this MUST be loaded + $routeCollector->shouldReceive('get')->with('/late', 'Handler::Late')->once(); + + // Create RouteFileCollector with only the initial route + $routeFileCollector = new RouteFileCollector([ + __DIR__ . '/routes/foo.php', + ]); + + $container = $this->getContainer([ + HyperfRouteCollector::class => fn () => $routeCollector, + RouteFileCollector::class => fn () => $routeFileCollector, + ]); + + // Create DispatcherFactory - this captures route files in constructor + $dispatcherFactory = new DispatcherFactory($container); + $container->define(Router::class, fn () => new Router($dispatcherFactory)); + + // Simulate service provider adding routes AFTER DispatcherFactory construction + // This is what loadRoutesFrom() does + $routeFileCollector->addRouteFile(__DIR__ . '/routes/late.php'); + + // Now trigger route loading - late.php should be included + $dispatcherFactory->getRouter('http'); + } + private function getContainer(array $bindings = []): Container { $container = new Container( diff --git a/tests/Router/routes/late.php b/tests/Router/routes/late.php new file mode 100644 index 000000000..35422ce04 --- /dev/null +++ b/tests/Router/routes/late.php @@ -0,0 +1,7 @@ +