diff --git a/.gitignore b/.gitignore index 37a78ec..8e8c373 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ -/vendor .DS_Store -composer.lock +/.php_cs.cache +/.phpunit.result.cache +/composer.lock +/vendor/ diff --git a/.php_cs.dist b/.php_cs.dist new file mode 100644 index 0000000..6883227 --- /dev/null +++ b/.php_cs.dist @@ -0,0 +1,25 @@ +in(__DIR__ . '/src') + ->in(__DIR__ . '/tests'); + +return PhpCsFixer\Config::create() + ->setRules([ + '@PSR2' => true, + '@Symfony' => true, + 'array_syntax' => ['syntax' => 'short'], + 'blank_line_before_statement' => false, + 'concat_space' => ['spacing' => 'one'], + 'function_declaration' => ['closure_function_spacing' => 'none'], + 'increment_style' => false, + 'phpdoc_align' => ['align' => 'left'], + 'phpdoc_separation' => false, + 'phpdoc_summary' => false, + 'phpdoc_to_comment' => false, + 'yoda_style' => false, + ]) + ->setFinder($finder); + +// __END__ +// vim: filetype=php diff --git a/.travis.yml b/.travis.yml index 5ea0a15..0c4845d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,10 @@ language: php php: - - 5.6 - - 7.0 - 7.1 - 7.2 + - 7.3 + - 7.4 cache: directories: @@ -16,9 +16,11 @@ install: script: - mkdir -p build/logs - - php ./vendor/bin/phpunit --coverage-clover build/logs/clover.xml + - ./vendor/bin/psalm --no-progress + - ./vendor/bin/php-cs-fixer fix -v --dry-run --stop-on-violation --using-cache=no + - ./vendor/bin/phpunit --verbose --coverage-clover build/logs/clover.xml after_script: - - travis_retry php ./vendor/bin/coveralls + - travis_retry php ./vendor/bin/php-coveralls sudo: false diff --git a/benchmarks/.gitignore b/benchmarks/.gitignore new file mode 100644 index 0000000..c8153b5 --- /dev/null +++ b/benchmarks/.gitignore @@ -0,0 +1,2 @@ +/composer.lock +/vendor/ diff --git a/benchmarks/composer.json b/benchmarks/composer.json new file mode 100644 index 0000000..4a3314d --- /dev/null +++ b/benchmarks/composer.json @@ -0,0 +1,13 @@ +{ + "require": { + "nikic/fast-route": "^1.0", + "php": ">=7.1", + "phpbench/phpbench": "^0.17" + }, + "autoload": { + "psr-4": { + "Emonkak\\Router\\": "../src/", + "Emonkak\\Router\\Benchmarks\\": "src/" + } + } +} diff --git a/benchmarks/phpbench.json b/benchmarks/phpbench.json new file mode 100644 index 0000000..355f124 --- /dev/null +++ b/benchmarks/phpbench.json @@ -0,0 +1,3 @@ +{ + "bootstrap": "vendor/autoload.php" +} diff --git a/benchmarks/AbstractRoutableRouterBench.php b/benchmarks/src/AbstractRoutableRouterBench.php similarity index 62% rename from benchmarks/AbstractRoutableRouterBench.php rename to benchmarks/src/AbstractRoutableRouterBench.php index 0ff2d8d..222b05b 100644 --- a/benchmarks/AbstractRoutableRouterBench.php +++ b/benchmarks/src/AbstractRoutableRouterBench.php @@ -1,5 +1,7 @@ prepareRouter() - ->route('/', 0) - ->route('/foo/', 1) - ->route('/bar/', 2) - ->route('/baz/', 3) - ->route('/foo/:first', 4) - ->route('/bar/:first', 5) - ->route('/baz/:first', 6) - ->route('/foo/:first/qux', 7) - ->route('/bar/:first/quux', 8) - ->route('/baz/:first/foobar', 9) - ->route('/foo/:first/qux/:second', 10) - ->route('/bar/:first/quux/:second', 11) - ->route('/baz/:first/foobar/:second', 12); + $router = $this->prepareRouter(); + $router->addroute('/', 0); + $router->addroute('/foo/', 1); + $router->addroute('/bar/', 2); + $router->addroute('/baz/', 3); + $router->addroute('/foo/:first', 4); + $router->addroute('/bar/:first', 5); + $router->addroute('/baz/:first', 6); + $router->addroute('/foo/:first/qux', 7); + $router->addroute('/bar/:first/quux', 8); + $router->addroute('/baz/:first/foobar', 9); + $router->addroute('/foo/:first/qux/:second', 10); + $router->addroute('/bar/:first/quux/:second', 11); + $router->addroute('/baz/:first/foobar/:second', 12); } abstract protected function prepareRouter(); diff --git a/benchmarks/FastRouteBench.php b/benchmarks/src/FastRouteBench.php similarity index 98% rename from benchmarks/FastRouteBench.php rename to benchmarks/src/FastRouteBench.php index 21c8b7d..ab6727b 100644 --- a/benchmarks/FastRouteBench.php +++ b/benchmarks/src/FastRouteBench.php @@ -1,5 +1,7 @@ =5.6" + "php": ">=7.1" }, "require-dev": { - "nikic/fast-route": "^1.0", - "phpbench/phpbench": "^0.10.0", - "phpunit/phpunit": "^5.7", - "satooshi/php-coveralls": "^1.0" + "phpunit/phpunit": "^7.0", + "php-coveralls/php-coveralls": "^2.0", + "vimeo/psalm": "^3.11", + "friendsofphp/php-cs-fixer": "^2.16" }, "autoload": { "psr-4": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index c98b18f..b48a411 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -4,7 +4,7 @@ - + ./tests/ diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..46206b5 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/src/AbstractRouterBuilder.php b/src/AbstractRouterBuilder.php index 7dc3839..99262df 100644 --- a/src/AbstractRouterBuilder.php +++ b/src/AbstractRouterBuilder.php @@ -1,97 +1,96 @@ > */ protected $routes = []; /** - * @param string $method - * @param string $path - * @param mixed $handler + * @param THandler $handler * @return $this */ - public function route($method, $path, $handler) + public function get(string $path, $handler): self { - if (!isset($this->routes[$path])) { - $this->routes[$path] = []; - } - - $this->routes[$path][$method] = $handler; - - return $this; + return $this->route('GET', $path, $handler); } /** - * @param string $path - * @param mixed $handler + * @param THandler $handler * @return $this */ - public function get($path, $handler) + public function post(string $path, $handler): self { - return $this->route('GET', $path, $handler); + return $this->route('POST', $path, $handler); } /** - * @param string $path - * @param mixed $handler + * @param THandler $handler * @return $this */ - public function post($path, $handler) + public function delete(string $path, $handler): self { - return $this->route('POST', $path, $handler); + return $this->route('DELETE', $path, $handler); } /** - * @param string $path - * @param mixed $handler + * @param THandler $handler * @return $this */ - public function delete($path, $handler) + public function put(string $path, $handler): self { - return $this->route('DELETE', $path, $handler); + return $this->route('PUT', $path, $handler); } /** - * @param string $path - * @param mixed $handler + * @param THandler $handler * @return $this */ - public function put($path, $handler) + public function patch(string $path, $handler): self { - return $this->route('PUT', $path, $handler); + return $this->route('PATCH', $path, $handler); } /** - * @param string $path - * @param mixed $handler + * @param THandler $handler * @return $this */ - public function patch($path, $handler) + public function route(string $method, string $path, $handler): self { - return $this->route('PATCH', $path, $handler); + if (!isset($this->routes[$path])) { + $this->routes[$path] = []; + } + + $this->routes[$path][$method] = $handler; + + return $this; } /** - * @return RouterInterface + * @return RouterInterface */ - public function build() + public function build(): RouterInterface { $router = $this->prepareRouter(); foreach ($this->routes as $path => $route) { - $router->route($path, $route); + $router->addRoute($path, $route); } return $router; } /** - * @return RoutableRouterInterface + * @return RoutableRouterInterface */ - abstract protected function prepareRouter(); + abstract protected function prepareRouter(): RoutableRouterInterface; } diff --git a/src/CompositeRouter.php b/src/CompositeRouter.php index 0475b95..7df148b 100644 --- a/src/CompositeRouter.php +++ b/src/CompositeRouter.php @@ -1,16 +1,22 @@ [] */ private $routers; /** - * @param array $routers + * @param RouterInterface[] $routers */ public function __construct(array $routers) { @@ -18,9 +24,9 @@ public function __construct(array $routers) } /** - * {@inheritDoc} + * {@inheritdoc} */ - public function match($path) + public function match(string $path): ?array { foreach ($this->routers as $prefix => $router) { if (strpos($path, $prefix) === 0) { diff --git a/src/RegexpRouter.php b/src/RegexpRouter.php index 2985142..b873bb1 100644 --- a/src/RegexpRouter.php +++ b/src/RegexpRouter.php @@ -1,7 +1,13 @@ + */ class RegexpRouter implements RoutableRouterInterface { /** @@ -15,19 +21,19 @@ class RegexpRouter implements RoutableRouterInterface private $capturePatterns = []; /** - * @var mixed[] + * @var THandler[] */ private $handlers = []; /** - * @var integer + * @var int */ private $maxPatternLength = 0; /** - * {@inheritDoc} + * @param THandler $handler */ - public function route($path, $handler) + public function addRoute(string $path, $handler): void { if ($path === '/') { $matchPattern = '/'; @@ -54,14 +60,12 @@ public function route($path, $handler) $this->capturePatterns[] = $capturePattern; $this->handlers[] = $handler; $this->maxPatternLength = max(strlen($matchPattern), $this->maxPatternLength); - - return $this; } /** - * {@inheritDoc} + * {@inheritdoc} */ - public function match($path) + public function match(string $path): ?array { $chunkSize = $this->computeChunkSize(); if ($chunkSize < 1) { @@ -84,20 +88,16 @@ public function match($path) } } - /** - * @return integer - */ - protected function computeChunkSize() + protected function computeChunkSize(): int { return (int) ((0x8000 - 9) / ($this->maxPatternLength + 3)); } /** - * @param string $path * @param string[] $patternChunk - * @return array|null + * @return ?array{0:THandler,1:string[]} */ - protected function processChunk($path, array $patternChunk) + protected function processChunk(string $path, array $patternChunk) { $pattern = '#^(?:' . implode('()|', $patternChunk) . '())$#'; diff --git a/src/RegexpRouterBuilder.php b/src/RegexpRouterBuilder.php index 8ff0b36..6bc81c0 100644 --- a/src/RegexpRouterBuilder.php +++ b/src/RegexpRouterBuilder.php @@ -1,13 +1,19 @@ + */ class RegexpRouterBuilder extends AbstractRouterBuilder { /** - * {@inheritDoc} + * @return RegexpRouter */ - public function prepareRouter() + public function prepareRouter(): RoutableRouterInterface { return new RegexpRouter(); } diff --git a/src/RoutableRouterInterface.php b/src/RoutableRouterInterface.php index 208c72f..4cfee06 100644 --- a/src/RoutableRouterInterface.php +++ b/src/RoutableRouterInterface.php @@ -1,13 +1,18 @@ + */ interface RoutableRouterInterface extends RouterInterface { /** - * @param string $path - * @param mixed $handler - * @return $this + * @param THandler $handler */ - public function route($path, $handler); + public function addRoute(string $path, $handler): void; } diff --git a/src/RouterInterface.php b/src/RouterInterface.php index e5a2b72..ba2540d 100644 --- a/src/RouterInterface.php +++ b/src/RouterInterface.php @@ -1,15 +1,19 @@ + */ class TrieRouter implements RoutableRouterInterface { const CHILDREN = 0; - const NAME = 1; - const HANDLER = 2; + const NAME = 1; + const HANDLER = 2; const WILDCARD = '*'; @@ -19,9 +25,9 @@ class TrieRouter implements RoutableRouterInterface ]; /** - * {@inheritDoc} + * @param THandler $handler */ - public function route($path, $handler) + public function addRoute(string $path, $handler): void { $node = &$this->root; @@ -51,14 +57,12 @@ public function route($path, $handler) } $node[self::HANDLER] = $handler; - - return $this; } /** - * {@inheritDoc} + * {@inheritdoc} */ - public function match($path) + public function match(string $path): ?array { $node = $this->root; $params = []; @@ -87,7 +91,7 @@ public function match($path) return [$node[self::HANDLER], $params]; } - public function trimRootSlash($path) + public function trimRootSlash(string $path): string { return $path !== '' && $path[0] === '/' ? substr($path, 1) : $path; } diff --git a/src/TrieRouterBuilder.php b/src/TrieRouterBuilder.php index 3f7a216..6c559b3 100644 --- a/src/TrieRouterBuilder.php +++ b/src/TrieRouterBuilder.php @@ -1,13 +1,19 @@ + */ class TrieRouterBuilder extends AbstractRouterBuilder { /** - * {@inheritDoc} + * @return TrieRouter */ - public function prepareRouter() + protected function prepareRouter(): RoutableRouterInterface { return new TrieRouter(); } diff --git a/tests/AbstractRoutableRouterTest.php b/tests/AbstractRoutableRouterTest.php index c26922c..0602340 100644 --- a/tests/AbstractRoutableRouterTest.php +++ b/tests/AbstractRoutableRouterTest.php @@ -2,27 +2,29 @@ namespace Emonkak\Router\Tests; -abstract class AbstractRoutableRouterTest extends \PHPUnit_Framework_TestCase +use PHPUnit\Framework\TestCase; + +abstract class AbstractRoutableRouterTest extends TestCase { /** * @dataProvider providerMatch */ public function testMatch($path, $expectedMetadata, array $expectedParams) { - $router = $this->prepareRouter() - ->route('/', 0) - ->route('/foo', 1) - ->route('/bar', 2) - ->route('/baz', 3) - ->route('/foo/:first', 4) - ->route('/bar/:first', 5) - ->route('/baz/:first', 6) - ->route('/foo/:first/qux', 7) - ->route('/bar/:first/quux', 8) - ->route('/baz/:first/foobar', 9) - ->route('/foo/:first/qux/:second', 10) - ->route('/bar/:first/quux/:second', 11) - ->route('/baz/:first/foobar/:second', 12); + $router = $this->prepareRouter(); + $router->addRoute('/', 0); + $router->addRoute('/foo', 1); + $router->addRoute('/bar', 2); + $router->addRoute('/baz', 3); + $router->addRoute('/foo/:first', 4); + $router->addRoute('/bar/:first', 5); + $router->addRoute('/baz/:first', 6); + $router->addRoute('/foo/:first/qux', 7); + $router->addRoute('/bar/:first/quux', 8); + $router->addRoute('/baz/:first/foobar', 9); + $router->addRoute('/foo/:first/qux/:second', 10); + $router->addRoute('/bar/:first/quux/:second', 11); + $router->addRoute('/baz/:first/foobar/:second', 12); $match = $router->match($path); $this->assertEquals([$expectedMetadata, $expectedParams], $match); @@ -63,20 +65,20 @@ public function providerMatch() */ public function testMatchFailure($path) { - $router = $this->prepareRouter() - ->route('/', 0) - ->route('/foo', 1) - ->route('/bar', 2) - ->route('/baz', 3) - ->route('/foo/:first', 4) - ->route('/bar/:first', 5) - ->route('/baz/:first', 6) - ->route('/foo/:first/qux', 7) - ->route('/bar/:first/quux', 8) - ->route('/baz/:first/foobar', 9) - ->route('/foo/:first/qux/:second', 10) - ->route('/bar/:first/quux/:second', 11) - ->route('/baz/:first/foobar/:second', 12); + $router = $this->prepareRouter(); + $router->addRoute('/', 0); + $router->addRoute('/foo', 1); + $router->addRoute('/bar', 2); + $router->addRoute('/baz', 3); + $router->addRoute('/foo/:first', 4); + $router->addRoute('/bar/:first', 5); + $router->addRoute('/baz/:first', 6); + $router->addRoute('/foo/:first/qux', 7); + $router->addRoute('/bar/:first/quux', 8); + $router->addRoute('/baz/:first/foobar', 9); + $router->addRoute('/foo/:first/qux/:second', 10); + $router->addRoute('/bar/:first/quux/:second', 11); + $router->addRoute('/baz/:first/foobar/:second', 12); $match = $router->match($path); $this->assertNull($match); @@ -101,7 +103,7 @@ public function testChunkedMatch() $router = $this->prepareRouter(); for ($i = 0; $i < 100; $i++) { - $router->route(str_repeat('/foo', $i), $i); + $router->addRoute(str_repeat('/foo', $i), $i); } $match = $router->match('/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo'); diff --git a/tests/AbstractRouterBuilderTest.php b/tests/AbstractRouterBuilderTest.php index 3de47f6..246fbe5 100644 --- a/tests/AbstractRouterBuilderTest.php +++ b/tests/AbstractRouterBuilderTest.php @@ -2,9 +2,9 @@ namespace Emonkak\Router\Tests; -use Emonkak\Router\RegexpRouterBuilder; +use PHPUnit\Framework\TestCase; -abstract class AbstractRouterBuilderTest extends \PHPUnit_Framework_TestCase +abstract class AbstractRouterBuilderTest extends TestCase { public function testBuild() { @@ -18,7 +18,7 @@ public function testBuild() $this->assertEquals([ ['GET' => 'IndexUser'], - [] + [], ], $router->match('/users')); $this->assertEquals([ [ @@ -27,7 +27,7 @@ public function testBuild() 'PUT' => 'UpdateUser', 'DELETE' => 'DestroyUser', ], - ['user_id' => '123'] + ['user_id' => '123'], ], $router->match('/users/123')); } } diff --git a/tests/CompositeRouterTest.php b/tests/CompositeRouterTest.php index fd94b0c..a88ab05 100644 --- a/tests/CompositeRouterTest.php +++ b/tests/CompositeRouterTest.php @@ -2,10 +2,11 @@ namespace Emonkak\Router\Tests; -use Emonkak\Router\RouterInterface; use Emonkak\Router\CompositeRouter; +use Emonkak\Router\RouterInterface; +use PHPUnit\Framework\TestCase; -class CompositeRouterTest extends \PHPUnit_Framework_TestCase +class CompositeRouterTest extends TestCase { public function testMatch() { @@ -14,21 +15,21 @@ public function testMatch() ->expects($this->once()) ->method('match') ->with($this->identicalTo('/qux')) - ->will($this->returnArgument(0)); + ->willReturn(['/qux', []]); $router2 = $this->createMock(RouterInterface::class); $router2 ->expects($this->once()) ->method('match') ->with($this->identicalTo('/quux')) - ->will($this->returnArgument(0)); + ->willReturn(['/quux', []]); $router3 = $this->createMock(RouterInterface::class); $router3 ->expects($this->once()) ->method('match') ->with($this->identicalTo('/baz/corge')) - ->will($this->returnArgument(0)); + ->willReturn(['/baz/corge', []]); $compositeRouter = new CompositeRouter([ '/foo/' => $router1, @@ -36,9 +37,9 @@ public function testMatch() '/' => $router3, ]); - $this->assertSame('/qux', $compositeRouter->match('/foo/qux')); - $this->assertSame('/quux', $compositeRouter->match('/bar/quux')); - $this->assertSame('/baz/corge', $compositeRouter->match('/baz/corge')); + $this->assertSame(['/qux', []], $compositeRouter->match('/foo/qux')); + $this->assertSame(['/quux', []], $compositeRouter->match('/bar/quux')); + $this->assertSame(['/baz/corge', []], $compositeRouter->match('/baz/corge')); } public function testMatchFailure() @@ -48,26 +49,26 @@ public function testMatchFailure() ->expects($this->once()) ->method('match') ->with($this->identicalTo('/qux')) - ->will($this->returnArgument(0)); + ->willReturn(['/qux', []]); $router2 = $this->createMock(RouterInterface::class); $router2 ->expects($this->once()) ->method('match') ->with($this->identicalTo('/quux')) - ->will($this->returnArgument(0)); + ->willReturn(['/quux', []]); $router3 = $this->createMock(RouterInterface::class); $router3 ->expects($this->at(0)) ->method('match') ->with($this->identicalTo('/baz/corge')) - ->will($this->returnArgument(0)); + ->willReturn(['/baz/corge', []]); $router3 ->expects($this->at(1)) ->method('match') ->with($this->identicalTo('/')) - ->will($this->returnArgument(0)); + ->willReturn(['/', []]); $compositeRouter = new CompositeRouter([ '/foo/' => $router1, @@ -75,10 +76,10 @@ public function testMatchFailure() '/' => $router3, ]); - $this->assertSame('/qux', $compositeRouter->match('/foo/qux')); - $this->assertSame('/quux', $compositeRouter->match('/bar/quux')); - $this->assertSame('/baz/corge', $compositeRouter->match('/baz/corge')); - $this->assertSame('/', $compositeRouter->match('/')); + $this->assertSame(['/qux', []], $compositeRouter->match('/foo/qux')); + $this->assertSame(['/quux', []], $compositeRouter->match('/bar/quux')); + $this->assertSame(['/baz/corge', []], $compositeRouter->match('/baz/corge')); + $this->assertSame(['/', []], $compositeRouter->match('/')); $this->assertNull((new CompositeRouter([]))->match('/')); } } diff --git a/tests/RegexpRouterBuilderTest.php b/tests/RegexpRouterBuilderTest.php index 32e6925..ff080f5 100644 --- a/tests/RegexpRouterBuilderTest.php +++ b/tests/RegexpRouterBuilderTest.php @@ -5,8 +5,8 @@ use Emonkak\Router\RegexpRouterBuilder; /** - * @covers Emonkak\Router\RegexpRouterBuilder - * @covers Emonkak\Router\AbstractRouterBuilder + * @covers \Emonkak\Router\RegexpRouterBuilder + * @covers \Emonkak\Router\AbstractRouterBuilder */ class RegexpRouterBuilderTest extends AbstractRouterBuilderTest { diff --git a/tests/RegexpRouterTest.php b/tests/RegexpRouterTest.php index e73f8d9..98dd65f 100644 --- a/tests/RegexpRouterTest.php +++ b/tests/RegexpRouterTest.php @@ -5,29 +5,28 @@ use Emonkak\Router\RegexpRouter; /** - * @covers Emonkak\Router\RegexpRouter + * @covers \Emonkak\Router\RegexpRouter */ class RegexpRouterTest extends AbstractRoutableRouterTest { - /** - * @expectedException OverflowException - */ public function testMatchThrowsOverflowException() { + $this->expectException(\OverflowException::class); + $path = str_repeat('/foo', 10000); - $router = (new RegexpRouter()) - ->route($path, 'bar'); + $router = new RegexpRouter(); + $router->addRoute($path, 'bar'); $router->match($path); } public function testChunkedMatch() { - $router = (new RegexpRouter()); + $router = new RegexpRouter(); for ($i = 0; $i < 100; $i++) { - $router->route(str_repeat('/foo', $i), $i); + $router->addRoute(str_repeat('/foo', $i), $i); } $match = $router->match('/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo'); diff --git a/tests/TrieRouterBuilderTest.php b/tests/TrieRouterBuilderTest.php index fc2eb76..9fd075f 100644 --- a/tests/TrieRouterBuilderTest.php +++ b/tests/TrieRouterBuilderTest.php @@ -5,8 +5,8 @@ use Emonkak\Router\TrieRouterBuilder; /** - * @covers Emonkak\Router\TrieRouterBuilder - * @covers Emonkak\Router\AbstractRouterBuilder + * @covers \Emonkak\Router\TrieRouterBuilder + * @covers \Emonkak\Router\AbstractRouterBuilder */ class TrieRouterBuilderTest extends AbstractRouterBuilderTest { diff --git a/tests/TrieRouterTest.php b/tests/TrieRouterTest.php index e4587be..be74aea 100644 --- a/tests/TrieRouterTest.php +++ b/tests/TrieRouterTest.php @@ -5,7 +5,7 @@ use Emonkak\Router\TrieRouter; /** - * @covers Emonkak\Router\TrieRouter + * @covers \Emonkak\Router\TrieRouter */ class TrieRouterTest extends AbstractRoutableRouterTest {