Skip to content

Commit

Permalink
Implement basics of scoped middleware
Browse files Browse the repository at this point in the history
Implement registering, enabling and finding middleware based on path.
Scopes with placeholders aren't done yet, the basics are in place.
  • Loading branch information
markstory committed Apr 25, 2017
1 parent 600439d commit 7b75286
Show file tree
Hide file tree
Showing 2 changed files with 262 additions and 0 deletions.
99 changes: 99 additions & 0 deletions src/Routing/RouteCollection.php
Expand Up @@ -18,6 +18,7 @@
use Cake\Routing\Exception\MissingRouteException;
use Cake\Routing\Route\Route;
use Psr\Http\Message\ServerRequestInterface;
use RuntimeException;

/**
* Contains a collection of routes.
Expand Down Expand Up @@ -58,6 +59,20 @@ class RouteCollection
*/
protected $_paths = [];

/**
* A map of middleware names and the related objects.
*
* @var array
*/
protected $_middleware = [];

/**
* A map of paths and the list of applicable middleware.
*
* @var array
*/
protected $_middlewarePaths = [];

/**
* Route extensions
*
Expand Down Expand Up @@ -363,4 +378,88 @@ public function extensions($extensions = null, $merge = true)

return $this->_extensions = $extensions;
}

/**
* Register a middleware with the RouteCollection.
*
* Once middleware has been registered, it can be applied to the current routing
* scope or any child scopes that share the same RoutingCollection.
*
* @param string $name The name of the middleware. Used when applying middleware to a scope.
* @param callable $middleware The middleware object to register.
* @return $this
*/
public function registerMiddleware($name, callable $middleware)
{
if (is_string($middleware)) {
throw new RuntimeException("The '$name' middleware is not a callable object.");
}
$this->_middleware[$name] = $middleware;

return $this;
}

/**
* Check if the named middleware has been registered.
*
* @param string $name The name of the middleware to check.
* @return void
*/
public function hasMiddleware($name)
{
return isset($this->_middleware[$name]);
}

/**
* Enable a registered middleware(s) for the provided path
*
* @param string $path The URL path to register middleware for.
* @param string[] $names The middleware names to add for the path.
* @return $this
*/
public function enableMiddleware($path, array $middleware)
{
foreach ($middleware as $name) {
if (!$this->hasMiddleware($name)) {
$message = "Cannot apply '$name' middleware to path '$path'. It has not been registered.";
throw new RuntimeException($message);
}
}
if (!isset($this->_middlewarePaths[$path])) {
$this->_middlewarePaths[$path] = [];
}
$this->_middlewarePaths[$path] = array_merge($this->_middlewarePaths[$path], $middleware);

return $this;
}

/**
* Get an array of middleware that matches the provided URL.
*
* All middleware lists that match the URL will be merged together from shortest
* path to longest path. If a middleware would be added to the set more than
* once because it is connected to multiple path substrings match, it will only
* be added once at its first occurrence.
*
* @param string $needle The URL path to find middleware for.
* @return array
*/
public function getMatchingMiddleware($needle)
{
$matching = [];
foreach ($this->_middlewarePaths as $path => $middleware) {
if (strpos($needle, $path) === 0) {
$matching = array_merge($matching, $middleware);
}
}

$resolved = [];
foreach ($matching as $name) {
if (!isset($resolved[$name])) {
$resolved[$name] = $this->_middleware[$name];
}
}

return array_values($resolved);
}
}
163 changes: 163 additions & 0 deletions tests/TestCase/Routing/RouteCollectionTest.php
Expand Up @@ -20,6 +20,7 @@
use Cake\Routing\RouteCollection;
use Cake\Routing\Route\Route;
use Cake\TestSuite\TestCase;
use \stdClass;

class RouteCollectionTest extends TestCase
{
Expand Down Expand Up @@ -609,4 +610,166 @@ public function testExtensions()
$this->collection->extensions(['csv'], false);
$this->assertEquals(['csv'], $this->collection->extensions());
}

/**
* String methods are not acceptable.
*
* @expectedException \RuntimeException
* @expectedExceptionMessage The 'bad' middleware is not a callable object.
* @return void
*/
public function testRegisterMiddlewareNoCallableString()
{
$this->collection->registerMiddleware('bad', 'strlen');
}

/**
* Test adding middleware to the collection.
*
* @return void
*/
public function testRegisterMiddleware()
{
$result = $this->collection->registerMiddleware('closure', function () {
});
$this->assertSame($result, $this->collection);

$mock = $this->getMockBuilder('\stdClass')
->setMethods(['__invoke'])
->getMock();
$result = $this->collection->registerMiddleware('callable', $mock);
$this->assertSame($result, $this->collection);

$this->assertTrue($this->collection->hasMiddleware('closure'));
$this->assertTrue($this->collection->hasMiddleware('callable'));
}

