diff --git a/Slim/Container.php b/Slim/Container.php index d8fbbcb1f..549dda9af 100644 --- a/Slim/Container.php +++ b/Slim/Container.php @@ -58,6 +58,7 @@ class Container extends PimpleContainer implements ContainerInterface 'determineRouteBeforeAppMiddleware' => false, 'displayErrorDetails' => false, 'addContentLengthHeader' => true, + 'routerCacheFile' => false, ]; /** @@ -96,7 +97,7 @@ private function registerDefaultServices($userSettings) $this['settings'] = function () use ($userSettings, $defaultSettings) { return new Collection(array_merge($defaultSettings, $userSettings)); }; - + $defaultProvider = new DefaultServicesProvider(); $defaultProvider->register($this); } diff --git a/Slim/DefaultServicesProvider.php b/Slim/DefaultServicesProvider.php index e41831b93..c18d87572 100644 --- a/Slim/DefaultServicesProvider.php +++ b/Slim/DefaultServicesProvider.php @@ -83,10 +83,17 @@ public function register($container) * This service MUST return a SHARED instance * of \Slim\Interfaces\RouterInterface. * + * @param Container $container + * * @return RouterInterface */ - $container['router'] = function () { - return new Router; + $container['router'] = function ($container) { + $routerCacheFile = false; + if (isset($container->get('settings')['routerCacheFile'])) { + $routerCacheFile = $container->get('settings')['routerCacheFile']; + } + + return (new Router)->setCacheFile($routerCacheFile); }; } diff --git a/Slim/Router.php b/Slim/Router.php index 5adcec342..b9d5d132a 100644 --- a/Slim/Router.php +++ b/Slim/Router.php @@ -44,6 +44,13 @@ class Router implements RouterInterface */ protected $basePath = ''; + /** + * Path to fast route cache file. Set to false to disable route caching + * + * @var string|False + */ + protected $cacheFile = false; + /** * Routes * @@ -97,6 +104,29 @@ public function setBasePath($basePath) return $this; } + /** + * Set path to fast route cache file. If this is false then route caching is disabled. + * + * @param string|false $cacheFile + * + * @return self + */ + public function setCacheFile($cacheFile) + { + if (!is_string($cacheFile) && $cacheFile !== false) { + throw new InvalidArgumentException('Router cacheFile must be a string or false'); + } + + $this->cacheFile = $cacheFile; + + if ($cacheFile !== false && !is_writable(dirname($cacheFile))) { + throw new RuntimeException('Router cacheFile directory must be writable'); + } + + + return $this; + } + /** * Add route * @@ -142,7 +172,7 @@ public function map($methods, $pattern, $handler) public function dispatch(ServerRequestInterface $request) { $uri = '/' . ltrim($request->getUri()->getPath(), '/'); - + return $this->createDispatcher()->dispatch( $request->getMethod(), $uri @@ -154,13 +184,28 @@ public function dispatch(ServerRequestInterface $request) */ protected function createDispatcher() { - return $this->dispatcher ?: \FastRoute\simpleDispatcher(function (RouteCollector $r) { + if ($this->dispatcher) { + return $this->dispatcher; + } + + $routeDefinitionCallback = function (RouteCollector $r) { foreach ($this->getRoutes() as $route) { $r->addRoute($route->getMethods(), $route->getPattern(), $route->getIdentifier()); } - }, [ - 'routeParser' => $this->routeParser - ]); + }; + + if ($this->cacheFile) { + $this->dispatcher = \FastRoute\cachedDispatcher($routeDefinitionCallback, [ + 'routeParser' => $this->routeParser, + 'cacheFile' => $this->cacheFile, + ]); + } else { + $this->dispatcher = \FastRoute\simpleDispatcher($routeDefinitionCallback, [ + 'routeParser' => $this->routeParser, + ]); + } + + return $this->dispatcher; } /** diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php index 85f47a1ef..7cab94f3a 100644 --- a/tests/ContainerTest.php +++ b/tests/ContainerTest.php @@ -156,4 +156,9 @@ public function testMagicGetMethod() $this->container->get('settings')['httpVersion'] = '1.2'; $this->assertSame('1.2', $this->container->__get('settings')['httpVersion']); } + + public function testRouteCacheDisabledByDefault() + { + $this->assertFalse($this->container->get('settings')['routerCacheFile']); + } } diff --git a/tests/RouterTest.php b/tests/RouterTest.php index 6e48dce8d..425e340a2 100644 --- a/tests/RouterTest.php +++ b/tests/RouterTest.php @@ -86,7 +86,7 @@ public function testRelativePathFor() $this->router->relativePathFor('foo', ['first' => 'josh', 'last' => 'lockhart']) ); } - + public function testPathForWithNoBasePath() { $this->router->setBasePath(''); @@ -104,7 +104,7 @@ public function testPathForWithNoBasePath() $this->router->pathFor('foo', ['first' => 'josh', 'last' => 'lockhart']) ); } - + public function testPathForWithBasePath() { $methods = ['GET']; @@ -306,4 +306,98 @@ public function testPathForWithModifiedRoutePattern() $this->router->relativePathFor('foo', ['voornaam' => 'josh', 'achternaam' => 'lockhart']) ); } + + /** + * Test cacheFile may be set to false + */ + public function testSettingCacheFileToFalse() + { + $this->router->setCacheFile(false); + + $class = new \ReflectionClass($this->router); + $property = $class->getProperty('cacheFile'); + $property->setAccessible(true); + + $this->assertFalse($property->getValue($this->router)); + } + + /** + * Test cacheFile should be a string or false + */ + public function testSettingInvalidCacheFileValue() + { + $this->setExpectedException( + '\InvalidArgumentException', + 'Router cacheFile must be a string' + ); + $this->router->setCacheFile(['invalid']); + } + + /** + * Test if cacheFile is not a writable directory + */ + public function testSettingInvalidCacheFileNotExisting() + { + $this->setExpectedException( + '\RuntimeException', + 'Router cacheFile directory must be writable' + ); + + $this->router->setCacheFile( + dirname(__FILE__) . uniqid(microtime(true)) . '/' . uniqid(microtime(true)) + ); + } + + /** + * Test cached routes file is created & that it holds our routes. + */ + public function testRouteCacheFileCanBeDispatched() + { + $methods = ['GET']; + $pattern = '/hello/{first}/{last}'; + $callable = function ($request, $response, $args) { + echo sprintf('Hello %s %s', $args['first'], $args['last']); + }; + $route = $this->router->map($methods, $pattern, $callable)->setName('foo'); + + $cacheFile = dirname(__FILE__) . '/' . uniqid(microtime(true)); + $this->router->setCacheFile($cacheFile); + $class = new \ReflectionClass($this->router); + $method = $class->getMethod('createDispatcher'); + $method->setAccessible(true); + + $dispatcher = $method->invoke($this->router); + $this->assertInstanceOf('\FastRoute\Dispatcher', $dispatcher); + $this->assertFileExists($cacheFile, 'cache file was not created'); + + // instantiate a new router & load the cached routes file & see if + // we can dispatch to the route we cached. + $router2 = new Router(); + $router2->setCacheFile($cacheFile); + + $class = new \ReflectionClass($router2); + $method = $class->getMethod('createDispatcher'); + $method->setAccessible(true); + + $dispatcher2 = $method->invoke($this->router); + $result = $dispatcher2->dispatch('GET', '/hello/josh/lockhart'); + $this->assertSame(\FastRoute\Dispatcher::FOUND, $result[0]); + + unlink($cacheFile); + } + + /** + * Calling createDispatcher as second time should give you back the same + * dispatcher as when you called it the first time. + */ + public function testCreateDispatcherReturnsSameDispatcherASecondTime() + { + $class = new \ReflectionClass($this->router); + $method = $class->getMethod('createDispatcher'); + $method->setAccessible(true); + + $dispatcher = $method->invoke($this->router); + $dispatcher2 = $method->invoke($this->router); + $this->assertSame($dispatcher2, $dispatcher); + } }