From a21af6444303c1287e8822305cc24cb2a1b63618 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juli=C3=A1n=20Guti=C3=A9rrez?= Date: Sun, 5 Nov 2023 21:35:47 +0100 Subject: [PATCH] new redirection response type and handler --- README.md | 21 ++- .../Handler/RedirectResponseHandler.php | 68 +++++++++ src/Response/RedirectResponse.php | 131 ++++++++++++++++++ .../Handler/RedirectResponseHandlerTest.php | 118 ++++++++++++++++ .../Routing/Response/RedirectResponseTest.php | 44 ++++++ 5 files changed, 375 insertions(+), 7 deletions(-) create mode 100644 src/Response/Handler/RedirectResponseHandler.php create mode 100644 src/Response/RedirectResponse.php create mode 100644 tests/Routing/Response/Handler/RedirectResponseHandlerTest.php create mode 100644 tests/Routing/Response/RedirectResponseTest.php diff --git a/README.md b/README.md index 233cefb..d115a05 100644 --- a/README.md +++ b/README.md @@ -58,8 +58,10 @@ require './vendor/autoload.php'; use Jgut\Slim\Routing\AppFactory; use Jgut\Slim\Routing\Configuration; use Jgut\Slim\Routing\Response\PayloadResponse; +use Jgut\Slim\Routing\Response\RedirectResponse; use Jgut\Slim\Routing\Response\ResponseType; use Jgut\Slim\Routing\Response\Handler\JsonResponseHandler; +use Jgut\Slim\Routing\Response\Handler\RedirectResponseHandler; use Jgut\Slim\Routing\Strategy\RequestHandler; use Psr\Http\Message\ServerRequestInterface; @@ -71,16 +73,19 @@ AppFactory::setRouteCollectorConfiguration($configuration); // Instantiate the app $app = AppFactory::create(); +$routeCollector = $app->getRouteCollector(); +$responseFactory = $app->getResponseFactory(); // Register custom invocation strategy to handle ResponseType objects $invocationStrategy = new RequestHandler( [ + RedirectResponse::class => new RedirectResponseHandler($responseFactory, $routeCollector), + // Handlers can be pulled from the container PayloadResponse::class => JsonResponseHandler::class, ], - $app->getResponseFactory(), + $responseFactory, $app->getContainer() ); -$routeCollector = $app->getRouteCollector(); $routeCollector->setDefaultInvocationStrategy($invocationStrategy); $cache = new PSR16Cache(); @@ -171,8 +176,9 @@ $app->get( If a route returns an instance of `\Jgut\Slim\Routing\Response\ResponseType` it will be passed to the corresponding handler according to configuration -There are two response types already provided: +There are three response types already provided: +* `RedirectResponse` Slim's route aware redirection, can redirect to a Slim route or an external location * `PayloadResponse` stores simple payload data to be later transformed for example into JSON or XML * `ViewResponse` keeps agnostic template parameters, so they can be rendered in a handler @@ -195,11 +201,12 @@ $invocationStrategy->setResponseHandler(PayloadResponse::class, JsonResponseHand Provided response types handlers: -* `JsonResponseHandler` receives a PayloadResponse and returns a JSON response -* `XmlResponseHandler` receives a PayloadResponse and returns a XML response (requires [spatie/array-to-xml](https://github.com/spatie/array-to-xml)) -* `TwigViewResponseHandler` receives a generic ViewResponse and returns a template rendered thanks to [slim/twig-view](https://github.com/slimphp/Twig-View) +* `RedirectResponseHandler` receives a RedirectResponse type and returns the corresponding PSR7 redirect response +* `JsonResponseHandler` receives a PayloadResponse type and returns a PSR7 JSON response +* `XmlResponseHandler` receives a PayloadResponse type and returns a PSR7 XML response (requires [spatie/array-to-xml](https://github.com/spatie/array-to-xml)) +* `TwigViewResponseHandler` receives a generic ViewResponse type and returns a template rendered PSR7 response thanks to [slim/twig-view](https://github.com/slimphp/Twig-View) -You can create your own response type handlers to compose specifically formatted response (JSON:API, ...) or use another template engines (Plates, ...) +You can create your own response type handlers to compose specifically formatted response (JSON:API, ...) or use another template engines (Plates, Blade, ...), or craft any other response ## Parameter transformation diff --git a/src/Response/Handler/RedirectResponseHandler.php b/src/Response/Handler/RedirectResponseHandler.php new file mode 100644 index 0000000..c862d8d --- /dev/null +++ b/src/Response/Handler/RedirectResponseHandler.php @@ -0,0 +1,68 @@ + + */ + +declare(strict_types=1); + +namespace Jgut\Slim\Routing\Response\Handler; + +use InvalidArgumentException; +use Jgut\Slim\Routing\Response\RedirectResponse; +use Jgut\Slim\Routing\Response\ResponseType; +use Psr\Http\Message\ResponseFactoryInterface; +use Psr\Http\Message\ResponseInterface; +use Slim\Interfaces\RouteCollectorInterface; + +final class RedirectResponseHandler extends AbstractResponseHandler +{ + private const NOT_MODIFIED_STATUS = 304; + + public function __construct( + ResponseFactoryInterface $responseFactory, + private RouteCollectorInterface $routeCollector, + ) { + parent::__construct($responseFactory); + } + + public function handle(ResponseType $responseType): ResponseInterface + { + if (!$responseType instanceof RedirectResponse) { + throw new InvalidArgumentException( + sprintf('Response type should be an instance of %s.', RedirectResponse::class), + ); + } + + if ($responseType->getStatus() === self::NOT_MODIFIED_STATUS) { + return $this->getResponse($responseType) + ->withStatus(304); + } + + $location = $responseType->getLocation(); + if (!str_starts_with($location, '/') && filter_var($location, \FILTER_VALIDATE_URL) === false) { + $location = $this->routeCollector + ->getRouteParser() + ->urlFor( + $location, + array_map( + static fn(int|float|string|null $data): string => (string) $data, + $responseType->getData(), + ), + array_map( + static fn(int|float|string|null $param): string => (string) $param, + $responseType->getQueryParams(), + ), + ); + } + + return $this->getResponse($responseType) + ->withStatus($responseType->getStatus()) + ->withHeader('Location', $location); + } +} diff --git a/src/Response/RedirectResponse.php b/src/Response/RedirectResponse.php new file mode 100644 index 0000000..ea90090 --- /dev/null +++ b/src/Response/RedirectResponse.php @@ -0,0 +1,131 @@ + + */ + +declare(strict_types=1); + +namespace Jgut\Slim\Routing\Response; + +use Psr\Http\Message\ServerRequestInterface; + +final class RedirectResponse extends AbstractResponse +{ + private function __construct( + private string $location, + private int $status, + ServerRequestInterface $request, + /** + * @var array + */ + private array $data = [], + /** + * @var array + */ + private array $queryParams = [], + ) { + parent::__construct($request); + } + + /** + * @param array $data + * @param array $queryParams + */ + public static function movedPermanently( + string $location, + ServerRequestInterface $request, + array $data = [], + array $queryParams = [], + ): self { + return new self($location, 301, $request, $data, $queryParams); + } + + /** + * @param array $data + * @param array $queryParams + */ + public static function found( + string $location, + ServerRequestInterface $request, + array $data = [], + array $queryParams = [], + ): self { + return new self($location, 302, $request, $data, $queryParams); + } + + /** + * @param array $data + * @param array $queryParams + */ + public static function seeOther( + string $location, + ServerRequestInterface $request, + array $data = [], + array $queryParams = [], + ): self { + return new self($location, 303, $request, $data, $queryParams); + } + + public static function notModified(ServerRequestInterface $request): self + { + return new self('', 304, $request); + } + + /** + * @param array $data + * @param array $queryParams + */ + public static function temporaryRedirect( + string $location, + ServerRequestInterface $request, + array $data = [], + array $queryParams = [], + ): self { + return new self($location, 307, $request, $data, $queryParams); + } + + /** + * @param array $data + * @param array $queryParams + */ + public static function permanentRedirect( + string $location, + ServerRequestInterface $request, + array $data = [], + array $queryParams = [], + ): self { + return new self($location, 308, $request, $data, $queryParams); + } + + public function getLocation(): string + { + return $this->location; + } + + public function getStatus(): int + { + return $this->status; + } + + /** + * @return array + */ + public function getData(): array + { + return $this->data; + } + + /** + * @return array + */ + public function getQueryParams(): array + { + return $this->queryParams; + } +} diff --git a/tests/Routing/Response/Handler/RedirectResponseHandlerTest.php b/tests/Routing/Response/Handler/RedirectResponseHandlerTest.php new file mode 100644 index 0000000..9bbc0a1 --- /dev/null +++ b/tests/Routing/Response/Handler/RedirectResponseHandlerTest.php @@ -0,0 +1,118 @@ + + */ + +declare(strict_types=1); + +namespace Jgut\Slim\Routing\Tests\Response\Handler; + +use InvalidArgumentException; +use Jgut\Slim\Routing\Response\Handler\RedirectResponseHandler; +use Jgut\Slim\Routing\Response\RedirectResponse; +use Jgut\Slim\Routing\Tests\Stubs\ResponseStub; +use Laminas\Diactoros\ResponseFactory; +use PHPUnit\Framework\TestCase; +use Psr\Http\Message\ResponseFactoryInterface; +use Psr\Http\Message\ServerRequestInterface; +use Slim\Routing\RouteCollector; +use Slim\Routing\RouteParser; + +/** + * @internal + */ +class RedirectResponseHandlerTest extends TestCase +{ + protected ServerRequestInterface $request; + + protected function setUp(): void + { + $this->request = $this->getMockBuilder(ServerRequestInterface::class) + ->getMock(); + } + + public function testInvalidResponseType(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage( + 'Response type should be an instance of Jgut\Slim\Routing\Response\RedirectResponse', + ); + + $responseFactory = $this->getMockBuilder(ResponseFactoryInterface::class) + ->getMock(); + $routeCollector = $this->getMockBuilder(RouteCollector::class) + ->disableOriginalConstructor() + ->getMock(); + + (new RedirectResponseHandler($responseFactory, $routeCollector))->handle(new ResponseStub($this->request)); + } + + public function testNotModified(): void + { + $responseFactory = new ResponseFactory(); + $routeCollector = $this->getMockBuilder(RouteCollector::class) + ->disableOriginalConstructor() + ->getMock(); + + $response = (new RedirectResponseHandler($responseFactory, $routeCollector)) + ->handle(RedirectResponse::notModified($this->request)); + + static::assertEquals(304, $response->getStatusCode()); + static::assertEquals('', $response->getHeaderLine('Location')); + } + + public function testUrlRedirect(): void + { + $responseFactory = new ResponseFactory(); + $routeCollector = $this->getMockBuilder(RouteCollector::class) + ->disableOriginalConstructor() + ->getMock(); + + $response = (new RedirectResponseHandler($responseFactory, $routeCollector)) + ->handle(RedirectResponse::permanentRedirect('https://example.com', $this->request)); + + static::assertEquals('https://example.com', $response->getHeaderLine('Location')); + } + + public function testPathRedirect(): void + { + $responseFactory = new ResponseFactory(); + $routeCollector = $this->getMockBuilder(RouteCollector::class) + ->disableOriginalConstructor() + ->getMock(); + + $response = (new RedirectResponseHandler($responseFactory, $routeCollector)) + ->handle(RedirectResponse::permanentRedirect('/home', $this->request)); + + static::assertEquals('/home', $response->getHeaderLine('Location')); + } + + public function testRouteRedirect(): void + { + $responseFactory = new ResponseFactory(); + $routeParser = $this->getMockBuilder(RouteParser::class) + ->disableOriginalConstructor() + ->getMock(); + $routeParser + ->method('urlFor') + ->with('home') + ->willReturn('https://example.com/home'); + $routeCollector = $this->getMockBuilder(RouteCollector::class) + ->disableOriginalConstructor() + ->getMock(); + $routeCollector + ->method('getRouteParser') + ->willReturn($routeParser); + + $response = (new RedirectResponseHandler($responseFactory, $routeCollector)) + ->handle(RedirectResponse::permanentRedirect('home', $this->request)); + + static::assertEquals('https://example.com/home', $response->getHeaderLine('Location')); + } +} diff --git a/tests/Routing/Response/RedirectResponseTest.php b/tests/Routing/Response/RedirectResponseTest.php new file mode 100644 index 0000000..4807d21 --- /dev/null +++ b/tests/Routing/Response/RedirectResponseTest.php @@ -0,0 +1,44 @@ + + */ + +declare(strict_types=1); + +namespace Jgut\Slim\Routing\Tests\Response; + +use Jgut\Slim\Routing\Response\RedirectResponse; +use Laminas\Diactoros\ServerRequestFactory; +use PHPUnit\Framework\TestCase; + +/** + * @internal + */ +class RedirectResponseTest extends TestCase +{ + public static function provideRedirectionResponseTypes(): iterable + { + $request = ServerRequestFactory::fromGlobals(); + + yield [301, RedirectResponse::movedPermanently('https://example.com', $request)]; + yield [302, RedirectResponse::found('https://example.com', $request)]; + yield [303, RedirectResponse::seeOther('https://example.com', $request)]; + yield [304, RedirectResponse::notModified($request)]; + yield [307, RedirectResponse::temporaryRedirect('https://example.com', $request)]; + yield [308, RedirectResponse::permanentRedirect('https://example.com', $request)]; + } + + /** + * @dataProvider provideRedirectionResponseTypes + */ + public function testRedirectionResponseTypes($expectedStatus, RedirectResponse $responseType): void + { + static::assertEquals($expectedStatus, $responseType->getStatus()); + } +}