diff options
author | Arnold Daniels <arnold@jasny.net> | 2016-10-12 21:25:44 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-10-12 21:25:44 +0200 |
commit | 2943ae006845288475caf271a81db1e8bff4c0cb (patch) | |
tree | 91b3a426fa45f5542753a9758d35851758183796 | |
parent | 2f69176c84865f372c4c50de271ab743067951be (diff) | |
parent | f9df2a343453c80ac982cb46af60c7b40ace74fc (diff) | |
download | router-2943ae006845288475caf271a81db1e8bff4c0cb.zip router-2943ae006845288475caf271a81db1e8bff4c0cb.tar.gz router-2943ae006845288475caf271a81db1e8bff4c0cb.tar.bz2 |
Merge pull request #5 from Minstel/router-cleanup
Implementation and tests for Router. Minor fixes
-rw-r--r-- | src/Router.php | 121 | ||||
-rw-r--r-- | src/Router/Routes.php | 10 | ||||
-rw-r--r-- | src/Router/Runner.php | 10 | ||||
-rw-r--r-- | src/Router/Runner/Callback.php | 6 | ||||
-rw-r--r-- | src/Router/Runner/Controller.php | 20 | ||||
-rw-r--r-- | src/Router/Runner/PhpScript.php | 8 | ||||
-rw-r--r-- | tests/Router/Runner/CallbackTest.php | 6 | ||||
-rw-r--r-- | tests/Router/Runner/ControllerTest.php | 140 | ||||
-rw-r--r-- | tests/Router/Runner/PhpScriptTest.php | 4 | ||||
-rw-r--r-- | tests/Router/RunnerTest.php | 4 | ||||
-rw-r--r-- | tests/RouterTest.php | 227 |
11 files changed, 502 insertions, 54 deletions
diff --git a/src/Router.php b/src/Router.php index e0f3acb..955e1e1 100644 --- a/src/Router.php +++ b/src/Router.php @@ -2,9 +2,10 @@ namespace Jasny; -use Jasny\Router\Routes; -use Psr7\Http\Message\ServerRequest; -use Psr7\Http\Message\Response; +use Jasny\Router\Runner; +use Jasny\Router\Routes\Glob; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\ResponseInterface; /** * Route pretty URLs to correct controller @@ -13,17 +14,22 @@ class Router { /** * Specific routes - * @var Routes + * @var array */ - protected $routes; - + protected $routes = []; + + /** + * Middlewares actions + * @var array + **/ + protected $middlewares = []; /** * Class constructor * - * @param Routes $routes + * @param array $routes */ - public function __construct(Routes $routes) + public function __construct(array $routes) { $this->routes = $routes; } @@ -36,46 +42,109 @@ class Router public function getRoutes() { return $this->routes; + } + + /** + * Get middlewares + * + * @return array + */ + public function getMiddlewares() + { + return $this->middlewares; + } + + /** + * Add middleware call to router + * + * @param callback $middleware + * @return Router $this + */ + public function add($middleware) + { + if (!is_callable($middleware)) { + throw new \InvalidArgumentException("Middleware should be a callable"); + } + + $this->middlewares[] = $middleware; + + return $this; } - /** * Run the action for the request * - * @param ServerRequest $request - * @param Response $response - * @return Response + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * @return ResponseInterface */ - final public function run(ServerRequest $request, Response $response) + final public function run(ServerRequestInterface $request, ResponseInterface $response) { return $this->__invoke($request, $response); } /** - * Run the action for the request (optionally as middleware) + * Run the action for the request (optionally as middleware), previously running middlewares, if any * - * @param ServerRequest $request - * @param Response $response - * @param callback $next - * @return Response + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * @param callback $next + * @return ResponseInterface */ - public function __invoke(ServerRequest $request, Response $response, $next = null) + public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next = null) { - return $this->handle($request, $response, $next); + $handle = [$this, 'handle']; + + #Call to $this->handle will be executed last in the chain of middlewares + $next = function(ServerRequestInterface $request, ResponseInterface $response) use ($next, $handle) { + return call_user_func($handle, $request, $response, $next); + }; + + #Build middlewares call chain, so that the last added was executed in first place + foreach ($this->middlewares as $middleware) { + $next = function(ServerRequestInterface $request, ResponseInterface $response) use ($next, $middleware) { + return $middleware($request, $response, $next); + }; + } + + return $next($request, $response); } /** * Run the action * - * @param ServerRequest $request - * @param Response $response + * @param ServerRequestInterface $request + * @param ResponseInterface $response * @param callback $next - * @return Response + * @return ResponseInterface */ - protected function handle(ServerRequest $request, Response $response, $next = null) + protected function handle(ServerRequestInterface $request, ResponseInterface $response, $next = null) { - // TODO find route and run. - // TODO if not found -> 404 + $glob = new Glob($this->routes); + $route = $glob->getRoute($request); + + if (!$route) return $this->notFound($response); + + $runner = Runner::create($route); + + return $runner($request, $response, $next); + } + + /** + * Return 'Not Found' response + * + * @param ResponseInterface $response + * @return ResponseInterface + */ + protected function notFound(ResponseInterface $response) + { + $message = 'Not Found'; + + $body = $response->getBody(); + $body->rewind(); + $body->write($message); + + return $response->withStatus(404, $message)->withBody($body); } } diff --git a/src/Router/Routes.php b/src/Router/Routes.php index 26d407c..ce4fa1a 100644 --- a/src/Router/Routes.php +++ b/src/Router/Routes.php @@ -2,7 +2,7 @@ namespace Jasny\Router; -use Psr\Http\Message\ServerRequestInterface as ServerRequest; +use Psr\Http\Message\ServerRequestInterface; /** * Collection of routes @@ -12,16 +12,16 @@ interface Routes /** * Check if a route for the request exists * - * @param ServerRequest $request + * @param ServerRequestInterface $request * @return boolean */ - public function hasRoute(ServerRequest $request); + public function hasRoute(ServerRequestInterface $request); /** * Get route for the request * - * @param ServerRequest $request + * @param ServerRequestInterface $request * @return Route */ - public function getRoute(ServerRequest $request); + public function getRoute(ServerRequestInterface $request); } diff --git a/src/Router/Runner.php b/src/Router/Runner.php index 2c8d259..51300a4 100644 --- a/src/Router/Runner.php +++ b/src/Router/Runner.php @@ -2,7 +2,7 @@ namespace Jasny\Router; -use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; use Jasny\Router\Route; @@ -39,21 +39,21 @@ abstract class Runner /** * Invoke the action specified in the route * - * @param RequestInterface $request + * @param ServerRequestInterface $request * @param ResponseInterface $response * @return ResponseInterface */ - abstract public function run(RequestInterface $request, ResponseInterface $response); + abstract public function run(ServerRequestInterface $request, ResponseInterface $response); /** * Invoke the action specified in the route and call the next method * - * @param RequestInterface $request + * @param ServerRequestInterface $request * @param ResponseInterface $response * @param callback $next Callback for if runner is used as middleware * @return ResponseInterface */ - public function __invoke(RequestInterface $request, ResponseInterface $response, $next = null) + public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next = null) { $response = $this->run($request, $response); diff --git a/src/Router/Runner/Callback.php b/src/Router/Runner/Callback.php index cf1063b..34ebc4c 100644 --- a/src/Router/Runner/Callback.php +++ b/src/Router/Runner/Callback.php @@ -3,7 +3,7 @@ namespace Jasny\Router\Runner; use Jasny\Router\Runner; -use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; /** @@ -16,11 +16,11 @@ class Callback extends Runner /** * Use function to handle request and response * - * @param RequestInterface $request + * @param ServerRequestInterface $request * @param ResponseInterface $response * @return ResponseInterface|mixed */ - public function run(RequestInterface $request, ResponseInterface $response) + public function run(ServerRequestInterface $request, ResponseInterface $response) { $callback = !empty($this->route->fn) ? $this->route->fn : null; diff --git a/src/Router/Runner/Controller.php b/src/Router/Runner/Controller.php index ed8c060..0488be5 100644 --- a/src/Router/Runner/Controller.php +++ b/src/Router/Runner/Controller.php @@ -3,7 +3,7 @@ namespace Jasny\Router\Runner; use Jasny\Router\Runner; -use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; /** @@ -14,14 +14,26 @@ use Psr\Http\Message\ResponseInterface; class Controller extends Runner { /** - * Route to a file + * Route to a controller * - * @param RequestInterface $request + * @param ServerRequestInterface $request * @param ResponseInterface $response * @return ResponseInterface|mixed */ - public function run(RequestInterface $request, ResponseInterface $response) + public function run(ServerRequestInterface $request, ResponseInterface $response) { + $class = !empty($this->route->controller) ? $this->route->controller : null; + if (!class_exists($class)) { + throw new \RuntimeException("Can not route to controller '$class': class not exists"); + } + + if (!method_exists($class, '__invoke')) { + throw new \RuntimeException("Can not route to controller '$class': class does not have '__invoke' method"); + } + + $controller = new $class($this->route); + + return $controller($request, $response); } } diff --git a/src/Router/Runner/PhpScript.php b/src/Router/Runner/PhpScript.php index 43e4934..f6595a3 100644 --- a/src/Router/Runner/PhpScript.php +++ b/src/Router/Runner/PhpScript.php @@ -3,7 +3,7 @@ namespace Jasny\Router\Runner; use Jasny\Router\Runner; -use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; /** @@ -24,11 +24,11 @@ class PhpScript extends Runner /** * Route to a file * - * @param RequestInterface $request - * @param ResponseInterface $response + * @param ServerRequestInterface $request + * @param ResponseInterface $response * @return ResponseInterface|mixed */ - public function run(RequestInterface $request, ResponseInterface $response) + public function run(ServerRequestInterface $request, ResponseInterface $response) { $file = !empty($this->route->file) ? ltrim($this->route->file, '/') : ''; diff --git a/tests/Router/Runner/CallbackTest.php b/tests/Router/Runner/CallbackTest.php index 04bbd27..b4ee4ef 100644 --- a/tests/Router/Runner/CallbackTest.php +++ b/tests/Router/Runner/CallbackTest.php @@ -2,7 +2,7 @@ use Jasny\Router\Route; use Jasny\Router\Runner\Callback; -use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; class CallbackTest extends PHPUnit_Framework_TestCase @@ -19,7 +19,7 @@ class CallbackTest extends PHPUnit_Framework_TestCase $runner = new Callback($route); $this->assertEquals($route, $runner->getRoute(), "Route was not set correctly"); - $request = $this->createMock(RequestInterface::class); + $request = $this->createMock(ServerRequestInterface::class); $response = $this->createMock(ResponseInterface::class); if (!$positive) $this->expectException(\RuntimeException::class); @@ -53,7 +53,7 @@ class CallbackTest extends PHPUnit_Framework_TestCase /** * Testable callback for creating Route * - * @param RequestInterface $request + * @param ServerRequestInterface $request * @param ResponseInterface $response * @return array */ diff --git a/tests/Router/Runner/ControllerTest.php b/tests/Router/Runner/ControllerTest.php new file mode 100644 index 0000000..1d64622 --- /dev/null +++ b/tests/Router/Runner/ControllerTest.php @@ -0,0 +1,140 @@ +<?php + +use Jasny\Router\Route; +use Jasny\Router\Runner\Controller; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\ResponseInterface; + +class ControllerTest extends PHPUnit_Framework_TestCase +{ + /** + * Tmp scripts + * @var array + **/ + public static $files = []; + + /** + * Test creating Controller runner + * + * @dataProvider phpScriptProvider + * @param Route $route + * @param boolean $positive + */ + public function testPhpScript($route, $positive) + { + $runner = new Controller($route); + $this->assertEquals($route, $runner->getRoute(), "Route was not set correctly"); + + $request = $this->createMock(ServerRequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + + if (!$positive) $this->expectException(\RuntimeException::class); + $result = $runner->run($request, $response); + + $this->assertEquals($request, $result['request'], "Request object was not passed correctly to result"); + $this->assertEquals($response, $result['response'], "Response object was not passed correctly to result"); + } + + /** + * Provide data for testing 'create' method + */ + public function phpScriptProvider() + { + foreach (['noInvoke', 'withInvoke'] as $type) { + list($class, $path) = static::createTmpScript($type); + static::$files[$type] = compact('class', 'path'); + } + + return [ + [Route::create(['test' => 'test']), false], + [Route::create(['fn' => 'testFunction', 'value' => 'test']), false], + [Route::create(['controller' => 'TestController', 'value' => 'test']), false], + [Route::create(['controller' => '', 'value' => 'test']), false], + [Route::create(['controller' => static::$files['noInvoke']['class'], 'path' => static::$files['noInvoke']['path']]), false], + [Route::create(['controller' => static::$files['withInvoke']['class'], 'path' => static::$files['withInvoke']['path']]), true], + ]; + } + + /** + * Delete tmp test scripts + */ + public static function tearDownAfterClass() + { + foreach (static::$files as $path) { + unlink($path['path']); + } + } + + /** + * Create single tmp script file for testing + * + * @param string $type ('returnTrue', 'returnNotTrue') + * @return string $path + */ + public static function createTmpScript($type) + { + $dir = rtrim(sys_get_temp_dir(), '/'); + + do { + $name = static::getRandomString() . '-test-script.php'; + $path = $dir . '/' . $name; + + if (!file_exists($path)) break; + } while (true); + + if ($type === 'noInvoke') { + $class = 'RunnerTestConrtollerInvalid'; + $content = +<<<CONTENT +<?php + +class $class { + public \$route = null; + + public function __construct(\$route) + { + \$this->route = \$route; + } +} +CONTENT; + } else { + $class = 'RunnerTestConrtoller'; + $content = +<<<CONTENT +<?php + +class $class { + public \$route = null; + + public function __construct(\$route) + { + \$this->route = \$route; + } + + public function __invoke(Psr\Http\Message\ServerRequestInterface \$request, Psr\Http\Message\ResponseInterface \$response) + { + return ['request' => \$request, 'response' => \$response]; + } +} +CONTENT; + } + + $bytes = file_put_contents($path, $content); + static::assertTrue((int)$bytes > 0); + + require_once $path; + + return [$class, $path]; + } + + /** + * Get random string of given length (no more then length of md5 hash) + * + * @param int $length + * @return string + */ + public static function getRandomString($length = 10) + { + return substr(md5(microtime(true) * mt_rand()), 0, $length); + } +} diff --git a/tests/Router/Runner/PhpScriptTest.php b/tests/Router/Runner/PhpScriptTest.php index a7cb4e1..12b9219 100644 --- a/tests/Router/Runner/PhpScriptTest.php +++ b/tests/Router/Runner/PhpScriptTest.php @@ -2,7 +2,7 @@ use Jasny\Router\Route; use Jasny\Router\Runner\PhpScript; -use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; class PhpScriptTest extends PHPUnit_Framework_TestCase @@ -19,7 +19,7 @@ class PhpScriptTest extends PHPUnit_Framework_TestCase $runner = new PhpScript($route); $this->assertEquals($route, $runner->getRoute(), "Route was not set correctly"); - $request = $this->createMock(RequestInterface::class); + $request = $this->createMock(ServerRequestInterface::class); $response = $this->createMock(ResponseInterface::class); if (!$positive) $this->expectException(\RuntimeException::class); diff --git a/tests/Router/RunnerTest.php b/tests/Router/RunnerTest.php index b6e2baf..60b80b9 100644 --- a/tests/Router/RunnerTest.php +++ b/tests/Router/RunnerTest.php @@ -5,7 +5,7 @@ use Jasny\Router\Runner; use Jasny\Router\Runner\Controller; use Jasny\Router\Runner\Callback; use Jasny\Router\Runner\PhpScript; -use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; class RunnerTest extends PHPUnit_Framework_TestCase @@ -50,7 +50,7 @@ class RunnerTest extends PHPUnit_Framework_TestCase { $runner = $this->getMockBuilder('Jasny\Router\Runner')->disableOriginalConstructor()->getMockForAbstractClass(); $queries = [ - 'request' => $this->createMock(RequestInterface::class), + 'request' => $this->createMock(ServerRequestInterface::class), 'response' => $this->createMock(ResponseInterface::class) ]; diff --git a/tests/RouterTest.php b/tests/RouterTest.php new file mode 100644 index 0000000..9b63360 --- /dev/null +++ b/tests/RouterTest.php @@ -0,0 +1,227 @@ +<?php + +use Jasny\Router; +use Jasny\Router\Route; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\StreamInterface; + +class RouterTest extends PHPUnit_Framework_TestCase +{ + /** + * Test creating Router + */ + public function testConstruct() + { + $routes = [ + '/foo' => ['fn' => 'test_function'], + '/foo/bar' => ['controller' => 'TestController'] + ]; + + $router = new Router($routes); + $this->assertEquals($routes, $router->getRoutes(), "Routes were not set correctly"); + } + + /** + * Test that on router 'run', method '__invoke' is called + */ + public function testRun() + { + $router = $this->createMock(Router::class, ['__invoke']); + list($request, $response) = $this->getRequests(); + + $router->method('__invoke')->will($this->returnCallback(function($arg1, $arg2) { + return ['request' => $arg1, 'response' => $arg2]; + })); + + $result = $router->run($request, $response); + + $this->assertEquals($request, $result['request'], "Request was not processed correctly"); + $this->assertEquals($response, $result['response'], "Response was not processed correctly"); + } + + /** + * Test '__invoke' method + */ + public function testInvoke() + { + $routes = [ + '/foo/bar' => Route::create(['controller' => 'TestController']), + '/foo' => Route::create(['fn' => function($arg1, $arg2) { + return ['request' => $arg1, 'response' => $arg2]; + }]) + ]; + + list($request, $response) = $this->getRequests(); + $router = new Router($routes); + $result = $router($request, $response); + + $this->assertEquals($request, $result['request'], "Request was not processed correctly"); + $this->assertEquals($response, $result['response'], "Response was not processed correctly"); + } + + /** + * Test '__invoke' method with 'next' callback + */ + public function testInvokeNext() + { + $routes = [ + '/foo/bar' => Route::create(['controller' => 'TestController']), + '/foo' => Route::create(['fn' => function($request, $response) { + return $response; + }]) + ]; + + list($request, $response) = $this->getRequests(); + $router = new Router($routes); + $result = $router($request, $response, function($arg1, $arg2) { + return ['request' => $arg1, 'response' => $arg2]; + }); + + $this->assertEquals($request, $result['request'], "Request was not processed correctly"); + $this->assertEquals($response, $result['response'], "Response was not processed correctly"); + } + + /** + * Test case when route is not found + */ + public function testNotFound() + { + $routes = [ + '/foo/bar' => Route::create(['controller' => 'TestController']) + ]; + + list($request, $response) = $this->getRequests(); + $this->expectNotFound($response); + + $router = new Router($routes); + $result = $router($request, $response); + + $this->assertEquals(get_class($response), get_class($result), "Returned result is not an instance of 'ServerRequestInterface'"); + } + + /** + * Test adding middleware action + * + * @dataProvider addProvider + * @param mixed $middleware1 + * @param callable $middleware2 + * @param boolean $positive + */ + public function testAdd($middleware1, $middleware2, $positive) + { + $router = new Router([]); + $this->assertEquals(0, count($router->getMiddlewares()), "Middlewares array should be empty"); + + if (!$positive) $this->expectException(\InvalidArgumentException::class); + + $result = $router->add($middleware1); + $this->assertEquals(1, count($router->getMiddlewares()), "There should be only one item in middlewares array"); + $this->assertEquals($middleware1, reset($router->getMiddlewares()), "Wrong item in middlewares array"); + $this->assertEquals($router, $result, "'Add' should return '\$this'"); + + if (!$middleware2) return; + + $router->add($middleware2); + $this->assertEquals(2, count($router->getMiddlewares()), "There should be two items in middlewares array"); + foreach ($router->getMiddlewares() as $action) { + $this->assertTrue($action == $middleware1 || $action == $middleware2, "Wrong item in middlewares array"); + } + } + + /** + * Provide data for testing 'add' method + */ + public function addProvider() + { + return [ + ['wrong_callback', null, false], + [[$this, 'getMiddlewareCalledFirst'], null, true], + [[$this, 'getMiddlewareCalledFirst'], [$this, 'getMiddlewareCalledLast'], true] + ]; + } + + /** + * Test executing router with middlewares chain (test only execution order) + */ + public function testRunMiddlewares() + { + $routes = [ + '/foo' => Route::create(['fn' => function($request, $response) { + $response->testMiddlewareCalls[] = 'handle'; + return $response; + }]) + ]; + + list($request, $response) = $this->getRequests(); + $router = new Router($routes); + $router->add([$this, 'getMiddlewareCalledLast'])->add([$this, 'getMiddlewareCalledFirst']); + + $result = $router($request, $response, function($request, $response) { + $response->testMiddlewareCalls[] = 'outer'; + return $response; + }); + + $this->assertEquals(['first','last','handle','outer'], $response->testMiddlewareCalls, "Actions were executed in wrong order"); + } + + /** + * Get requests for testing + * + * @return array + */ + public function getRequests() + { + $request = $this->createMock(ServerRequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + + $request->method('getUri')->will($this->returnValue('/foo')); + $request->method('getMethod')->will($this->returnValue('GET')); + + return [$request, $response]; + } + + /** + * Get middleware action, that should ba called first in middleware chain + * + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * @param callback $next + * @return ResponseInterface + */ + public function getMiddlewareCalledFirst(ServerRequestInterface $request, ResponseInterface $response, $next) + { + $response->testMiddlewareCalls[] = 'first'; + return $next($request, $response); + } + + /** + * Get middleware action, that should be called last in middleware chain + * + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * @param callback $next + * @return ResponseInterface + */ + public function getMiddlewareCalledLast(ServerRequestInterface $request, ResponseInterface $response, $next) + { + $response->testMiddlewareCalls[] = 'last'; + return $next($request, $response); + } + + /** + * Expect 'not found' response + * + * @param ResponseInterface + */ + public function expectNotFound(ResponseInterface $response) + { + $stream = $this->createMock(StreamInterface::class); + $stream->expects($this->once())->method('rewind'); + $stream->expects($this->once())->method('write')->with($this->equalTo('Not Found')); + + $response->method('getBody')->will($this->returnValue($stream)); + $response->expects($this->once())->method('withBody')->with($this->equalTo($stream))->will($this->returnSelf()); + $response->expects($this->once())->method('withStatus')->with($this->equalTo(404), $this->equalTo('Not Found'))->will($this->returnSelf()); + } +} |