Skip to content

Commit 4e8f809

Browse files
authored
Add ContentNegotiator middleware
1 parent 3a1c1e5 commit 4e8f809

File tree

4 files changed

+222
-0
lines changed

4 files changed

+222
-0
lines changed

README.md

+24
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,30 @@ $middleware = (new FormatDataResponse(new JsonDataResponseFormatter()));
7070
//$middleware->process($request, $handler);
7171
```
7272

73+
Also the package provides PSR-15 middleware for content negotiation
74+
75+
```php
76+
use Yiisoft\DataResponse\Formatter\HtmlDataResponseFormatter;
77+
use Yiisoft\DataResponse\Formatter\XmlDataResponseFormatter;
78+
use Yiisoft\DataResponse\Formatter\JsonDataResponseFormatter;
79+
use Yiisoft\DataResponse\Middleware\ContentNegotiator;
80+
81+
$middleware = new ContentNegotiator([
82+
'text/html' => new HtmlDataResponseFormatter(),
83+
'application/xml' => new XmlDataResponseFormatter(),
84+
'application/json' => new JsonDataResponseFormatter(),
85+
]);
86+
```
87+
88+
You can override middlewares with method `withContentFormatters`
89+
90+
```php
91+
$middleware->withContentFormatters([
92+
'application/xml' => new XmlDataResponseFormatter(),
93+
'application/json' => new JsonDataResponseFormatter(),
94+
]);
95+
```
96+
7397
### Unit testing
7498

7599
The package is tested with [PHPUnit](https://phpunit.de/). To run tests:

config/web.php

+12
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,22 @@
66
use Yiisoft\DataResponse\DataResponseFactoryInterface;
77
use Yiisoft\DataResponse\DataResponseFormatterInterface;
88
use Yiisoft\DataResponse\Formatter\HtmlDataResponseFormatter;
9+
use Yiisoft\DataResponse\Formatter\XmlDataResponseFormatter;
10+
use Yiisoft\DataResponse\Formatter\JsonDataResponseFormatter;
11+
use Yiisoft\DataResponse\Middleware\ContentNegotiator;
912

1013
/* @var $params array */
1114

1215
return [
1316
DataResponseFormatterInterface::class => HtmlDataResponseFormatter::class,
1417
DataResponseFactoryInterface::class => DataResponseFactory::class,
18+
ContentNegotiator::class => [
19+
'__construct()' => [
20+
'contentFormatters' => [
21+
'text/html' => new HtmlDataResponseFormatter(),
22+
'application/xml' => new XmlDataResponseFormatter(),
23+
'application/json' => new JsonDataResponseFormatter(),
24+
],
25+
],
26+
],
1527
];

src/Middleware/ContentNegotiator.php

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\DataResponse\Middleware;
6+
7+
use Psr\Http\Message\ResponseInterface;
8+
use Psr\Http\Message\ServerRequestInterface;
9+
use Psr\Http\Server\MiddlewareInterface;
10+
use Psr\Http\Server\RequestHandlerInterface;
11+
use Yiisoft\DataResponse\DataResponse;
12+
use Yiisoft\DataResponse\DataResponseFormatterInterface;
13+
use Yiisoft\Http\Header;
14+
15+
/**
16+
* ContentNegotiator supports response format negotiation.
17+
*/
18+
final class ContentNegotiator implements MiddlewareInterface
19+
{
20+
private array $contentFormatters;
21+
22+
public function __construct(array $contentFormatters)
23+
{
24+
$this->checkFormatters($contentFormatters);
25+
$this->contentFormatters = $contentFormatters;
26+
}
27+
28+
/**
29+
* @param array $contentFormatters
30+
*/
31+
public function withContentFormatters(array $contentFormatters): self
32+
{
33+
$this->checkFormatters($contentFormatters);
34+
$new = clone $this;
35+
$new->contentFormatters = $contentFormatters;
36+
return $new;
37+
}
38+
39+
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
40+
{
41+
$response = $handler->handle($request);
42+
if ($response instanceof DataResponse && !$response->hasResponseFormatter()) {
43+
$accepted = $request->getHeader(Header::ACCEPT);
44+
45+
foreach ($accepted as $accept) {
46+
foreach ($this->contentFormatters as $contentType => $formatter) {
47+
if (strpos($accept, $contentType) !== false) {
48+
return $response->withResponseFormatter($formatter);
49+
}
50+
}
51+
}
52+
}
53+
54+
return $response;
55+
}
56+
57+
private function checkFormatters(array $contentFormatters): void
58+
{
59+
foreach ($contentFormatters as $contentType => $formatter) {
60+
if (!(is_string($contentType) && $formatter instanceof DataResponseFormatterInterface)) {
61+
throw new \RuntimeException('Invalid formatter type.');
62+
}
63+
}
64+
}
65+
}
+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\DataResponse\Tests\Middleware;
6+
7+
use Nyholm\Psr7\Factory\Psr17Factory;
8+
use Nyholm\Psr7\ServerRequest;
9+
use PHPUnit\Framework\TestCase;
10+
use Psr\Http\Message\ResponseInterface;
11+
use Psr\Http\Message\ServerRequestInterface;
12+
use Psr\Http\Server\RequestHandlerInterface;
13+
use Yiisoft\DataResponse\DataResponse;
14+
use Yiisoft\DataResponse\DataResponseFactory;
15+
use Yiisoft\DataResponse\Formatter\HtmlDataResponseFormatter;
16+
use Yiisoft\DataResponse\Formatter\XmlDataResponseFormatter;
17+
use Yiisoft\DataResponse\Formatter\JsonDataResponseFormatter;
18+
use Yiisoft\DataResponse\Middleware\ContentNegotiator;
19+
use Yiisoft\Http\Header;
20+
21+
class ContentNegotiatorTest extends TestCase
22+
{
23+
public function testAcceptHtml(): void
24+
{
25+
$middleware = new ContentNegotiator($this->getContentFormatters());
26+
$request = new ServerRequest('GET', '/', [Header::ACCEPT => 'text/html']);
27+
$response = $middleware->process($request, $this->getHandler('<div>Hello</div>'));
28+
$response->getBody()->rewind();
29+
$content = $response->getBody()->getContents();
30+
31+
$this->assertInstanceOf(DataResponse::class, $response);
32+
$this->assertTrue($response->hasResponseFormatter());
33+
$this->assertSame('<div>Hello</div>', $content);
34+
$this->assertSame('text/html; charset=UTF-8', $response->getHeader(Header::CONTENT_TYPE)[0]);
35+
}
36+
37+
public function testAcceptXml(): void
38+
{
39+
$middleware = new ContentNegotiator($this->getContentFormatters());
40+
$request = new ServerRequest('GET', '/', [Header::ACCEPT => 'application/xml']);
41+
$response = $middleware->process($request, $this->getHandler('Hello'));
42+
$response->getBody()->rewind();
43+
$content = $response->getBody()->getContents();
44+
45+
$this->assertInstanceOf(DataResponse::class, $response);
46+
$this->assertTrue($response->hasResponseFormatter());
47+
$this->assertSame($this->xml('<response>Hello</response>'), $content);
48+
$this->assertSame('application/xml; UTF-8', $response->getHeader(Header::CONTENT_TYPE)[0]);
49+
}
50+
51+
public function testAcceptJson(): void
52+
{
53+
$middleware = new ContentNegotiator($this->getContentFormatters());
54+
$request = new ServerRequest('GET', '/', [Header::ACCEPT => 'application/json']);
55+
$response = $middleware->process($request, $this->getHandler(['test' => 'Hello']));
56+
$response->getBody()->rewind();
57+
$content = $response->getBody()->getContents();
58+
59+
$this->assertInstanceOf(DataResponse::class, $response);
60+
$this->assertTrue($response->hasResponseFormatter());
61+
$this->assertSame('{"test":"Hello"}', $content);
62+
$this->assertSame('application/json', $response->getHeader(Header::CONTENT_TYPE)[0]);
63+
}
64+
65+
public function testWrongContentFormattersInConstructor()
66+
{
67+
$this->expectException(\RuntimeException::class);
68+
$this->expectErrorMessage('Invalid formatter type.');
69+
new ContentNegotiator($this->getWrongContentFormatters());
70+
}
71+
72+
public function testWrongContentFormattersInSetter()
73+
{
74+
$this->expectException(\RuntimeException::class);
75+
$this->expectErrorMessage('Invalid formatter type.');
76+
$middleware = new ContentNegotiator($this->getContentFormatters());
77+
$middleware->withContentFormatters($this->getWrongContentFormatters());
78+
}
79+
80+
private function getHandler($data): RequestHandlerInterface
81+
{
82+
return new class($data) implements RequestHandlerInterface {
83+
private $data;
84+
85+
public function __construct($data)
86+
{
87+
$this->data = $data;
88+
}
89+
90+
public function handle(ServerRequestInterface $request): ResponseInterface
91+
{
92+
$factory = new DataResponseFactory(new Psr17Factory());
93+
return $factory->createResponse($this->data);
94+
}
95+
};
96+
}
97+
98+
private function getContentFormatters(): array
99+
{
100+
return [
101+
'text/html' => new HtmlDataResponseFormatter(),
102+
'application/xml' => new XmlDataResponseFormatter(),
103+
'application/json' => new JsonDataResponseFormatter(),
104+
];
105+
}
106+
107+
private function getWrongContentFormatters(): array
108+
{
109+
return [
110+
'text/html' => new HtmlDataResponseFormatter(),
111+
'application/xml' => new \StdClass(),
112+
'application/json' => new JsonDataResponseFormatter(),
113+
];
114+
}
115+
116+
private function xml(string $data, string $version = '1.0', string $encoding = 'UTF-8'): string
117+
{
118+
$startLine = sprintf('<?xml version="%s" encoding="%s"?>', $version, $encoding);
119+
return $startLine . "\n" . preg_replace('/(?!item)\s(?!key)/', '', $data) . "\n";
120+
}
121+
}

0 commit comments

Comments
 (0)