Browse Source

Implement basics of scoped middleware

Implement registering, enabling and finding middleware based on path.
Scopes with placeholders aren't done yet, the basics are in place.
Mark Story 9 years ago
parent
commit
7b752864a0
2 changed files with 262 additions and 0 deletions
  1. 99 0
      src/Routing/RouteCollection.php
  2. 163 0
      tests/TestCase/Routing/RouteCollectionTest.php

+ 99 - 0
src/Routing/RouteCollection.php

@@ -18,6 +18,7 @@ use Cake\Routing\Exception\DuplicateNamedRouteException;
 use Cake\Routing\Exception\MissingRouteException;
 use Cake\Routing\Route\Route;
 use Psr\Http\Message\ServerRequestInterface;
+use RuntimeException;
 
 /**
  * Contains a collection of routes.
@@ -59,6 +60,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
      *
      * @var array
@@ -363,4 +378,88 @@ class RouteCollection
 
         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 - 0
tests/TestCase/Routing/RouteCollectionTest.php

@@ -20,6 +20,7 @@ use Cake\Routing\RouteBuilder;
 use Cake\Routing\RouteCollection;
 use Cake\Routing\Route\Route;
 use Cake\TestSuite\TestCase;
+use \stdClass;
 
 class RouteCollectionTest extends TestCase
 {
@@ -609,4 +610,166 @@ class RouteCollectionTest extends TestCase
         $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']);
+    }
 }