Skip to content

Commit

Permalink
Merge pull request #10646 from cakephp/3next-routing-hook
Browse files Browse the repository at this point in the history
3.next - Add application routes hook
  • Loading branch information
markstory committed May 21, 2017
2 parents 474ebc2 + 3d96541 commit e23f836
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 74 deletions.
13 changes: 13 additions & 0 deletions src/Http/BaseApplication.php
Expand Up @@ -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.
*
Expand Down
4 changes: 4 additions & 0 deletions src/Http/ServerRequestFactory.php
Expand Up @@ -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;
Expand Down
49 changes: 44 additions & 5 deletions src/Routing/Middleware/RoutingMiddleware.php
Expand Up @@ -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;
Expand All @@ -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.
Expand All @@ -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', []);
Expand All @@ -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);
Expand Down
85 changes: 38 additions & 47 deletions src/Routing/Router.php
Expand Up @@ -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(),
];
}

/**
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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()
Expand Down
30 changes: 30 additions & 0 deletions tests/TestCase/Routing/Middleware/RoutingMiddlewareTest.php
Expand Up @@ -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;

Expand Down Expand Up @@ -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
*
Expand Down
40 changes: 18 additions & 22 deletions tests/TestCase/Routing/RouterTest.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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([
Expand Down Expand Up @@ -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());
}

/**
Expand Down
13 changes: 13 additions & 0 deletions tests/test_app/TestApp/Application.php
Expand Up @@ -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']);
});
}
}

0 comments on commit e23f836

Please sign in to comment.