diff --git a/composer.json b/composer.json index 73771ba..776ec0c 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,8 @@ "type": "library", "require": { "php": "^7.2", - "psr\/http-message": "^1.0" + "psr\/http-message": "^1.0", + "psr/http-server-middleware": "^1.0" }, "license": "MIT", "authors": [ @@ -39,4 +40,4 @@ "Limber\\Tests\\": "tests\/" } } -} \ No newline at end of file +} diff --git a/composer.lock b/composer.lock index fc24f26..f5bd149 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "ec92c700f0713223f7dba5e298963133", + "content-hash": "ec5e4ebf186051859c502dd2a0275f35", "packages": [ { "name": "psr/http-message", @@ -55,6 +55,112 @@ "response" ], "time": "2016-08-06T14:39:51+00:00" + }, + { + "name": "psr/http-server-handler", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-handler.git", + "reference": "aff2f80e33b7f026ec96bb42f63242dc50ffcae7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/aff2f80e33b7f026ec96bb42f63242dc50ffcae7", + "reference": "aff2f80e33b7f026ec96bb42f63242dc50ffcae7", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side request handler", + "keywords": [ + "handler", + "http", + "http-interop", + "psr", + "psr-15", + "psr-7", + "request", + "response", + "server" + ], + "time": "2018-10-30T16:46:14+00:00" + }, + { + "name": "psr/http-server-middleware", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-middleware.git", + "reference": "2296f45510945530b9dceb8bcedb5cb84d40c5f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/2296f45510945530b9dceb8bcedb5cb84d40c5f5", + "reference": "2296f45510945530b9dceb8bcedb5cb84d40c5f5", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0", + "psr/http-server-handler": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side middleware", + "keywords": [ + "http", + "http-interop", + "middleware", + "psr", + "psr-15", + "psr-7", + "request", + "response" + ], + "time": "2018-10-30T17:12:04+00:00" } ], "packages-dev": [ diff --git a/src/Application.php b/src/Application.php index 187de14..01f3f23 100644 --- a/src/Application.php +++ b/src/Application.php @@ -2,15 +2,18 @@ namespace Limber; +use Limber\Exceptions\ApplicationException; use Limber\Exceptions\DispatchException; use Limber\Exceptions\MethodNotAllowedHttpException; use Limber\Exceptions\NotFoundHttpException; -use Limber\Middleware\MiddlewareLayerInterface; -use Limber\Middleware\MiddlewareManager; +use Limber\Middleware\CallableMiddleware; +use Limber\Middleware\RequestHandler; use Limber\Router\Route; use Limber\Router\Router; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; use Throwable; class Application @@ -18,7 +21,7 @@ class Application /** * Router instance. * - * @var RouterAbstract + * @var Router */ protected $router; @@ -40,33 +43,42 @@ class Application * * Limber Framework Application constructor. * - * @param RouterAbstract $router + * @param Router $router */ public function __construct(Router $router) { - $this->router = $router; + $this->router = $router; } /** * Set the global middleware to run. * - * @param array $middleware + * @param array $middlewares * @return void */ - public function setMiddleware(array $middleware): void + public function setMiddleware(array $middlewares): void { - $this->middleware = $middleware; + foreach( $middlewares as $middleware ){ + $this->addMiddleware($middleware); + } } - /** - * Add a middleware layer. - * - * @param string|MiddlewareLayerInterface|callable $middleware - * @return void - */ - public function addMiddleware($middleware): void - { - $this->middleware[] = $middleware; + /** + * Add a middleware to the stack. + * + * @param MiddlewareInterface|callable $middleware + */ + public function addMiddleware($middleware): void + { + if( \is_callable($middleware) ){ + $middleware = new CallableMiddleware($middleware); + } + + if( $middleware instanceof MiddlewareInterface === false ){ + throw new ApplicationException("Provided middleware must be either instance of \callable or Psr\Http\Server\MiddlewareInterface."); + } + + $this->middleware[] = $middleware; } /** @@ -89,107 +101,83 @@ public function setExceptionHandler(callable $exceptionHandler): void */ public function dispatch(ServerRequestInterface $request): ResponseInterface { - try { - - $route = $this->resolveRoute($request); - - } catch( Throwable $exception ){ - - return $this->handleException($exception); + // Resolve the route now to check for Routed middleware. + $route = $this->router->resolve($request); - } - - // Build MiddlewareManager - $middlewareManager = new MiddlewareManager( - \array_merge($this->middleware, $route->getMiddleware()) + // Create final stack of middleware to compile + $middleware = \array_merge( + $this->middleware, + $route ? $route->getMiddleware() : [] ); - return $this->runMiddleware( - $middlewareManager, - $request, - $route - ); - } - - /** - * Run the request through the middleware. - * - * @param MiddlewareManager $middlewareManager - * @param ServerRequestInterface $request - * @param Route $route - * @return ResponseInterface - */ - private function runMiddleware(MiddlewareManager $middlewareManager, ServerRequestInterface $request, Route $route): ResponseInterface - { - return $middlewareManager->run($request, function(ServerRequestInterface $request) use ($route): ResponseInterface { - - try { + $requestHandler = $this->compileMiddleware( + $middleware, + new RequestHandler(function(ServerRequestInterface $request) use ($route): ResponseInterface { - $kernel = $this->resolveAction($route); + try { - $response = \call_user_func_array($kernel, \array_merge( - [$request], - \array_values($route->getPathParams($request->getUri()->getPath())) - )); + if( empty($route) ){ - } catch( Throwable $exception ){ + // 405 Method Not Allowed + if( ($methods = $this->router->getMethods($request)) ){ + throw new MethodNotAllowedHttpException($methods); + } - $response = $this->handleException($exception); - } + // 404 Not Found + throw new NotFoundHttpException("Route not found"); + } - return $response; + return \call_user_func_array( + $route->getCallableAction(), + \array_merge( + [$request], + \array_values( + $route->getPathParams($request->getUri()->getPath()) + ) + ) + ); - }); - } - - /** - * Resolve to a Route instance or throw exception. - * - * @param ServerRequestInterface $request - * @throws NotFoundHttpException - * @throws MethodNotAllowedHttpException - * @return Route - */ - private function resolveRoute(ServerRequestInterface $request): Route - { - if( ($route = $this->router->resolve($request)) === null ){ + } catch( Throwable $exception ){ - // 405 Method Not Allowed - if( ($methods = $this->router->getMethods($request)) ){ - throw new MethodNotAllowedHttpException($methods); - } + $this->handleException($exception); + } - // 404 Not Found - throw new NotFoundHttpException("Route not found"); - } + }) + ); - return $route; + return $requestHandler->handle($request); } /** - * Resolve the route action into a callable. + * Compile a middleware stack. * - * @param Route $route - * @return callable + * @param array + * @param RequestHandlerInterface $kernel + * @return RequestHandlerInterface */ - private function resolveAction(Route $route): callable + private function compileMiddleware(array $middleware, RequestHandlerInterface $kernel): RequestHandler { - // Callable/closure style route - if( \is_callable($route->getAction()) ){ - return $route->getAction(); - } + $middleware = \array_reverse($middleware); - // Class@Method style route - elseif( \is_string($route->getAction()) ) { - return \class_method($route->getAction()); - } + return \array_reduce($middleware, function(RequestHandlerInterface $handler, MiddlewareInterface $middleware) { + + return new RequestHandler(function(ServerRequestInterface $request) use ($handler, $middleware): ResponseInterface { + + try { + return $middleware->process($request, $handler); + } + catch( Throwable $exception ){ + $this->handleException($exception); + } - throw new DispatchException("Cannot dispatch request because route action cannot be resolved into callable."); + }); + + }, $kernel); } /** - * Handle an exception by trying to resolve to a Response. If no exception handler - * was provided, throw the exception again. + * Handle a thrown exception by either passing it to user provided exception handler + * or throwing it. * * @param Throwable $exception * @throws Throwable @@ -197,11 +185,11 @@ private function resolveAction(Route $route): callable */ private function handleException(Throwable $exception): ResponseInterface { - if( empty($this->exceptionHandler) ){ - throw $exception; - } + if( $this->exceptionHandler ){ + return \call_user_func($this->exceptionHandler, $exception); + }; - return \call_user_func($this->exceptionHandler, $exception); + throw $exception; } /** diff --git a/src/Exceptions/ApplicationException.php b/src/Exceptions/ApplicationException.php new file mode 100644 index 0000000..20d7913 --- /dev/null +++ b/src/Exceptions/ApplicationException.php @@ -0,0 +1,14 @@ +callback, [$request, $next]); + return \call_user_func_array($this->callback, [$request, $handler]); } } \ No newline at end of file diff --git a/src/Middleware/MiddlewareLayerInterface.php b/src/Middleware/MiddlewareLayerInterface.php deleted file mode 100644 index 842cab3..0000000 --- a/src/Middleware/MiddlewareLayerInterface.php +++ /dev/null @@ -1,18 +0,0 @@ - - */ - protected $middlewareStack = []; - - /** - * MiddlewareManager constructor. - * - * @param array $layers - */ - public function __construct(array $layers = []) - { - foreach( $layers as $layer ){ - - if( $layer instanceof MiddlewareLayerInterface ){ - $this->add($layer); - } - elseif( \is_callable($layer) ){ - $this->add( - new CallableMiddlewareLayer($layer) - ); - } - else { - - /** - * @psalm-suppress InvalidStringClass - * @psalm-suppress ArgumentTypeCoercion - */ - $this->add(new $layer); - } - } - } - - /** - * Add a layer to the stack. - * - * @param MiddlewareLayerInterface $layer - */ - public function add(MiddlewareLayerInterface $layer): void - { - $this->middlewareStack[] = $layer; - } - - /** - * Get the middleware stack. - * - * @return array - */ - public function getMiddleware(): array - { - return $this->middlewareStack; - } - - /** - * Run the middleware stack. - * - * @param ServerRequestInterface $request - * @param callable $kernel - * @return ResponseInterface - */ - public function run(ServerRequestInterface $request, callable $kernel): ResponseInterface - { - $next = \array_reduce(\array_reverse($this->getMiddleware()), function(callable $next, MiddlewareLayerInterface $layer): \closure { - - return function(ServerRequestInterface $request) use ($next, $layer): ResponseInterface { - return $layer->handle($request, $next); - }; - - }, function(ServerRequestInterface $request) use ($kernel): ResponseInterface { - return $kernel($request); - }); - - return $next($request); - } -} \ No newline at end of file diff --git a/src/Middleware/RequestHandler.php b/src/Middleware/RequestHandler.php new file mode 100644 index 0000000..77756cf --- /dev/null +++ b/src/Middleware/RequestHandler.php @@ -0,0 +1,38 @@ +handler = $handler; + } + + /** + * Handle server request. + * + * @param ServerRequestInterface $request + * @return ResponseInterface + */ + public function handle(ServerRequestInterface $request): ResponseInterface + { + return \call_user_func($this->handler, $request); + } +} \ No newline at end of file diff --git a/src/Router/Route.php b/src/Router/Route.php index 407488c..29bc347 100644 --- a/src/Router/Route.php +++ b/src/Router/Route.php @@ -2,6 +2,9 @@ namespace Limber\Router; +use Limber\Exceptions\ApplicationException; +use Throwable; + class Route { /** @@ -103,14 +106,14 @@ public function __construct($methods, string $path, $action, array $config = []) if( \preg_match('/{([a-z0-9_]+)(?:\:([a-z0-9_]+))?}/i', $part, $match) ){ if( \in_array($match[1], $this->namedPathParameters) ){ - throw new \Exception("Path parameter \"{$match[1]}\" already defined for route {$match[0]}"); + throw new ApplicationException("Path parameter \"{$match[1]}\" already defined for route {$match[0]}"); } // Predefined pattern if( isset($match[2]) ){ - if( ($part = RouterAbstract::getPattern($match[2])) === null ){ - throw new \Exception("Router pattern not found: {$match[2]}"); + if( ($part = Router::getPattern($match[2])) === null ){ + throw new ApplicationException("Router pattern not found: {$match[2]}"); } } @@ -268,7 +271,22 @@ public function getAction() } return $this->action; - } + } + + /** + * Get the callable action for this route. + * + * @throws Throwable + * @return callable + */ + public function getCallableAction(): callable + { + if( \is_callable($this->action) ){ + return $this->action; + } + + return \class_method($this->action); + } /** * Get the path prefix. diff --git a/src/functions.php b/src/functions.php index 6033ce4..ccae4b8 100644 --- a/src/functions.php +++ b/src/functions.php @@ -1,5 +1,7 @@ handle($request); + + }; + $application->setMiddleware([ - "\App\Middleware\MyMiddleware" + $middleware ]); $reflection = new \ReflectionClass($application); @@ -62,7 +70,7 @@ public function test_set_middleware() $this->assertEquals( [ - "\App\Middleware\MyMiddleware" + new CallableMiddleware($middleware), ], $property->getValue($application) ); @@ -74,11 +82,13 @@ public function test_add_middleware() new Router ); - $application->setMiddleware([ - "\App\Middleware\MyMiddleware" - ]); + $middleware = function(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - $application->addMiddleware("\App\Middleware\MyOtherMiddleware"); + return $handler->handle($request); + + }; + + $application->addMiddleware($middleware); $reflection = new \ReflectionClass($application); $property = $reflection->getProperty('middleware'); @@ -86,8 +96,7 @@ public function test_add_middleware() $this->assertEquals( [ - "\App\Middleware\MyMiddleware", - "\App\Middleware\MyOtherMiddleware" + new CallableMiddleware($middleware) ], $property->getValue($application) ); @@ -110,133 +119,6 @@ public function test_set_exception_handler() $this->assertSame($handler, $property->getValue($application)); } - public function test_resolve_route_found() - { - $router = new Router; - - $route = $router->get("/books", function(ServerRequestInterface $request){ - return new Response( - ResponseStatus::OK, - "OK" - ); - }); - - $application = new Application($router); - - $reflection = new \ReflectionClass($application); - $method = $reflection->getMethod('resolveRoute'); - $method->setAccessible(true); - - $request = ServerRequest::create("get", "http://example.org/books", null, [], [], [], []); - - $resolvedRoute = $method->invokeArgs($application, [$request]); - - $this->assertSame($route, $resolvedRoute); - } - - public function test_resolve_route_not_found() - { - $router = new Router; - - $router->get("/books", function(ServerRequestInterface $request){ - return new Response( - ResponseStatus::OK, - "OK" - ); - }); - - $application = new Application($router); - - $reflection = new \ReflectionClass($application); - $method = $reflection->getMethod('resolveRoute'); - $method->setAccessible(true); - - $request = ServerRequest::create("get", "http://example.org/authors", null, [], [], [], []); - - $this->expectException(NotFoundHttpException::class); - $method->invokeArgs($application, [$request]); - } - - public function test_resolve_route_method_not_allowed() - { - $router = new Router; - - $router->get("/books", function(ServerRequestInterface $request){ - return new Response( - ResponseStatus::OK, - "OK" - ); - }); - - $application = new Application($router); - - $reflection = new \ReflectionClass($application); - $method = $reflection->getMethod('resolveRoute'); - $method->setAccessible(true); - - $request = ServerRequest::create("post", "http://example.org/books", null, [], [], [], []); - - $this->expectException(MethodNotAllowedHttpException::class); - $method->invokeArgs($application, [$request]); - } - - public function test_resolve_action_closure() - { - $router = new Router; - $handler = function(ServerRequestInterface $request){ - return new Response( - ResponseStatus::OK, - "OK" - ); - }; - $route = $router->get("/books", $handler); - - $application = new Application($router); - - $reflection = new \ReflectionClass($application); - $method = $reflection->getMethod('resolveAction'); - $method->setAccessible(true); - - $action = $method->invokeArgs($application, [$route]); - - $this->assertSame( - $handler, - $action - ); - } - - public function test_resolve_action_string() - { - $router = new Router; - $route = $router->get("/books", self::class . "@test_resolve_action_string"); - - $application = new Application($router); - - $reflection = new \ReflectionClass($application); - $method = $reflection->getMethod('resolveAction'); - $method->setAccessible(true); - - $action = $method->invokeArgs($application, [$route]); - - $this->assertTrue(\is_callable($action)); - } - - public function test_resolve_action_with_unresolvable() - { - $router = new Router; - $route = $router->get("/books", new \StdClass); - - $application = new Application($router); - - $reflection = new \ReflectionClass($application); - $method = $reflection->getMethod('resolveAction'); - $method->setAccessible(true); - - $this->expectException(DispatchException::class); - - $action = $method->invokeArgs($application, [$route]); - } - public function test_handle_exception_with_exception_handler_set() { $application = new Application(new Router); @@ -276,107 +158,6 @@ public function test_handle_exception_with_no_exception_handler_set() $response = $method->invokeArgs($application, [new NotFoundHttpException("Route not found")]); } - public function test_run_middleware() - { - $router = new Router; - $route = $router->get("/books", function(ServerRequestInterface $request){ - return new Response( - ResponseStatus::OK, - "OK" - ); - }); - - $application = new Application($router); - - $reflection = new \ReflectionClass($application); - $method = $reflection->getMethod('runMiddleware'); - $method->setAccessible(true); - - $request = ServerRequest::create("get", "http://example.org/authors", null, [], [], [], []); - - $response = $method->invokeArgs($application, [new MiddlewareManager, $request, $route]); - - $this->assertEquals(ResponseStatus::OK, $response->getStatusCode()); - $this->assertEquals("OK", $response->getBody()->getContents()); - } - - public function test_run_middleware_with_thrown_exception_and_exception_handler_set() - { - $router = new Router; - $route = $router->get("/books", function(ServerRequestInterface $request){ - throw new BadRequestHttpException("Bad request"); - }); - - $application = new Application($router); - $application->setExceptionHandler(function(\Throwable $exception){ - - return new Response( - $exception->getHttpStatus(), - $exception->getMessage() - ); - - }); - - $reflection = new \ReflectionClass($application); - $method = $reflection->getMethod('runMiddleware'); - $method->setAccessible(true); - - $request = ServerRequest::create("get", "http://example.org/authors", null, [], [], [], []); - - $response = $method->invokeArgs($application, [new MiddlewareManager, $request, $route]); - - $this->assertEquals(ResponseStatus::BAD_REQUEST, $response->getStatusCode()); - $this->assertEquals("Bad request", $response->getBody()->getContents()); - } - - public function test_run_middleware_with_thrown_exception_and_no_exception_handler_set() - { - $router = new Router; - $route = $router->get("/books", function(ServerRequestInterface $request){ - throw new BadRequestHttpException("Bad request"); - }); - - $application = new Application($router); - - $reflection = new \ReflectionClass($application); - $method = $reflection->getMethod('runMiddleware'); - $method->setAccessible(true); - - $request = ServerRequest::create("get", "http://example.org/authors", null, [], [], [], []); - - $this->expectException(BadRequestHttpException::class); - $response = $method->invokeArgs($application, [new MiddlewareManager, $request, $route]); - } - - public function test_run_middleware_passes_path_parameters_in_order() - { - $router = new Router; - $route = $router->get("/books/{isbn}/comments/{id}", function(ServerRequestInterface $request, string $isbn, string $id){ - return new Response( - ResponseStatus::OK, - \json_encode([ - "isbn" => $isbn, - "id" => $id - ]) - ); - }); - - $application = new Application($router); - - $reflection = new \ReflectionClass($application); - $method = $reflection->getMethod('runMiddleware'); - $method->setAccessible(true); - - $request = ServerRequest::create("get", "http://example.org/books/123-isbn-456/comments/987-comment-654", null, [], [], [], []); - - $response = $method->invokeArgs($application, [new MiddlewareManager, $request, $route]); - - $payload = \json_decode($response->getBody()->getContents()); - - $this->assertEquals("123-isbn-456", $payload->isbn); - $this->assertEquals("987-comment-654", $payload->id); - } - public function test_dispatch_with_unresolvable_route() { $application = new Application(new Router); diff --git a/tests/MiddlewareTest.php b/tests/MiddlewareTest.php deleted file mode 100644 index d6d2886..0000000 --- a/tests/MiddlewareTest.php +++ /dev/null @@ -1,89 +0,0 @@ -getMiddleware(); - - $this->assertTrue($middleware[0] instanceof MiddlewareLayerInterface); - } - - public function test_constructor_compiles_callables() - { - $middlewareManager = new MiddlewareManager([ - function(ServerRequestInterface $request, callable $next): ResponseInterface { - $request = $request->withAddedHeader("X-Request-Header", "Limber"); - return $next($request); - } - ]); - - $middleware = $middlewareManager->getMiddleware(); - - $this->assertTrue($middleware[0] instanceof CallableMiddlewareLayer); - } - - public function test_constructor_adds_layer_instances() - { - $middlewareManager = new MiddlewareManager([ - new SampleMiddleware - ]); - - $middleware = $middlewareManager->getMiddleware(); - - $this->assertTrue($middleware[0] instanceof MiddlewareLayerInterface); - } - - public function test_add() - { - $middlewareManager = new MiddlewareManager([ - new SampleMiddleware - ]); - - $middlewareManager->add(new SampleMiddleware); - - $middleware = $middlewareManager->getMiddleware(); - - $this->assertEquals(2, \count($middleware)); - $this->assertTrue($middleware[1] instanceof MiddlewareLayerInterface); - } - - public function test_run() - { - $middlewareManager = new MiddlewareManager([ - new SampleMiddleware - ]); - - $response = $middlewareManager->run( - ServerRequest::create("get", "/books", null, [], [], [], []), - function(ServerRequestInterface $request) { - return new Response( - ResponseStatus::OK - ); - } - ); - - $this->assertEquals(ResponseStatus::OK, $response->getStatusCode()); - $this->assertEquals("X-Sample-Middleware: Limber", $response->getHeaderLine("X-Sample-Middleware")); - } -} \ No newline at end of file diff --git a/tests/RouteTest.php b/tests/RouteTest.php index 692c3fc..31c40c5 100644 --- a/tests/RouteTest.php +++ b/tests/RouteTest.php @@ -2,11 +2,15 @@ namespace Limber\Tests; +use Capsule\Response; +use Capsule\ResponseStatus; use Limber\Router\Route; use PHPUnit\Framework\TestCase; +use Throwable; /** * @covers Limber\Router\Route + * @covers ::class_method */ class RouteTest extends TestCase { @@ -180,5 +184,38 @@ public function test_non_matching_path() { $route = new Route("get", "books/{bookId}/comments/{commentId}", "BooksController@get"); $this->assertFalse($route->matchPath("books/1234")); - } + } + + public function test_get_callable_action_string() + { + $route = new Route("get", "books/{bookId}/comments/{commentId}", static::class . "@test_get_callable_action_string"); + $this->assertTrue( + \is_callable($route->getCallableAction()) + ); + } + + public function test_get_callable_action_unresolvable() + { + $route = new Route("get", "/books", new \StdClass); + + $this->expectException(Throwable::class); + $route->getCallableAction(); + } + + public function test_get_callable_action_closure() + { + $handler = function(ServerRequestInterface $request){ + return new Response( + ResponseStatus::OK, + "OK" + ); + }; + + $route = new Route("get", "/books", $handler); + + $this->assertSame( + $handler, + $route->getCallableAction() + ); + } } \ No newline at end of file