diff --git a/src/Http/BaseApplication.php b/src/Http/BaseApplication.php index bc6d0e19a0c..128ba89e4fd 100644 --- a/src/Http/BaseApplication.php +++ b/src/Http/BaseApplication.php @@ -61,6 +61,19 @@ public function bootstrap() require_once $this->configDir . '/bootstrap.php'; } + /** + * Define the routes for an application. + * + * By default this will load `config/routes.php` for ease of use and backwards compatibility. + * + * @param \Cake\Routing\RouteBuilder $routes A route builder to add routes into. + * @return void + */ + public function routes($routes) + { + require $this->configDir . '/routes.php'; + } + /** * Invoke the application. * diff --git a/src/Http/ServerRequestFactory.php b/src/Http/ServerRequestFactory.php index e482e97d3c9..f6b79665abd 100644 --- a/src/Http/ServerRequestFactory.php +++ b/src/Http/ServerRequestFactory.php @@ -100,6 +100,10 @@ public static function marshalUriFromServer(array $server, array $headers) $uri = static::updatePath($base, $uri); } + if (!$uri->getHost()) { + $uri = $uri->withHost('localhost'); + } + // Splat on some extra attributes to save // some method calls. $uri->base = $base; diff --git a/src/Routing/Middleware/RoutingMiddleware.php b/src/Routing/Middleware/RoutingMiddleware.php index 60704bc5cd2..e027715b488 100644 --- a/src/Routing/Middleware/RoutingMiddleware.php +++ b/src/Routing/Middleware/RoutingMiddleware.php @@ -14,8 +14,11 @@ */ namespace Cake\Routing\Middleware; +use Cake\Http\BaseApplication; +use Cake\Http\MiddlewareQueue; use Cake\Http\Runner; use Cake\Routing\Exception\RedirectException; +use Cake\Routing\RouteBuilder; use Cake\Routing\Router; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -27,12 +30,46 @@ */ class RoutingMiddleware { + /** + * The application that will have its routing hook invoked. + * + * @var \Cake\Http\BaseApplication + */ + protected $app; + + /** + * Constructor + * + * @param \Cake\Http\BaseApplication $app The application instance that routes are defined on. + */ + public function __construct(BaseApplication $app = null) + { + $this->app = $app; + } + + /** + * Trigger the application's routes() hook if the application exists. + * + * If the middleware is created without an Application, routes will be + * loaded via the automatic route loading that pre-dates the routes() hook. + * + * @return void + */ + protected function loadRoutes() + { + if ($this->app) { + $builder = Router::createRouteBuilder('/'); + $this->app->routes($builder); + // Prevent routes from being loaded again + Router::$initialized = true; + } + } /** * Apply routing and update the request. * - * Any route/path specific middleware will be wrapped around $next and then the new middleware stack - * will be invoked. + * Any route/path specific middleware will be wrapped around $next and then the new middleware stack will be + * invoked. * * @param \Psr\Http\Message\ServerRequestInterface $request The request. * @param \Psr\Http\Message\ResponseInterface $response The response. @@ -41,6 +78,7 @@ class RoutingMiddleware */ public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next) { + $this->loadRoutes(); try { Router::setRequestContext($request); $params = (array)$request->getAttribute('params', []); @@ -61,11 +99,12 @@ public function __invoke(ServerRequestInterface $request, ResponseInterface $res $response->getHeaders() ); } - $middleware = Router::getMatchingMiddleware($request->getUri()->getPath()); - if (!$middleware) { + $matching = Router::getRouteCollection()->getMatchingMiddleware($request->getUri()->getPath()); + if (!$matching) { return $next($request, $response); } - $middleware->add($next); + $matching[] = $next; + $middleware = new MiddlewareQueue($matching); $runner = new Runner(); return $runner->run($middleware, $request, $response); diff --git a/src/Routing/Router.php b/src/Routing/Router.php index 7a3aacb56f7..4e6296b6107 100644 --- a/src/Routing/Router.php +++ b/src/Routing/Router.php @@ -419,34 +419,19 @@ public static function pushRequest(ServerRequest $request) /** * Store the request context for a given request. * - * @param \Cake\Http\ServerRequest|\Psr\Http\Message\ServerRequestInterface $request The request instance. + * @param \Psr\Http\Message\ServerRequestInterface $request The request instance. * @return void * @throws InvalidArgumentException When parameter is an incorrect type. */ - public static function setRequestContext($request) + public static function setRequestContext(ServerRequestInterface $request) { - if ($request instanceof ServerRequest) { - static::$_requestContext = [ - '_base' => $request->base, - '_port' => $request->port(), - '_scheme' => $request->scheme(), - '_host' => $request->host() - ]; - - return; - } - if ($request instanceof ServerRequestInterface) { - $uri = $request->getUri(); - static::$_requestContext = [ - '_base' => $request->getAttribute('base'), - '_port' => $uri->getPort(), - '_scheme' => $uri->getScheme(), - '_host' => $uri->getHost(), - ]; - - return; - } - throw new InvalidArgumentException('Unknown request type received.'); + $uri = $request->getUri(); + static::$_requestContext = [ + '_base' => $request->getAttribute('base'), + '_port' => $uri->getPort(), + '_scheme' => $uri->getScheme(), + '_host' => $uri->getHost(), + ]; } /** @@ -925,6 +910,27 @@ public static function parseNamedParams(ServerRequest $request, array $options = return $request; } + /** + * Create a RouteBuilder for the provided path. + * + * @param string $path The path to set the builder to. + * @param array $options The options for the builder + * @return \Cake\Routing\RouteBuilder + */ + public static function createRouteBuilder($path, array $options = []) + { + $defaults = [ + 'routeClass' => static::defaultRouteClass(), + 'extensions' => static::$_defaultExtensions, + ]; + $options += $defaults; + + return new RouteBuilder(static::$_collection, $path, [], [ + 'routeClass' => $options['routeClass'], + 'extensions' => $options['extensions'], + ]); + } + /** * Create a routing scope. * @@ -969,18 +975,12 @@ public static function parseNamedParams(ServerRequest $request, array $options = */ public static function scope($path, $params = [], $callback = null) { - $options = [ - 'routeClass' => static::defaultRouteClass(), - 'extensions' => static::$_defaultExtensions, - ]; + $options = []; if (is_array($params)) { - $options = $params + $options; + $options = $params; unset($params['routeClass'], $params['extensions']); } - $builder = new RouteBuilder(static::$_collection, '/', [], [ - 'routeClass' => $options['routeClass'], - 'extensions' => $options['extensions'], - ]); + $builder = static::createRouteBuilder('/', $options); $builder->scope($path, $params, $callback); } @@ -1075,28 +1075,19 @@ public static function routes() } /** - * Get a MiddlewareQueue of middleware that matches the provided path. + * Get the RouteCollection inside the Router * - * @param string $path The URL path to match for. - * @return \Cake\Http\MiddlewareQueue|null Either a queue or null if there are no matching middleware. + * @return \Cake\Routing\RouteCollection */ - public static function getMatchingMiddleware($path) + public static function getRouteCollection() { - if (!static::$initialized) { - static::_loadRoutes(); - } - - $middleware = static::$_collection->getMatchingMiddleware($path); - if ($middleware) { - return new MiddlewareQueue($middleware); - } - - return null; + return static::$_collection; } /** * Loads route configuration * + * @deprecated 3.5.0 Routes will be loaded via the Application::routes() hook in 4.0.0 * @return void */ protected static function _loadRoutes() diff --git a/tests/TestCase/Routing/Middleware/RoutingMiddlewareTest.php b/tests/TestCase/Routing/Middleware/RoutingMiddlewareTest.php index c0f045e27c1..64836e1752d 100644 --- a/tests/TestCase/Routing/Middleware/RoutingMiddlewareTest.php +++ b/tests/TestCase/Routing/Middleware/RoutingMiddlewareTest.php @@ -17,6 +17,7 @@ use Cake\Routing\Middleware\RoutingMiddleware; use Cake\Routing\Router; use Cake\TestSuite\TestCase; +use TestApp\Application; use Zend\Diactoros\Response; use Zend\Diactoros\ServerRequestFactory; @@ -104,6 +105,35 @@ public function testRouterSetParams() $middleware($request, $response, $next); } + /** + * Test middleware invoking hook method + * + * @return void + */ + public function testRoutesHookInvokedOnApp() + { + Router::reload(); + $this->assertFalse(Router::$initialized, 'Router precondition failed'); + + $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/app/articles']); + $response = new Response(); + $next = function ($req, $res) { + $expected = [ + 'controller' => 'Articles', + 'action' => 'index', + 'plugin' => null, + 'pass' => [], + '_matchedRoute' => '/app/articles' + ]; + $this->assertEquals($expected, $req->getAttribute('params')); + $this->assertTrue(Router::$initialized, 'Router state should indicate routes loaded'); + $this->assertCount(1, Router::routes()); + }; + $app = new Application(CONFIG); + $middleware = new RoutingMiddleware($app); + $middleware($request, $response, $next); + } + /** * Test that routing is not applied if a controller exists already * diff --git a/tests/TestCase/Routing/RouterTest.php b/tests/TestCase/Routing/RouterTest.php index 507b2d8736b..cd545ae3e6e 100644 --- a/tests/TestCase/Routing/RouterTest.php +++ b/tests/TestCase/Routing/RouterTest.php @@ -19,6 +19,8 @@ use Cake\Http\MiddlewareQueue; use Cake\Http\ServerRequest; use Cake\Http\ServerRequestFactory; +use Cake\Routing\RouteBuilder; +use Cake\Routing\RouteCollection; use Cake\Routing\Router; use Cake\Routing\Route\Route; use Cake\TestSuite\TestCase; @@ -2035,7 +2037,6 @@ public function testGenerationWithSslOption() Router::connect('/:controller/:action/*'); $request = new ServerRequest(); - $request->env('HTTP_HOST', 'localhost'); Router::pushRequest( $request->addParams([ 'plugin' => null, 'controller' => 'images', 'action' => 'index' @@ -2067,7 +2068,6 @@ public function testGenerateWithSslInSsl() Router::connect('/:controller/:action/*'); $request = new ServerRequest(); - $request->env('HTTP_HOST', 'localhost'); $request->env('HTTPS', 'on'); Router::pushRequest( $request->addParams([ @@ -3342,38 +3342,34 @@ public function testSetRequestContextPsr() } /** - * Test setting the request context. + * Test getting the route collection * - * @expectedException \InvalidArgumentException * @return void */ - public function testSetRequestContextInvalid() + public function testGetRouteCollection() { - Router::setRequestContext(new \stdClass); + $collection = Router::getRouteCollection(); + $this->assertInstanceOf(RouteCollection::class, $collection); + $this->assertCount(0, $collection->routes()); } /** - * Test getting path specific middleware. + * Test getting a route builder instance. * * @return void */ - public function testGetMatchingMiddleware() + public function testCreateRouteBuilder() { - Router::scope('/', function ($routes) { - $routes->connect('/articles', ['controller' => 'Articles']); - $routes->registerMiddleware('noop', function () { - }); - }); - Router::scope('/api/v1', function ($routes) { - $routes->applyMiddleware('noop'); - $routes->connect('/articles', ['controller' => 'Articles', 'prefix' => 'Api']); - }); - $result = Router::getMatchingMiddleware('/articles'); - $this->assertNull($result); + $builder = Router::createRouteBuilder('/api'); + $this->assertInstanceOf(RouteBuilder::class, $builder); + $this->assertSame('/api', $builder->path()); - $result = Router::getMatchingMiddleware('/api/v1/articles'); - $this->assertInstanceOf(MiddlewareQueue::class, $result); - $this->assertCount(1, $result); + $builder = Router::createRouteBuilder('/', [ + 'routeClass' => 'InflectedRoute', + 'extensions' => ['json'] + ]); + $this->assertInstanceOf(RouteBuilder::class, $builder); + $this->assertSame(['json'], $builder->extensions()); } /** diff --git a/tests/test_app/TestApp/Application.php b/tests/test_app/TestApp/Application.php index d8cef3c4527..837fe5c9a71 100644 --- a/tests/test_app/TestApp/Application.php +++ b/tests/test_app/TestApp/Application.php @@ -42,4 +42,17 @@ public function middleware($middleware) return $middleware; } + + /** + * Routes hook, used for testing with RoutingMiddleware. + * + * @param \Cake\Routing\RouteBuilder $routes + * @return void + */ + public function routes($routes) + { + $routes->scope('/app', function ($routes) { + $routes->connect('/articles', ['controller' => 'Articles']); + }); + } }