Skip to content

Commit

Permalink
new redirection response type and handler
Browse files Browse the repository at this point in the history
  • Loading branch information
juliangut committed Nov 5, 2023
1 parent c9d4018 commit a21af64
Show file tree
Hide file tree
Showing 5 changed files with 375 additions and 7 deletions.
21 changes: 14 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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();
Expand Down Expand Up @@ -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

Expand All @@ -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

Expand Down
68 changes: 68 additions & 0 deletions src/Response/Handler/RedirectResponseHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

/*
* slim-routing (https://github.com/juliangut/slim-routing).
* Slim framework routing.
*
* @license BSD-3-Clause
* @link https://github.com/juliangut/slim-routing
* @author Julián Gutiérrez <[email protected]>
*/

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);
}
}
131 changes: 131 additions & 0 deletions src/Response/RedirectResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<?php

/*
* slim-routing (https://github.com/juliangut/slim-routing).
* Slim framework routing.
*
* @license BSD-3-Clause
* @link https://github.com/juliangut/slim-routing
* @author Julián Gutiérrez <[email protected]>
*/

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<string, int|float|string|null>
*/
private array $data = [],
/**
* @var array<string, int|float|string|null>
*/
private array $queryParams = [],
) {
parent::__construct($request);
}

/**
* @param array<string, int|float|string|null> $data
* @param array<string, int|float|string|null> $queryParams
*/
public static function movedPermanently(
string $location,
ServerRequestInterface $request,
array $data = [],
array $queryParams = [],
): self {
return new self($location, 301, $request, $data, $queryParams);
}

/**
* @param array<string, int|float|string|null> $data
* @param array<string, int|float|string|null> $queryParams
*/
public static function found(
string $location,
ServerRequestInterface $request,
array $data = [],
array $queryParams = [],
): self {
return new self($location, 302, $request, $data, $queryParams);
}

/**
* @param array<string, int|float|string|null> $data
* @param array<string, int|float|string|null> $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<string, int|float|string|null> $data
* @param array<string, int|float|string|null> $queryParams
*/
public static function temporaryRedirect(
string $location,
ServerRequestInterface $request,
array $data = [],
array $queryParams = [],
): self {
return new self($location, 307, $request, $data, $queryParams);
}

/**
* @param array<string, int|float|string|null> $data
* @param array<string, int|float|string|null> $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<string, int|float|string|null>
*/
public function getData(): array
{
return $this->data;
}

/**
* @return array<string, int|float|string|null>
*/
public function getQueryParams(): array
{
return $this->queryParams;
}
}
118 changes: 118 additions & 0 deletions tests/Routing/Response/Handler/RedirectResponseHandlerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php

/*
* slim-routing (https://github.com/juliangut/slim-routing).
* Slim framework routing.
*
* @license BSD-3-Clause
* @link https://github.com/juliangut/slim-routing
* @author Julián Gutiérrez <[email protected]>
*/

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'));
}
}
Loading

0 comments on commit a21af64

Please sign in to comment.