diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f4f46145..c6bff33d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ## 4.0.0 - 2019-07-03 ### Added +- [#2641](https://github.com/slimphp/Slim/pull/2641) Add `RouteCollectorProxyInterface` which extracts all the route mapping functionality from app into its own interface. - [#2640](https://github.com/slimphp/Slim/pull/2640) Add `RouteParserInterface` and decouple FastRoute route parser entirely from core. The methods `relativePathFor()`, `urlFor()` and `fullUrlFor()` are now located on this interface. - [#2639](https://github.com/slimphp/Slim/pull/2639) Add `DispatcherInterface` and decouple FastRoute dispatcher entirely from core. This enables us to swap out our router implementation for any other router. - [#2638](https://github.com/slimphp/Slim/pull/2638) Add `RouteCollector::fullUrlFor()` to give the ability to generate fully qualified URLs @@ -24,6 +25,7 @@ ### Deprecated +- [#2641](https://github.com/slimphp/Slim/pull/2641) Deprecate `RouteCollector::pushGroup()`,`RouteCollector::popGroup()` which gets replaced by `RouteCollector::group()` - [#2638](https://github.com/slimphp/Slim/pull/2638) Deprecate `RouteCollector::pathFor()` which gets replaced by `RouteCollector::urlFor()` preserving the orignal functionality - [#2555](https://github.com/slimphp/Slim/pull/2555) Double-Pass Middleware Support has been deprecated diff --git a/Slim/App.php b/Slim/App.php index 8bb1a860c..bd4238f49 100644 --- a/Slim/App.php +++ b/Slim/App.php @@ -9,20 +9,16 @@ namespace Slim; -use Closure; use Psr\Container\ContainerInterface; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Message\UriInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Slim\Interfaces\CallableResolverInterface; use Slim\Interfaces\RouteCollectorInterface; -use Slim\Interfaces\RouteGroupInterface; -use Slim\Interfaces\RouteInterface; use Slim\Interfaces\RouteResolverInterface; -use Slim\Routing\RouteCollector; +use Slim\Routing\RouteCollectorProxy; use Slim\Routing\RouteResolver; use Slim\Routing\RouteRunner; @@ -33,7 +29,7 @@ * configure, and run a Slim Framework application. * The \Slim\App class also accepts Slim Framework middleware. */ -class App implements RequestHandlerInterface +class App extends RouteCollectorProxy implements RequestHandlerInterface { /** * Current version @@ -42,50 +38,24 @@ class App implements RequestHandlerInterface */ public const VERSION = '4.0.0-dev'; - /** - * Container - * - * @var ContainerInterface|null - */ - private $container; - - /** - * @var CallableResolverInterface - */ - protected $callableResolver; - /** * @var MiddlewareDispatcher */ protected $middlewareDispatcher; - /** - * @var RouteCollectorInterface - */ - protected $routeCollector; - /** * @var RouteResolverInterface */ protected $routeResolver; - /** - * @var ResponseFactoryInterface - */ - protected $responseFactory; - - /******************************************************************************** - * Constructor - *******************************************************************************/ - /** * Create new application * - * @param ResponseFactoryInterface $responseFactory - * @param ContainerInterface|null $container - * @param CallableResolverInterface $callableResolver - * @param RouteCollectorInterface $routeCollector - * @param RouteResolverInterface $routeResolver + * @param ResponseFactoryInterface $responseFactory + * @param ContainerInterface|null $container + * @param CallableResolverInterface|null $callableResolver + * @param RouteCollectorInterface|null $routeCollector + * @param RouteResolverInterface|null $routeResolver */ public function __construct( ResponseFactoryInterface $responseFactory, @@ -94,20 +64,27 @@ public function __construct( RouteCollectorInterface $routeCollector = null, RouteResolverInterface $routeResolver = null ) { - $this->responseFactory = $responseFactory; - $this->container = $container; - $this->callableResolver = $callableResolver ?? new CallableResolver($container); - $this->routeCollector = $routeCollector ?? new RouteCollector( + parent::__construct( $responseFactory, - $this->callableResolver, - $container + $callableResolver ?? new CallableResolver($container), + $container, + $routeCollector ); - $this->routeResolver = $routeResolver ?? new RouteResolver($this->routeCollector); + $this->routeResolver = $routeResolver ?? new RouteResolver($this->routeCollector); $routeRunner = new RouteRunner($this->routeResolver); + $this->middlewareDispatcher = new MiddlewareDispatcher($routeRunner, $container); } + /** + * @return RouteResolverInterface + */ + public function getRouteResolver(): RouteResolverInterface + { + return $this->routeResolver; + } + /** * @param MiddlewareInterface|string|callable $middleware * @return self @@ -128,207 +105,6 @@ public function addMiddleware(MiddlewareInterface $middleware): self return $this; } - /******************************************************************************** - * Getter methods - *******************************************************************************/ - - /** - * Get container - * - * @return ContainerInterface|null - */ - public function getContainer(): ?ContainerInterface - { - return $this->container; - } - - /** - * Get callable resolver - * - * @return CallableResolverInterface - */ - public function getCallableResolver(): CallableResolverInterface - { - return $this->callableResolver; - } - - /** - * Get route collector - * - * @return RouteCollectorInterface - */ - public function getRouteCollector(): RouteCollectorInterface - { - return $this->routeCollector; - } - - /** - * Get route resolver - * - * @return RouteResolverInterface - */ - public function getRouteResolver(): RouteResolverInterface - { - return $this->routeResolver; - } - - /******************************************************************************** - * Router proxy methods - *******************************************************************************/ - - /** - * Add GET route - * - * @param string $pattern The route URI pattern - * @param callable|string $callable The route callback routine - * - * @return RouteInterface - */ - public function get(string $pattern, $callable): RouteInterface - { - return $this->map(['GET'], $pattern, $callable); - } - - /** - * Add POST route - * - * @param string $pattern The route URI pattern - * @param callable|string $callable The route callback routine - * - * @return RouteInterface - */ - public function post(string $pattern, $callable): RouteInterface - { - return $this->map(['POST'], $pattern, $callable); - } - - /** - * Add PUT route - * - * @param string $pattern The route URI pattern - * @param callable|string $callable The route callback routine - * - * @return RouteInterface - */ - public function put(string $pattern, $callable): RouteInterface - { - return $this->map(['PUT'], $pattern, $callable); - } - - /** - * Add PATCH route - * - * @param string $pattern The route URI pattern - * @param callable|string $callable The route callback routine - * - * @return RouteInterface - */ - public function patch(string $pattern, $callable): RouteInterface - { - return $this->map(['PATCH'], $pattern, $callable); - } - - /** - * Add DELETE route - * - * @param string $pattern The route URI pattern - * @param callable|string $callable The route callback routine - * - * @return RouteInterface - */ - public function delete(string $pattern, $callable): RouteInterface - { - return $this->map(['DELETE'], $pattern, $callable); - } - - /** - * Add OPTIONS route - * - * @param string $pattern The route URI pattern - * @param callable|string $callable The route callback routine - * - * @return RouteInterface - */ - public function options(string $pattern, $callable): RouteInterface - { - return $this->map(['OPTIONS'], $pattern, $callable); - } - - /** - * Add route for any HTTP method - * - * @param string $pattern The route URI pattern - * @param callable|string $callable The route callback routine - * - * @return RouteInterface - */ - public function any(string $pattern, $callable): RouteInterface - { - return $this->map(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], $pattern, $callable); - } - - /** - * Add route with multiple methods - * - * @param string[] $methods Numeric array of HTTP method names - * @param string $pattern The route URI pattern - * @param callable|string $callable The route callback routine - * - * @return RouteInterface - */ - public function map(array $methods, string $pattern, $callable): RouteInterface - { - // Bind route callable to container, if present - if ($this->container instanceof ContainerInterface && $callable instanceof Closure) { - $callable = $callable->bindTo($this->container); - } - - return $this->routeCollector->map($methods, $pattern, $callable); - } - - /** - * Add a route that sends an HTTP redirect - * - * @param string $from - * @param string|UriInterface $to - * @param int $status - * - * @return RouteInterface - */ - public function redirect(string $from, $to, int $status = 302): RouteInterface - { - $handler = function () use ($to, $status) { - $response = $this->responseFactory->createResponse($status); - return $response->withHeader('Location', (string)$to); - }; - - return $this->get($from, $handler); - } - - /** - * Route Groups - * - * This method accepts a route pattern and a callback. All route - * declarations in the callback will be prepended by the group(s) - * that it is in. - * - * @param string $pattern - * @param callable $callable - * - * @return RouteGroupInterface - */ - public function group(string $pattern, $callable): RouteGroupInterface - { - $group = $this->routeCollector->pushGroup($pattern, $callable); - $group($this); - $this->routeCollector->popGroup(); - return $group; - } - - /******************************************************************************** - * Runner - *******************************************************************************/ - /** * Run application * diff --git a/Slim/Interfaces/RouteCollectorInterface.php b/Slim/Interfaces/RouteCollectorInterface.php index 47b083c86..c0835056c 100644 --- a/Slim/Interfaces/RouteCollectorInterface.php +++ b/Slim/Interfaces/RouteCollectorInterface.php @@ -98,6 +98,8 @@ public function getNamedRoute(string $name): RouteInterface; public function removeNamedRoute(string $name): RouteCollectorInterface; /** + * Lookup a route via the route's unique identifier + * * @param string $identifier * * @return RouteInterface @@ -107,21 +109,13 @@ public function removeNamedRoute(string $name): RouteCollectorInterface; public function lookupRoute(string $identifier): RouteInterface; /** - * Add a route group to the array - * - * @param string $pattern The group pattern - * @param callable $callable A group callable + * Add route group * + * @param string $pattern + * @param string|callable $callable * @return RouteGroupInterface */ - public function pushGroup(string $pattern, $callable): RouteGroupInterface; - - /** - * Removes the last route group from the array - * - * @return RouteGroupInterface|null - */ - public function popGroup(): ?RouteGroupInterface; + public function group(string $pattern, $callable): RouteGroupInterface; /** * Add route diff --git a/Slim/Interfaces/RouteCollectorProxyInterface.php b/Slim/Interfaces/RouteCollectorProxyInterface.php new file mode 100644 index 000000000..4235ab772 --- /dev/null +++ b/Slim/Interfaces/RouteCollectorProxyInterface.php @@ -0,0 +1,153 @@ +callableResolver); - $this->routeGroups[] = $group; - return $group; + $pattern = ''; + foreach ($this->routeGroups as $group) { + $pattern .= $group->getPattern(); + } + return $pattern; } /** - * Removes the last route group from the array - * - * @return RouteGroupInterface|null The last RouteGroup, if one exists + * {@inheritdoc} */ - public function popGroup(): ?RouteGroupInterface + public function group(string $pattern, $callable): RouteGroupInterface { - return array_pop($this->routeGroups); - } + $routeCollectorProxy = new RouteCollectorProxy( + $this->responseFactory, + $this->callableResolver, + $this->container, + $this + ); - /** - * Process route groups - * - * @return string A group pattern to prefix routes with - */ - protected function processGroups(): string - { - $pattern = ''; - foreach ($this->routeGroups as $group) { - $pattern .= $group->getPattern(); - } - return $pattern; + $routeGroup = new RouteGroup($pattern, $callable, $this->callableResolver, $routeCollectorProxy); + $this->routeGroups[] = $routeGroup; + + $routeGroup->collectRoutes(); + array_shift($this->routeGroups); + + return $routeGroup; } /** @@ -280,7 +275,7 @@ public function map(array $methods, string $pattern, $handler): RouteInterface { // Prepend parent group pattern(s) if ($this->routeGroups) { - $pattern = $this->processGroups() . $pattern; + $pattern = $this->computeRoutePatternPrefix() . $pattern; } $route = $this->createRoute($methods, $pattern, $handler); diff --git a/Slim/Routing/RouteCollectorProxy.php b/Slim/Routing/RouteCollectorProxy.php new file mode 100644 index 000000000..6d05e5e8e --- /dev/null +++ b/Slim/Routing/RouteCollectorProxy.php @@ -0,0 +1,206 @@ +responseFactory = $responseFactory; + $this->callableResolver = $callableResolver; + $this->container = $container; + $this->routeCollector = $routeCollector ?? new RouteCollector($responseFactory, $callableResolver, $container); + $this->basePath = $basePath; + } + + /** + * {@inheritdoc} + */ + public function getCallableResolver(): CallableResolverInterface + { + return $this->callableResolver; + } + + /** + * {@inheritdoc} + */ + public function getContainer(): ?ContainerInterface + { + return $this->container; + } + + /** + * {@inheritdoc} + */ + public function getRouteCollector(): RouteCollectorInterface + { + return $this->routeCollector; + } + + /** + * {@inheritdoc} + */ + public function getBasePath(): string + { + return $this->basePath; + } + + /** + * {@inheritdoc} + */ + public function setBasePath(string $basePath): RouteCollectorProxyInterface + { + $this->basePath = $basePath; + return $this; + } + + /** + * {@inheritdoc} + */ + public function get(string $pattern, $callable): RouteInterface + { + return $this->routeCollector->map(['GET'], $pattern, $callable); + } + + /** + * {@inheritdoc} + */ + public function post(string $pattern, $callable): RouteInterface + { + return $this->routeCollector->map(['POST'], $pattern, $callable); + } + + /** + * {@inheritdoc} + */ + public function put(string $pattern, $callable): RouteInterface + { + return $this->routeCollector->map(['PUT'], $pattern, $callable); + } + + /** + * {@inheritdoc} + */ + public function patch(string $pattern, $callable): RouteInterface + { + return $this->routeCollector->map(['PATCH'], $pattern, $callable); + } + + /** + * {@inheritdoc} + */ + public function delete(string $pattern, $callable): RouteInterface + { + return $this->routeCollector->map(['DELETE'], $pattern, $callable); + } + + /** + * {@inheritdoc} + */ + public function options(string $pattern, $callable): RouteInterface + { + return $this->routeCollector->map(['OPTIONS'], $pattern, $callable); + } + + /** + * {@inheritdoc} + */ + public function any(string $pattern, $callable): RouteInterface + { + return $this->routeCollector->map(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], $pattern, $callable); + } + + /** + * {@inheritdoc} + */ + public function map(array $methods, string $pattern, $callable): RouteInterface + { + if ($this->container && $callable instanceof Closure) { + $callable = $callable->bindTo($this->container); + } + + return $this->routeCollector->map($methods, $pattern, $callable); + } + + /** + * {@inheritdoc} + */ + public function redirect(string $from, $to, int $status = 302): RouteInterface + { + $handler = function () use ($to, $status) { + $response = $this->responseFactory->createResponse($status); + return $response->withHeader('Location', (string) $to); + }; + + return $this->get($from, $handler); + } + + /** + * {@inheritdoc} + */ + public function group(string $pattern, $callable): RouteGroupInterface + { + $pattern = $this->basePath . $pattern; + return $this->routeCollector->group($pattern, $callable); + } +} diff --git a/Slim/Routing/RouteGroup.php b/Slim/Routing/RouteGroup.php index 228b39372..af62a3cce 100644 --- a/Slim/Routing/RouteGroup.php +++ b/Slim/Routing/RouteGroup.php @@ -10,8 +10,8 @@ namespace Slim\Routing; use Psr\Http\Server\MiddlewareInterface; -use Slim\App; use Slim\Interfaces\CallableResolverInterface; +use Slim\Interfaces\RouteCollectorProxyInterface; use Slim\Interfaces\RouteGroupInterface; use Slim\MiddlewareDispatcher; @@ -27,6 +27,11 @@ class RouteGroup implements RouteGroupInterface */ protected $callableResolver; + /** + * @var RouteCollectorProxyInterface + */ + protected $routeCollectorProxy; + /** * @var MiddlewareInterface[]|string[]|callable[] */ @@ -40,18 +45,31 @@ class RouteGroup implements RouteGroupInterface /** * Create a new RouteGroup * - * @param string $pattern The pattern prefix for the group - * @param callable $callable The group callable - * @param CallableResolverInterface $callableResolver + * @param string $pattern + * @param callable|string $callable + * @param CallableResolverInterface $callableResolver + * @param RouteCollectorProxyInterface $routeCollectorProxy */ public function __construct( string $pattern, $callable, - CallableResolverInterface $callableResolver + CallableResolverInterface $callableResolver, + RouteCollectorProxyInterface $routeCollectorProxy ) { $this->pattern = $pattern; $this->callable = $callable; $this->callableResolver = $callableResolver; + $this->routeCollectorProxy = $routeCollectorProxy; + } + + /** + * {@inheritdoc} + */ + public function collectRoutes(): RouteGroupInterface + { + $callable = $this->callableResolver->resolve($this->callable); + $callable($this->routeCollectorProxy); + return $this; } /** @@ -91,15 +109,4 @@ public function getPattern(): string { return $this->pattern; } - - /** - * {@inheritdoc} - */ - public function __invoke(App $app = null): RouteGroupInterface - { - /** @var callable $callable */ - $callable = $this->callableResolver->resolve($this->callable); - $callable($app); - return $this; - } } diff --git a/tests/AppTest.php b/tests/AppTest.php index 6f8ad8d64..e13124e1e 100644 --- a/tests/AppTest.php +++ b/tests/AppTest.php @@ -26,8 +26,10 @@ use Slim\Handlers\Strategies\RequestResponseArgs; use Slim\Interfaces\CallableResolverInterface; use Slim\Interfaces\RouteCollectorInterface; +use Slim\Interfaces\RouteCollectorProxyInterface; use Slim\Interfaces\RouteResolverInterface; use Slim\Routing\RouteCollector; +use Slim\Routing\RouteCollectorProxy; use Slim\Tests\Mocks\MockAction; use stdClass; @@ -89,22 +91,6 @@ public function testGetRouteCollectorReturnsInjectedInstance() $this->assertSame($routeCollectorProphecy->reveal(), $app->getRouteCollector()); } - public function testGetRouteResolverReturnsInjectedInstance() - { - $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class); - $routeCollectorProphecy = $this->prophesize(RouteCollectorInterface::class); - $routeResolverProphecy = $this->prophesize(RouteResolverInterface::class); - $app = new App( - $responseFactoryProphecy->reveal(), - null, - null, - $routeCollectorProphecy->reveal(), - $routeResolverProphecy->reveal() - ); - - $this->assertSame($routeResolverProphecy->reveal(), $app->getRouteResolver()); - } - public function testCreatesRouteCollectorWhenNullWithInjectedContainer() { $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class); @@ -785,8 +771,8 @@ public function testAddMiddlewareOnRouteGroup() }); $app = new App($responseFactoryProphecy->reveal()); - $app->group('/foo', function (App $app) use (&$output) { - $app->get('/bar', function (ServerRequestInterface $request, ResponseInterface $response) use (&$output) { + $app->group('/foo', function (RouteCollectorProxy $proxy) use (&$output) { + $proxy->get('/bar', function (ServerRequestInterface $request, ResponseInterface $response) use (&$output) { $output .= 'Center'; return $response; }); @@ -889,9 +875,16 @@ public function testAddMiddlewareOnTwoRouteGroup() }); $app = new App($responseFactoryProphecy->reveal()); - $app->group('/foo', function (App $app) use ($middlewareProphecy2, $middlewareProphecy3, &$output) { - $app->group('/bar', function (App $app) use ($middlewareProphecy3, &$output) { - $app->get('/baz', function ( + $app->group('/foo', function (RouteCollectorProxyInterface $group) use ( + $middlewareProphecy2, + $middlewareProphecy3, + &$output + ) { + $group->group('/bar', function (RouteCollectorProxyInterface $group) use ( + $middlewareProphecy3, + &$output + ) { + $group->get('/baz', function ( ServerRequestInterface $request, ResponseInterface $response ) use (&$output) { diff --git a/tests/Routing/RouteCollectorTest.php b/tests/Routing/RouteCollectorTest.php index 17030e86c..251308760 100644 --- a/tests/Routing/RouteCollectorTest.php +++ b/tests/Routing/RouteCollectorTest.php @@ -15,6 +15,7 @@ use Slim\Interfaces\CallableResolverInterface; use Slim\Interfaces\InvocationStrategyInterface; use Slim\Routing\RouteCollector; +use Slim\Routing\RouteCollectorProxy; use Slim\Tests\TestCase; class RouteCollectorTest extends TestCase @@ -59,19 +60,25 @@ public function testMap() public function testMapPrependsGroupPattern() { + $self = $this; + $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class); - $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class); - $routeCollector = new RouteCollector($responseFactoryProphecy->reveal(), $callableResolverProphecy->reveal()); - $routeCollector->pushGroup('/prefix', function () { - }); + $callable = function (RouteCollectorProxy $proxy) use ($self) { + $route = $proxy->get('/test', function () { + }); - $route = $routeCollector->map(['GET'], '/test', function () { - }); + $self->assertEquals('/prefix/test', $route->getPattern()); + }; - $routeCollector->popGroup(); + $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class); + $callableResolverProphecy + ->resolve($callable) + ->willReturn($callable) + ->shouldBeCalledOnce(); - $this->assertEquals('/prefix/test', $route->getPattern()); + $routeCollector = new RouteCollector($responseFactoryProphecy->reveal(), $callableResolverProphecy->reveal()); + $routeCollector->group('/prefix', $callable); } public function testGetRouteInvocationStrategy() diff --git a/tests/Routing/RouteTest.php b/tests/Routing/RouteTest.php index 57c445291..c52572af8 100644 --- a/tests/Routing/RouteTest.php +++ b/tests/Routing/RouteTest.php @@ -11,15 +11,20 @@ use Closure; use Exception; +use Prophecy\Argument; use Psr\Container\ContainerInterface; +use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\StreamInterface; use Psr\Http\Server\RequestHandlerInterface; use Slim\CallableResolver; use Slim\DeferredCallable; use Slim\Handlers\Strategies\RequestHandler; use Slim\Handlers\Strategies\RequestResponse; +use Slim\Interfaces\CallableResolverInterface; use Slim\Interfaces\InvocationStrategyInterface; +use Slim\Interfaces\RouteCollectorProxyInterface; use Slim\Routing\Route; use Slim\Routing\RouteGroup; use Slim\Tests\Mocks\CallableTest; @@ -38,15 +43,56 @@ class RouteTest extends TestCase */ public function createRoute($methods = 'GET', string $pattern = '/', $callable = null): Route { - $callable = $callable ?? function (ServerRequestInterface $request, ResponseInterface $response, $args) { + $callable = $callable ?? function (ServerRequestInterface $request, ResponseInterface $response) { return $response; }; - $responseFactory = $this->getResponseFactory(); - $callableResolver = new CallableResolver(); + $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class); + $callableResolverProphecy + ->resolve($callable) + ->willReturn($callable); + + $streamProphecy = $this->prophesize(StreamInterface::class); + + $value = ''; + $streamProphecy + ->write(Argument::type('string')) + ->will(function ($args) use ($value) { + $value .= $args[0]; + $this->__toString()->willReturn($value); + return $this->reveal(); + }); + + $streamProphecy + ->__toString() + ->willReturn($value); + + $responseProphecy = $this->prophesize(ResponseInterface::class); + + $responseProphecy + ->getBody() + ->willReturn($streamProphecy->reveal()); + + $responseProphecy + ->withStatus(Argument::type('integer')) + ->will(function ($args) { + $this->getStatusCode()->willReturn($args[0]); + return $this->reveal(); + }); + + $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class); + $responseFactoryProphecy + ->createResponse() + ->willReturn($responseProphecy->reveal()); $methods = is_string($methods) ? [$methods] : $methods; - return new Route($methods, $pattern, $callable, $responseFactory, $callableResolver); + return new Route( + $methods, + $pattern, + $callable, + $responseFactoryProphecy->reveal(), + $callableResolverProphecy->reveal() + ); } public function testConstructor() @@ -58,12 +104,12 @@ public function testConstructor() }; $route = $this->createRoute($methods, $pattern, $callable); - $this->assertAttributeEquals($methods, 'methods', $route); - $this->assertAttributeEquals($pattern, 'pattern', $route); - $this->assertAttributeEquals($callable, 'callable', $route); + $this->assertEquals($methods, $route->getMethods()); + $this->assertEquals($pattern, $route->getPattern()); + $this->assertEquals($callable, $route->getCallable()); } - public function testGetMethodsReturnsArrayWhenContructedWithString() + public function testGetMethodsReturnsArrayWhenConstructedWithString() { $route = $this->createRoute(); @@ -94,63 +140,99 @@ public function testGetCallable() public function testGetCallableResolver() { - $callable = function (ServerRequestInterface $request, ResponseInterface $response, $args) { + $callable = function (ServerRequestInterface $request, ResponseInterface $response) { return $response; }; - $responseFactory = $this->getResponseFactory(); - $callableResolver = new CallableResolver(); + $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class); + $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class); - $route = new Route(['GET'], '/', $callable, $responseFactory, $callableResolver); + $route = new Route( + ['GET'], + '/', + $callable, + $responseFactoryProphecy->reveal(), + $callableResolverProphecy->reveal() + ); - $this->assertEquals($callableResolver, $route->getCallableResolver()); + $this->assertSame($callableResolverProphecy->reveal(), $route->getCallableResolver()); } public function testGetInvocationStrategy() { - $callable = function (ServerRequestInterface $request, ResponseInterface $response, $args) { + $callable = function (ServerRequestInterface $request, ResponseInterface $response) { return $response; }; - $responseFactory = $this->getResponseFactory(); - $callableResolver = new CallableResolver(); - $strategy = new RequestResponse(); - - $route = new Route(['GET'], '/', $callable, $responseFactory, $callableResolver, null, $strategy); + $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class); + $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class); + $containerProphecy = $this->prophesize(ContainerInterface::class); + $strategyProphecy = $this->prophesize(InvocationStrategyInterface::class); + + $route = new Route( + ['GET'], + '/', + $callable, + $responseFactoryProphecy->reveal(), + $callableResolverProphecy->reveal(), + $containerProphecy->reveal(), + $strategyProphecy->reveal() + ); - $this->assertEquals($strategy, $route->getInvocationStrategy()); + $this->assertSame($strategyProphecy->reveal(), $route->getInvocationStrategy()); } public function testSetInvocationStrategy() { - $callable = function (ServerRequestInterface $request, ResponseInterface $response, $args) { + $callable = function (ServerRequestInterface $request, ResponseInterface $response) { return $response; }; - $responseFactory = $this->getResponseFactory(); - $callableResolver = new CallableResolver(); - $strategy = new RequestResponse(); + $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class); + $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class); + $strategyProphecy = $this->prophesize(InvocationStrategyInterface::class); - $route = new Route(['GET'], '/', $callable, $responseFactory, $callableResolver); - $route->setInvocationStrategy($strategy); + $route = new Route( + ['GET'], + '/', + $callable, + $responseFactoryProphecy->reveal(), + $callableResolverProphecy->reveal() + ); + $route->setInvocationStrategy($strategyProphecy->reveal()); - $this->assertSame($strategy, $route->getInvocationStrategy()); + $this->assertSame($strategyProphecy->reveal(), $route->getInvocationStrategy()); } public function testGetGroups() { - $callable = function (ServerRequestInterface $request, ResponseInterface $response, $args) { + $callable = function (ServerRequestInterface $request, ResponseInterface $response) { return $response; }; - $responseFactory = $this->getResponseFactory(); - $callableResolver = new CallableResolver(); - $strategy = new RequestResponse(); + $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class); + $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class); + $strategyProphecy = $this->prophesize(InvocationStrategyInterface::class); + $routeCollectorProxyProphecy = $this->prophesize(RouteCollectorProxyInterface::class); - $routeGroup = new RouteGroup('/group', $callable, $callableResolver); + $routeGroup = new RouteGroup( + '/group', + $callable, + $callableResolverProphecy->reveal(), + $routeCollectorProxyProphecy->reveal() + ); $groups = [$routeGroup]; - $route = new Route(['GET'], '/', $callable, $responseFactory, $callableResolver, null, $strategy, $groups); + $route = new Route( + ['GET'], + '/', + $callable, + $responseFactoryProphecy->reveal(), + $callableResolverProphecy->reveal(), + null, + $strategyProphecy->reveal(), + $groups + ); $this->assertEquals($groups, $route->getGroups()); } @@ -178,7 +260,7 @@ public function testAddMiddleware() $route = $this->createRoute(); $called = 0; - $mw = function ($request, $handler) use (&$called) { + $mw = function (ServerRequestInterface $request, RequestHandlerInterface $handler) use (&$called) { $called++; return $handler->handle($request); }; @@ -192,12 +274,25 @@ public function testAddMiddleware() public function testAddMiddlewareOnGroup() { - $callable = function (ServerRequestInterface $request, ResponseInterface $response, $args) { + $callable = function (ServerRequestInterface $request, ResponseInterface $response) { return $response; }; - $responseFactory = $this->getResponseFactory(); - $callableResolver = new CallableResolver(); + $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class); + $callableResolverProphecy + ->resolve($callable) + ->willReturn($callable) + ->shouldBeCalledOnce(); + + $responseProphecy = $this->prophesize(ResponseInterface::class); + + $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class); + $responseFactoryProphecy + ->createResponse() + ->willReturn($responseProphecy->reveal()) + ->shouldBeCalledOnce(); + + $routeCollectorProxyProphecy = $this->prophesize(RouteCollectorProxyInterface::class); $strategy = new RequestResponse(); $called = 0; @@ -205,11 +300,26 @@ public function testAddMiddlewareOnGroup() $called++; return $handler->handle($request); }; - $routeGroup = new RouteGroup('/group', $callable, $callableResolver); + + $routeGroup = new RouteGroup( + '/group', + $callable, + $callableResolverProphecy->reveal(), + $routeCollectorProxyProphecy->reveal() + ); $routeGroup->add($mw); $groups = [$routeGroup]; - $route = new Route(['GET'], '/', $callable, $responseFactory, $callableResolver, null, $strategy, $groups); + $route = new Route( + ['GET'], + '/', + $callable, + $responseFactoryProphecy->reveal(), + $callableResolverProphecy->reveal(), + null, + $strategy, + $groups + ); $request = $this->createServerRequest('/'); $route->run($request); @@ -222,7 +332,7 @@ public function testAddClosureMiddleware() $route = $this->createRoute(); $called = 0; - $route->add(function ($request, $handler) use (&$called) { + $route->add(function (ServerRequestInterface $request, RequestHandlerInterface $handler) use (&$called) { $called++; return $handler->handle($request); }); @@ -292,17 +402,45 @@ public function testControllerMethodAsStringResolvesWithoutContainer() public function testControllerMethodAsStringResolvesWithContainer() { - $containerProphecy = $this->prophesize(ContainerInterface::class); - $containerProphecy->has('CallableTest')->willReturn(true); - $containerProphecy->get('CallableTest')->willReturn(new CallableTest()); - - $callableResolver = new CallableResolver($containerProphecy->reveal()); - $responseFactory = $this->getResponseFactory(); - - $deferred = new DeferredCallable('CallableTest:toCall', $callableResolver); - $route = new Route(['GET'], '/', $deferred, $responseFactory, $callableResolver); - - CallableTest::$CalledCount = 0; + $self = $this; + + $responseProphecy = $this->prophesize(ResponseInterface::class); + $callableResolverProphecy = $this->prophesize(CallableResolverInterface::class); + + $callable = 'CallableTest:toCall'; + $deferred = new DeferredCallable($callable, $callableResolverProphecy->reveal()); + + $callableResolverProphecy + ->resolve($callable) + ->willReturn(function ( + ServerRequestInterface $request, + ResponseInterface $response + ) use ( + $self, + $responseProphecy +) { + $self->assertSame($responseProphecy->reveal(), $response); + return $response; + }) + ->shouldBeCalledOnce(); + + $callableResolverProphecy + ->resolve($deferred) + ->willReturn($deferred) + ->shouldBeCalledOnce(); + + $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class); + $responseFactoryProphecy + ->createResponse() + ->willReturn($responseProphecy->reveal()); + + $route = new Route( + ['GET'], + '/', + $deferred, + $responseFactoryProphecy->reveal(), + $callableResolverProphecy->reveal() + ); $request = $this->createServerRequest('/'); $response = $route->run($request); @@ -317,14 +455,12 @@ public function testControllerMethodAsStringResolvesWithContainer() */ public function testProcessWhenReturningAResponse() { - $callable = function (ServerRequestInterface $request, ResponseInterface $response, $args) { + $callable = function (ServerRequestInterface $request, ResponseInterface $response) { $response->getBody()->write('foo'); return $response; }; $route = $this->createRoute(['GET'], '/', $callable); - CallableTest::$CalledCount = 0; - $request = $this->createServerRequest('/'); $response = $route->run($request); @@ -337,7 +473,7 @@ public function testProcessWhenReturningAResponse() */ public function testRouteCallableDoesNotAppendEchoedOutput() { - $callable = function (ServerRequestInterface $request, ResponseInterface $response, $args) { + $callable = function (ServerRequestInterface $request, ResponseInterface $response) { echo "foo"; return $response->withStatus(201); }; @@ -361,7 +497,7 @@ public function testRouteCallableDoesNotAppendEchoedOutput() */ public function testRouteCallableAppendsCorrectOutputToResponse() { - $callable = function (ServerRequestInterface $request, ResponseInterface $response, $args) { + $callable = function (ServerRequestInterface $request, ResponseInterface $response) { $response->getBody()->write('foo'); return $response; }; @@ -378,7 +514,7 @@ public function testRouteCallableAppendsCorrectOutputToResponse() */ public function testInvokeWithException() { - $callable = function (ServerRequestInterface $request, ResponseInterface $response, $args) { + $callable = function (ServerRequestInterface $request, ResponseInterface $response) { throw new Exception(); }; $route = $this->createRoute(['GET'], '/', $callable); @@ -392,12 +528,27 @@ public function testInvokeWithException() */ public function testInvokeDeferredCallableWithNoContainer() { + $responseProphecy = $this->prophesize(ResponseInterface::class); + + $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class); + $responseFactoryProphecy + ->createResponse() + ->willReturn($responseProphecy->reveal()) + ->shouldBeCalledOnce(); + $callableResolver = new CallableResolver(); - $responseFactory = $this->getResponseFactory(); $invocationStrategy = new InvocationStrategyTest(); $deferred = '\Slim\Tests\Mocks\CallableTest:toCall'; - $route = new Route(['GET'], '/', $deferred, $responseFactory, $callableResolver, null, $invocationStrategy); + $route = new Route( + ['GET'], + '/', + $deferred, + $responseFactoryProphecy->reveal(), + $callableResolver, + null, + $invocationStrategy + ); $request = $this->createServerRequest('/'); $response = $route->run($request); @@ -411,17 +562,31 @@ public function testInvokeDeferredCallableWithNoContainer() */ public function testInvokeDeferredCallableWithContainer() { + $responseProphecy = $this->prophesize(ResponseInterface::class); + + $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class); + $responseFactoryProphecy + ->createResponse() + ->willReturn($responseProphecy->reveal()) + ->shouldBeCalledOnce(); + $containerProphecy = $this->prophesize(ContainerInterface::class); $containerProphecy->has('\Slim\Tests\Mocks\CallableTest')->willReturn(true); $containerProphecy->get('\Slim\Tests\Mocks\CallableTest')->willReturn(new CallableTest()); - $container = $containerProphecy->reveal(); - $callableResolver = new CallableResolver($container); - $responseFactory = $this->getResponseFactory(); + $callableResolver = new CallableResolver($containerProphecy->reveal()); $strategy = new InvocationStrategyTest(); $deferred = '\Slim\Tests\Mocks\CallableTest:toCall'; - $route = new Route(['GET'], '/', $deferred, $responseFactory, $callableResolver, $container, $strategy); + $route = new Route( + ['GET'], + '/', + $deferred, + $responseFactoryProphecy->reveal(), + $callableResolver, + $containerProphecy->reveal(), + $strategy + ); $request = $this->createServerRequest('/'); $response = $route->run($request); @@ -432,22 +597,38 @@ public function testInvokeDeferredCallableWithContainer() public function testInvokeUsesRequestHandlerStrategyForRequestHandlers() { + $responseProphecy = $this->prophesize(ResponseInterface::class); + + $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class); + $responseFactoryProphecy + ->createResponse() + ->willReturn($responseProphecy->reveal()) + ->shouldBeCalledOnce(); + $containerProphecy = $this->prophesize(ContainerInterface::class); $containerProphecy->has(RequestHandlerTest::class)->willReturn(true); $containerProphecy->get(RequestHandlerTest::class)->willReturn(new RequestHandlerTest()); - $container = $containerProphecy->reveal(); - $callableResolver = new CallableResolver($container); - $responseFactory = $this->getResponseFactory(); + $callableResolver = new CallableResolver($containerProphecy->reveal()); $deferred = RequestHandlerTest::class; - $route = new Route(['GET'], '/', $deferred, $responseFactory, $callableResolver, $container); + $route = new Route( + ['GET'], + '/', + $deferred, + $responseFactoryProphecy->reveal(), + $callableResolver, + $containerProphecy->reveal() + ); $request = $this->createServerRequest('/', 'GET'); $route->run($request); /** @var InvocationStrategyInterface $strategy */ - $strategy = $container->get(RequestHandlerTest::class)::$strategy; + $strategy = $containerProphecy + ->reveal() + ->get(RequestHandlerTest::class)::$strategy; + $this->assertEquals(RequestHandler::class, $strategy); } @@ -467,11 +648,24 @@ public function testPatternCanBeChanged() */ public function testChangingCallableWithNoContainer() { + $responseProphecy = $this->prophesize(ResponseInterface::class); + + $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class); + $responseFactoryProphecy + ->createResponse() + ->willReturn($responseProphecy->reveal()) + ->shouldBeCalledOnce(); + $callableResolver = new CallableResolver(); - $responseFactory = $this->getResponseFactory(); $deferred = 'NonExistent:toCall'; - $route = new Route(['GET'], '/', $deferred, $responseFactory, $callableResolver, null); + $route = new Route( + ['GET'], + '/', + $deferred, + $responseFactoryProphecy->reveal(), + $callableResolver + ); $route->setCallable('\Slim\Tests\Mocks\CallableTest:toCall'); //Then we fix it here. $request = $this->createServerRequest('/'); @@ -486,46 +680,96 @@ public function testChangingCallableWithNoContainer() */ public function testChangingCallableWithContainer() { + $responseProphecy = $this->prophesize(ResponseInterface::class); + + $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class); + $responseFactoryProphecy + ->createResponse() + ->willReturn($responseProphecy->reveal()) + ->shouldBeCalledOnce(); + $containerProphecy = $this->prophesize(ContainerInterface::class); $containerProphecy->has('CallableTest2')->willReturn(true); $containerProphecy->get('CallableTest2')->willReturn(new CallableTest()); - $container = $containerProphecy->reveal(); - $callableResolver = new CallableResolver($container); - $responseFactory = $this->getResponseFactory(); + $callableResolver = new CallableResolver($containerProphecy->reveal()); $strategy = new InvocationStrategyTest(); $deferred = 'NonExistent:toCall'; - $route = new Route(['GET'], '/', $deferred, $responseFactory, $callableResolver, $container, $strategy); + $route = new Route( + ['GET'], + '/', + $deferred, + $responseFactoryProphecy->reveal(), + $callableResolver, + $containerProphecy->reveal(), + $strategy + ); $route->setCallable('CallableTest2:toCall'); //Then we fix it here. $request = $this->createServerRequest('/'); $response = $route->run($request); $this->assertInstanceOf(ResponseInterface::class, $response); - $this->assertEquals([$container->get('CallableTest2'), 'toCall'], InvocationStrategyTest::$LastCalledFor); + $this->assertEquals( + [$containerProphecy->reveal()->get('CallableTest2'), 'toCall'], + InvocationStrategyTest::$LastCalledFor + ); } public function testRouteCallableIsResolvedUsingContainerWhenCallableResolverIsPresent() { - $responseFactory = $this->getResponseFactory(); + $streamProphecy = $this->prophesize(StreamInterface::class); + + $value = ''; + $streamProphecy + ->write(Argument::type('string')) + ->will(function ($args) use ($value) { + $value .= $args[0]; + $this->__toString()->willReturn($value); + return $this->reveal(); + }); + + $streamProphecy + ->__toString() + ->willReturn($value); + + $responseProphecy = $this->prophesize(ResponseInterface::class); + + $responseProphecy + ->getBody() + ->willReturn($streamProphecy->reveal()) + ->shouldBeCalledTimes(2); + + $responseFactoryProphecy = $this->prophesize(ResponseFactoryInterface::class); + $responseFactoryProphecy + ->createResponse() + ->willReturn($responseProphecy->reveal()) + ->shouldBeCalledOnce(); $containerProphecy = $this->prophesize(ContainerInterface::class); $containerProphecy->has('CallableTest3')->willReturn(true); $containerProphecy->get('CallableTest3')->willReturn(new CallableTest()); $containerProphecy->has('ClosureMiddleware')->willReturn(true); - $containerProphecy->get('ClosureMiddleware')->willReturn(function ($request, $handler) use ($responseFactory) { - $response = $responseFactory->createResponse(); + $containerProphecy->get('ClosureMiddleware')->willReturn(function () use ($responseFactoryProphecy) { + $response = $responseFactoryProphecy->reveal()->createResponse(); $response->getBody()->write('Hello'); return $response; }); - $container = $containerProphecy->reveal(); - $callableResolver = new CallableResolver($container); + $callableResolver = new CallableResolver($containerProphecy->reveal()); $strategy = new InvocationStrategyTest(); $deferred = 'CallableTest3'; - $route = new Route(['GET'], '/', $deferred, $responseFactory, $callableResolver, $container, $strategy); + $route = new Route( + ['GET'], + '/', + $deferred, + $responseFactoryProphecy->reveal(), + $callableResolver, + $containerProphecy->reveal(), + $strategy + ); $route->add('ClosureMiddleware'); $request = $this->createServerRequest('/');