/**
* Test adding middleware with a placeholder in the path.
*
* @return void
*/
public function testEnableMiddlewareBasic()
{
$mock = $this->getMockBuilder('\stdClass')
->setMethods(['__invoke'])
->getMock();
$this->collection->registerMiddleware('callable', $mock);
$this->collection->registerMiddleware('callback_two', $mock);

$result = $this->collection->enableMiddleware('/api', ['callable', 'callback_two']);
$this->assertSame($result, $this->collection);
}

/**
* Test adding middleware with a placeholder in the path.
*
* @return void
*/
public function testGetMatchingMiddlewareBasic()
{
$mock = $this->getMockBuilder('\stdClass')
->setMethods(['__invoke'])
->getMock();
$this->collection->registerMiddleware('callable', $mock);
$this->collection->registerMiddleware('callback_two', $mock);

$result = $this->collection->enableMiddleware('/api', ['callable']);
$middleware = $this->collection->getMatchingMiddleware('/api/v1/articles');
$this->assertCount(1, $middleware);
$this->assertSame($middleware[0], $mock);
}

/**
* Test enabling and matching
*
* @return void
*/
public function testGetMatchingMiddlewareMultiplePaths()
{
$mock = $this->getMockBuilder('\stdClass')
->setMethods(['__invoke'])
->getMock();
$mockTwo = $this->getMockBuilder('\stdClass')
->setMethods(['__invoke'])
->getMock();
$this->collection->registerMiddleware('callable', $mock);
$this->collection->registerMiddleware('callback_two', $mockTwo);

$this->collection->enableMiddleware('/api', ['callable']);
$this->collection->enableMiddleware('/api/v1/articles', ['callback_two']);

$middleware = $this->collection->getMatchingMiddleware('/articles');
$this->assertCount(0, $middleware);

$middleware = $this->collection->getMatchingMiddleware('/api/v1/articles/1');
$this->assertCount(2, $middleware);
$this->assertEquals([$mock, $mockTwo], $middleware, 'Both middleware match');

$middleware = $this->collection->getMatchingMiddleware('/api/v1/comments');
$this->assertCount(1, $middleware);
$this->assertEquals([$mock], $middleware, 'Should not match /articles middleware');
}

/**
* Test enabling and matching
*
* @return void
*/
public function testGetMatchingMiddlewareDeduplicate()
{
$mock = $this->getMockBuilder('\stdClass')
->setMethods(['__invoke'])
->getMock();
$mockTwo = $this->getMockBuilder('\stdClass')
->setMethods(['__invoke'])
->getMock();
$this->collection->registerMiddleware('callable', $mock);
$this->collection->registerMiddleware('callback_two', $mockTwo);

$this->collection->enableMiddleware('/api', ['callable']);
$this->collection->enableMiddleware('/api/v1/articles', ['callback_two', 'callable']);

$middleware = $this->collection->getMatchingMiddleware('/api/v1/articles/1');
$this->assertCount(2, $middleware);
$this->assertEquals([$mock, $mockTwo], $middleware, 'Both middleware match');
}

/**
* Test adding middleware with a placeholder in the path.
*
* @return void
*/
public function testEnableMiddlewareWithPlaceholder()
{
$mock = $this->getMockBuilder('\stdClass')
->setMethods(['__invoke'])
->getMock();
$this->collection->registerMiddleware('callable', $mock);

$this->collection->enableMiddleware('/articles/:article_id/comments', ['callable']);
$this->markTestIncomplete();

$middleware = $this->collection->getMatchingMiddleware('/articles/123/comments');
$this->assertEquals([$mock], $middleware);

$middleware = $this->collection->getMatchingMiddleware('/articles/abc-123/comments/99');
$this->assertEquals([$mock], $middleware);
}

/**
* Test applying middleware to a scope when it doesn't exist
*
* @expectedException \RuntimeException
* @expectedExceptionMessage Cannot apply 'bad' middleware to path '/api'. It has not been registered.
* @return void
*/
public function testEnableMiddlewareUnregistered()
{
$mock = $this->getMockBuilder('\stdClass')
->setMethods(['__invoke'])
->getMock();
$this->collection->registerMiddleware('callable', $mock);
$this->collection->enableMiddleware('/api', ['callable', 'bad']);
}
}

0 comments on commit 7b75286

Please sign in to comment.