diff --git a/.travis.yml b/.travis.yml index 462afde..6c2df14 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,10 @@ php: - 7.0 - 7.1 +before_install: + - echo "extension = apcu.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini + - echo "apc.enable_cli = 1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini + before_script: - travis_retry composer install --no-interaction --prefer-source diff --git a/composer.json b/composer.json index 28849c9..0c584fc 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,9 @@ "nikic/fast-route": "^1.2.0" }, "require-dev": { - "phpunit/phpunit": "^6.0.8" + "phpunit/phpunit": "^6.0.8", + "http-interop/http-factory-diactoros": "0.2.0", + "atanvarno/cache-apcu": "^0.1.1" }, "autoload": { "psr-4": { diff --git a/src/CachedRouter.php b/src/CachedRouter.php index 03c4b1b..49fa022 100644 --- a/src/CachedRouter.php +++ b/src/CachedRouter.php @@ -17,7 +17,7 @@ class CachedRouter extends SimpleRouter public function __construct( CacheInterface $cache, - string $cacheKey, + string $cacheKey = 'routerData', $driver = Router::GROUP_COUNT, array $routes = [], bool $cacheDisabled = false diff --git a/src/Exception/MethodNotAllowedException.php b/src/Exception/MethodNotAllowedException.php index 44c1e2b..2664e7c 100644 --- a/src/Exception/MethodNotAllowedException.php +++ b/src/Exception/MethodNotAllowedException.php @@ -19,8 +19,8 @@ class MethodNotAllowedException extends UnexpectedValueException public function __construct(array $allowed, string $actual) { $this->allowed = $allowed; - $msg = sprintf('$s is not allowed for this route', $actual); - parent::__construct($msg, 405); + $msg = sprintf('%s is not allowed for this route', $actual); + parent::__construct($msg); } /** diff --git a/src/Router.php b/src/Router.php index cde5a19..6538df0 100644 --- a/src/Router.php +++ b/src/Router.php @@ -9,8 +9,6 @@ namespace Atanvarno\Router; /** SPL use block. */ -use Atanvarno\Router\Exception\MethodNotAllowedException; -use Atanvarno\Router\Exception\NotFoundException; use InvalidArgumentException; /** PSR-7 use block */ @@ -19,6 +17,11 @@ /** HTTP Message Utilities use block. */ use Fig\Http\Message\RequestMethodInterface; +/** Package use block. */ +use Atanvarno\Router\Exception\{ + MethodNotAllowedException, NotFoundException +}; + /** * Atanvarno\Router\Router * diff --git a/src/SimpleRouter.php b/src/SimpleRouter.php index 7e521a5..2ea57cd 100644 --- a/src/SimpleRouter.php +++ b/src/SimpleRouter.php @@ -8,7 +8,6 @@ namespace Atanvarno\Router; - /** PSR-7 use block */ use Psr\Http\Message\RequestInterface; @@ -48,6 +47,7 @@ public function __construct( throw new InvalidArgumentException($msg); } $this->driver = $driver; + $this->routes = []; // Ensure routes array is numerically indexed. $routes = array_values($routes); @@ -105,7 +105,7 @@ public function dispatch(RequestInterface $request) throw new MethodNotAllowedException($result[1], $method); default: // No break case Dispatcher::FOUND: - return $result; + return [$result[1], $result[2]]; } } diff --git a/tests/CachedRouterTest.php b/tests/CachedRouterTest.php new file mode 100644 index 0000000..f925bc4 --- /dev/null +++ b/tests/CachedRouterTest.php @@ -0,0 +1,126 @@ + + * @copyright 2017 atanvarno.com + * @license https://opensource.org/licenses/MIT The MIT License + */ + +namespace Atanvarno\Router\Test; + +/** PSR-16 use block. */ +use Psr\SimpleCache\CacheInterface; + +/** PSR-17 use block. */ +use Http\Factory\Diactoros\{ + RequestFactory, UriFactory +}; + +/** HTTP Message Utilities use block. */ +use Fig\Http\Message\RequestMethodInterface; + +/** PHP Unit use block. */ +use PHPUnit\Framework\TestCase; + +/** Package use block. */ +use Atanvarno\Router\{ + Router, + CachedRouter +}; + +/** Dependency use block. */ +use Atanvarno\Cache\{ + Apcu\APCuDriver, Cache +}; + +class CachedRouterTest extends TestCase +{ + /** @var CacheInterface $cache */ + private $cache; + + private $request; + + /** @var Router $router */ + private $router; + + public function setUp() + { + $this->cache = new Cache(new APCuDriver()); + $this->router = new CachedRouter($this->cache, 'routerData'); + $uri = (new UriFactory())->createUri('http://atanvarno.com/test/uri/'); + $this->request = (new RequestFactory())->createRequest( + RequestMethodInterface::METHOD_HEAD, $uri + ); + } + + public function tearDown() + { + $this->cache->clear(); + } + + public function testImplementsInterface() + { + $this->assertInstanceOf(Router::class, $this->router); + } + + public function testFirstRunPopulatesCache() + { + $this->assertFalse($this->cache->has('routerData')); + $this->router->add( + RequestMethodInterface::METHOD_HEAD, + '/{name}/uri', + 'handler' + ); + $result = $this->router->dispatch($this->request); + $expected = ['handler', ['name' => 'test']]; + $this->assertSame($expected, $result); + $this->assertTrue($this->cache->has('routerData')); + } + + public function testCachedDataIsUsed() + { + $data = [ + 0 => [], + 1 => [ + 'HEAD' => [ + 0 => [ + 'regex' => '~^(?|/([^/]+)/uri)$~', + 'routeMap' => [ + 2 => [ + 0 => 'handler', + 1 => [ + 'name' => 'name' + ], + ], + ], + ], + ], + ], + ]; + $this->cache->set('routerData', $data); + $result = $this->router->dispatch($this->request); + $expected = ['handler', ['name' => 'test']]; + $this->assertSame($expected, $result); + } + + public function testCacheDisabledUsesSimpleRouter() + { + $router = new CachedRouter( + $this->cache, + 'routerData', + Router::GROUP_COUNT, + [ + [ + RequestMethodInterface::METHOD_HEAD, + '/{name}/uri', + 'handler', + ], + ], + true + ); + $result = $router->dispatch($this->request); + $expected = ['handler', ['name' => 'test']]; + $this->assertSame($expected, $result); + $this->assertFalse($this->cache->has('routerData')); + } +} diff --git a/tests/InvalidArgumentExceptionTest.php b/tests/InvalidArgumentExceptionTest.php new file mode 100644 index 0000000..135c074 --- /dev/null +++ b/tests/InvalidArgumentExceptionTest.php @@ -0,0 +1,27 @@ + + * @copyright 2017 atanvarno.com + * @license https://opensource.org/licenses/MIT The MIT License + */ + +namespace Atanvarno\Router\Test; + +/** SPL use block. */ +use InvalidArgumentException as SplInvalidArgumentException; + +/** PHP Unit use block. */ +use PHPUnit\Framework\TestCase; + +/** Package use block. */ +use Atanvarno\Router\Exception\InvalidArgumentException; + +class InvalidArgumentExceptionTest extends TestCase +{ + public function testExtendsSplInvalidArgumentException() + { + $exception = new InvalidArgumentException(); + $this->assertInstanceOf(SplInvalidArgumentException::class, $exception); + } +} diff --git a/tests/MethodNotAllowedExceptionTest.php b/tests/MethodNotAllowedExceptionTest.php new file mode 100644 index 0000000..1b8db44 --- /dev/null +++ b/tests/MethodNotAllowedExceptionTest.php @@ -0,0 +1,61 @@ + + * @copyright 2017 atanvarno.com + * @license https://opensource.org/licenses/MIT The MIT License + */ + +namespace Atanvarno\Router\Test; + +/** SPL use block. */ +use UnexpectedValueException; + +/** HTTP Message Utilities use block. */ +use Fig\Http\Message\RequestMethodInterface; + +/** PHP Unit use block. */ +use PHPUnit\Framework\TestCase; + +/** Package use block. */ +use Atanvarno\Router\Exception\MethodNotAllowedException; + +class MethodNotAllowedExceptionTest extends TestCase +{ + private $exception; + + public function setUp() + { + $this->exception = new MethodNotAllowedException( + [ + RequestMethodInterface::METHOD_GET, + RequestMethodInterface::METHOD_HEAD, + ], + RequestMethodInterface::METHOD_POST + ); + } + + public function testExtendsOutOfBoundsException() + { + $this->assertInstanceOf( + UnexpectedValueException::class, + $this->exception + ); + } + + public function testCaught() + { + try { + throw $this->exception; + } catch (MethodNotAllowedException $caught) { + $this->assertInstanceOf(UnexpectedValueException::class, $caught); + $expectedMessage = 'POST is not allowed for this route'; + $this->assertSame($expectedMessage, $caught->getMessage()); + $expectedAllowed = [ + RequestMethodInterface::METHOD_GET, + RequestMethodInterface::METHOD_HEAD + ]; + $this->assertSame($expectedAllowed, $caught->getAllowed()); + } + } +} diff --git a/tests/NotFoundExceptionTest.php b/tests/NotFoundExceptionTest.php new file mode 100644 index 0000000..5d4528e --- /dev/null +++ b/tests/NotFoundExceptionTest.php @@ -0,0 +1,27 @@ + + * @copyright 2017 atanvarno.com + * @license https://opensource.org/licenses/MIT The MIT License + */ + +namespace Atanvarno\Router\Test; + +/** SPL use block. */ +use OutOfBoundsException; + +/** PHP Unit use block. */ +use PHPUnit\Framework\TestCase; + +/** Package use block. */ +use Atanvarno\Router\Exception\NotFoundException; + +class NotFoundExceptionTest extends TestCase +{ + public function testExtendsOutOfBoundsException() + { + $exception = new NotFoundException(); + $this->assertInstanceOf(OutOfBoundsException::class, $exception); + } +} diff --git a/tests/SimpleRouterTest.php b/tests/SimpleRouterTest.php new file mode 100644 index 0000000..8c1911d --- /dev/null +++ b/tests/SimpleRouterTest.php @@ -0,0 +1,309 @@ + + * @copyright 2017 atanvarno.com + * @license https://opensource.org/licenses/MIT The MIT License + */ + +namespace Atanvarno\Router\Test; + +/** PSR-17 use block. */ +use Http\Factory\Diactoros\{ + RequestFactory, UriFactory +}; + +/** HTTP Message Utilities use block. */ +use Fig\Http\Message\RequestMethodInterface; + +/** PHP Unit use block. */ +use PHPUnit\Framework\TestCase; + +/** Package use block. */ +use Atanvarno\Router\{ + Router, + SimpleRouter +}; +use Atanvarno\Router\Exception\{ + InvalidArgumentException, MethodNotAllowedException, NotFoundException +}; + +class SimpleRouterTest extends TestCase +{ + /** @var Router $router */ + private $router; + + public function setUp() + { + $this->router = new SimpleRouter(); + } + + public function testImplementsInterface() + { + $this->assertInstanceOf(Router::class, $this->router); + } + + public function testConstructorWithValidDrivers() + { + foreach (Router::VALID_DRIVERS as $driver) { + $router = new SimpleRouter($driver); + $this->assertInstanceOf(Router::class, $router); + } + } + + public function testConstructorRejectsInvalidDriver() + { + $this->expectException(InvalidArgumentException::class); + new SimpleRouter('invalid'); + } + + public function testConstructorRejectsNonArrayRouteDefinition() + { + $this->expectException(InvalidArgumentException::class); + new SimpleRouter(Router::GROUP_COUNT, ['route']); + } + + public function testConstructorRejectsTooShortRouteDefinition() + { + $this->expectException(InvalidArgumentException::class); + new SimpleRouter( + Router::GROUP_COUNT, + [[RequestMethodInterface::METHOD_HEAD, '/pattern']] + ); + } + + public function testConstructorRejectsTooLongRouteDefinition() + { + $this->expectException(InvalidArgumentException::class); + new SimpleRouter( + Router::GROUP_COUNT, + [ + [ + RequestMethodInterface::METHOD_HEAD, + '/pattern', + 'handler', + 'extra' + ] + ] + ); + } + + public function testConstructorCorrectlyBubblesAddException() + { + $this->expectException(InvalidArgumentException::class); + new SimpleRouter( + Router::GROUP_COUNT, + [['invalid', '/pattern', 'handler']] + ); + } + + public function testAdd() + { + $result = $this->router->add( + RequestMethodInterface::METHOD_HEAD, '/pattern', 'handler' + ); + $expected = [ + [RequestMethodInterface::METHOD_HEAD, '/pattern', 'handler'], + ]; + $this->assertAttributeEquals($expected, 'routes', $this->router); + $this->assertTrue($result); + } + + public function testAddWithArrayMethods() + { + $this->router->add( + [ + RequestMethodInterface::METHOD_HEAD, + RequestMethodInterface::METHOD_POST + ], + '/pattern', + 'handler' + ); + $expected = [ + [ + [ + RequestMethodInterface::METHOD_HEAD, + RequestMethodInterface::METHOD_POST + ], + '/pattern', + 'handler' + ], + ]; + $this->assertAttributeEquals($expected, 'routes', $this->router); + } + + public function testAddNormalisesPattern() + { + $this->router->add( + RequestMethodInterface::METHOD_HEAD, 'pattern/', 'handler' + ); + $expected = [ + [RequestMethodInterface::METHOD_HEAD, '/pattern', 'handler'], + ]; + $this->assertAttributeEquals($expected, 'routes', $this->router); + } + + public function testAddRejectsNonStringSingleMethod() + { + $this->expectException(InvalidArgumentException::class); + $this->router->add(5, '/pattern', 'handler'); + } + + public function testAddRejectsNonStringArrayMethod() + { + $this->expectException(InvalidArgumentException::class); + $this->router->add( + [RequestMethodInterface::METHOD_HEAD, 5], '/pattern', 'handler' + ); + } + + public function testAddRejectsInvalidSingleMethod() + { + $this->expectException(InvalidArgumentException::class); + $this->router->add('INVALID', '/pattern', 'handler'); + } + + public function testAddRejectsInvalidArrayMethod() + { + $this->expectException(InvalidArgumentException::class); + $this->router->add( + [RequestMethodInterface::METHOD_HEAD, 'INVALID'], + '/pattern', + 'handler' + ); + } + + public function testDispatch() + { + $uri = (new UriFactory())->createUri('http://atanvarno.com/test/uri/'); + foreach (Router::VALID_HTTP_METHODS as $method) { + $this->router->add($method, '/{name}/uri', 'handler'); + $request = (new RequestFactory())->createRequest($method, $uri); + $result = $this->router->dispatch($request); + $expected = ['handler', ['name' => 'test']]; + $this->assertSame($expected, $result); + $this->setUp(); + } + } + + public function testDispatchWithNotFound() + { + $uri = (new UriFactory())->createUri('http://atanvarno.com/test/uri/'); + $request = (new RequestFactory()) + ->createRequest(RequestMethodInterface::METHOD_HEAD, $uri); + $this->expectException(NotFoundException::class); + $this->router->dispatch($request); + } + + public function testDispatchWithNotAllowed() + { + $this->router->add( + RequestMethodInterface::METHOD_GET, '/{name}/uri', 'handler' + ); + $uri = (new UriFactory())->createUri('http://atanvarno.com/test/uri/'); + $request = (new RequestFactory()) + ->createRequest(RequestMethodInterface::METHOD_POST, $uri); + $this->expectException(MethodNotAllowedException::class); + $this->router->dispatch($request); + } + + public function testConnect() + { + $result = $this->router->connect('/pattern', 'handler'); + $expected = [ + [RequestMethodInterface::METHOD_CONNECT, '/pattern', 'handler'], + ]; + $this->assertAttributeEquals($expected, 'routes', $this->router); + $this->assertTrue($result); + } + + public function testDelete() + { + $result = $this->router->delete('/pattern', 'handler'); + $expected = [ + [RequestMethodInterface::METHOD_DELETE, '/pattern', 'handler'], + ]; + $this->assertAttributeEquals($expected, 'routes', $this->router); + $this->assertTrue($result); + } + + public function testGet() + { + $result = $this->router->get('/pattern', 'handler'); + $expected = [ + [RequestMethodInterface::METHOD_GET, '/pattern', 'handler'], + ]; + $this->assertAttributeEquals($expected, 'routes', $this->router); + $this->assertTrue($result); + } + + public function testHead() + { + $result = $this->router->head('/pattern', 'handler'); + $expected = [ + [RequestMethodInterface::METHOD_HEAD, '/pattern', 'handler'], + ]; + $this->assertAttributeEquals($expected, 'routes', $this->router); + $this->assertTrue($result); + } + + public function testOptions() + { + $result = $this->router->options('/pattern', 'handler'); + $expected = [ + [RequestMethodInterface::METHOD_OPTIONS, '/pattern', 'handler'], + ]; + $this->assertAttributeEquals($expected, 'routes', $this->router); + $this->assertTrue($result); + } + + public function testPatch() + { + $result = $this->router->patch('/pattern', 'handler'); + $expected = [ + [RequestMethodInterface::METHOD_PATCH, '/pattern', 'handler'], + ]; + $this->assertAttributeEquals($expected, 'routes', $this->router); + $this->assertTrue($result); + } + + public function testPost() + { + $result = $this->router->post('/pattern', 'handler'); + $expected = [ + [RequestMethodInterface::METHOD_POST, '/pattern', 'handler'], + ]; + $this->assertAttributeEquals($expected, 'routes', $this->router); + $this->assertTrue($result); + } + + public function testPurge() + { + $result = $this->router->purge('/pattern', 'handler'); + $expected = [ + [RequestMethodInterface::METHOD_PURGE, '/pattern', 'handler'], + ]; + $this->assertAttributeEquals($expected, 'routes', $this->router); + $this->assertTrue($result); + } + + public function testPut() + { + $result = $this->router->put('/pattern', 'handler'); + $expected = [ + [RequestMethodInterface::METHOD_PUT, '/pattern', 'handler'], + ]; + $this->assertAttributeEquals($expected, 'routes', $this->router); + $this->assertTrue($result); + } + + public function testTrace() + { + $result = $this->router->trace('/pattern', 'handler'); + $expected = [ + [RequestMethodInterface::METHOD_TRACE, '/pattern', 'handler'], + ]; + $this->assertAttributeEquals($expected, 'routes', $this->router); + $this->assertTrue($result); + } +}