Skip to content

Commit 184bea1

Browse files
committed
feat: add JsonResponder
1 parent 4cd8dd1 commit 184bea1

File tree

6 files changed

+279
-0
lines changed

6 files changed

+279
-0
lines changed

src/Resources/config/handler.php

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
use Pitch\AdrBundle\DependencyInjection\Compiler\ResponseHandlerPass;
55
use Pitch\AdrBundle\Responder\Handler\ObjectHandler;
66
use Pitch\AdrBundle\Responder\Handler\ScalarHandler;
7+
use Pitch\AdrBundle\Responder\Handler\JsonResponder;
78
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
89

910
return static function (ContainerConfigurator $container) {
@@ -14,5 +15,7 @@
1415
->tag(ResponseHandlerPass::TAG, ['priority' => -1024])
1516
->set(ObjectHandler::class)
1617
->tag(ResponseHandlerPass::TAG, ['priority' => -1024])
18+
->set(JsonResponder::class)
19+
->tag(ResponseHandlerPass::TAG, ['priority' => -8192])
1720
;
1821
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
namespace Pitch\AdrBundle\Responder\Handler;
3+
4+
use Pitch\AdrBundle\Configuration\DefaultContentType;
5+
use Pitch\AdrBundle\Responder\ResponsePayloadEvent;
6+
use Symfony\Component\HttpFoundation\AcceptHeader;
7+
use Symfony\Component\HttpFoundation\Request;
8+
9+
trait AcceptPriorityTrait
10+
{
11+
public function getResponseHandlerPriority(ResponsePayloadEvent $event): ?float
12+
{
13+
return $this->getAcceptPriority($event->request);
14+
}
15+
16+
/**
17+
* @return string[]
18+
*/
19+
abstract protected function getSupportedContentTypes(): array;
20+
21+
protected function getAcceptPriority(
22+
Request $request
23+
): ?float {
24+
$accept = $this->getRequestAcceptHeader($request);
25+
26+
if ($accept) {
27+
foreach ($accept->all() as $a) {
28+
$v = $a->getValue();
29+
if ($v === '*/*') {
30+
return $this->supportsDefaultContentType($request) ? $a->getQuality() : 0;
31+
} elseif (\in_array($v, $this->getSupportedContentTypes())) {
32+
return $a->getQuality();
33+
}
34+
}
35+
return null;
36+
}
37+
38+
return $this->supportsDefaultContentType($request) ? 1 : null;
39+
}
40+
41+
private function supportsDefaultContentType(
42+
Request $request
43+
): bool {
44+
$defaultType = $request->attributes->has('_' . DefaultContentType::class)
45+
? $request->attributes->get('_' . DefaultContentType::class)
46+
: null;
47+
48+
return $defaultType instanceof DefaultContentType
49+
? \in_array($defaultType->value, $this->getSupportedContentTypes())
50+
: true;
51+
}
52+
53+
private function getRequestAcceptHeader(
54+
Request $request
55+
): ?AcceptHeader {
56+
if ($request->attributes->has(AcceptHeader::class)) {
57+
$accept = $request->attributes->get(AcceptHeader::class);
58+
} else {
59+
$accept = $request->headers->has('accept')
60+
? AcceptHeader::fromString($request->headers->get('accept'))
61+
: null;
62+
63+
$request->attributes->set(AcceptHeader::class, $accept);
64+
}
65+
66+
return $accept;
67+
}
68+
}
+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
namespace Pitch\AdrBundle\Responder\Handler;
3+
4+
use JsonException;
5+
use Pitch\AdrBundle\Responder\PrioritisedResponseHandlerInterface;
6+
use Pitch\AdrBundle\Responder\ResponsePayloadEvent;
7+
use Symfony\Component\HttpFoundation\JsonResponse;
8+
use Symfony\Component\HttpFoundation\Response;
9+
10+
class JsonResponder implements PrioritisedResponseHandlerInterface
11+
{
12+
use AcceptPriorityTrait;
13+
14+
public function getSupportedPayloadTypes(): array
15+
{
16+
return [
17+
'array',
18+
'object',
19+
];
20+
}
21+
22+
protected function getSupportedContentTypes(): array
23+
{
24+
return ['application/json'];
25+
}
26+
27+
public function handleResponsePayload(ResponsePayloadEvent $payloadEvent)
28+
{
29+
if (!($payloadEvent->payload instanceof Response)) {
30+
try {
31+
$payloadEvent->payload = new JsonResponse(
32+
\json_encode($payloadEvent->payload, \JSON_THROW_ON_ERROR),
33+
$payloadEvent->httpStatus ?? 200,
34+
$payloadEvent->httpHeaders->all(),
35+
true,
36+
);
37+
$payloadEvent->stopPropagation = true;
38+
} catch (JsonException $e) {
39+
}
40+
}
41+
}
42+
}

test/PitchAdrBundleTest.php

+28
Original file line numberDiff line numberDiff line change
@@ -175,4 +175,32 @@ public function testCustomResponseHandler()
175175

176176
$this->assertEquals(['value' => 'foo'], $event->getControllerResult());
177177
}
178+
179+
public function testDefaultJsonResponse()
180+
{
181+
static::$containerConfigurator = function (LoaderInterface $loader) {
182+
$loader->load(function (ContainerBuilder $containerBuilder) {
183+
$containerBuilder->setParameter('pitch_adr.defaultContentType', null);
184+
});
185+
};
186+
187+
$this->boot();
188+
189+
$event = $this->dispatchViewEvent('foo');
190+
191+
$this->assertTrue($event->hasResponse());
192+
$this->assertEquals('{"value":"foo"}', $event->getResponse()->getContent());
193+
}
194+
195+
public function testNegotiatedJsonResponse()
196+
{
197+
$this->boot();
198+
199+
$request = new Request();
200+
$request->headers->set('accept', 'text/plain, application/json;q=0.5');
201+
$event = $this->dispatchViewEvent('foo', $request);
202+
203+
$this->assertTrue($event->hasResponse());
204+
$this->assertEquals('{"value":"foo"}', $event->getResponse()->getContent());
205+
}
178206
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
namespace Pitch\AdrBundle\Responder\Handler;
3+
4+
use PHPUnit\Framework\TestCase;
5+
use Pitch\AdrBundle\Configuration\DefaultContentType;
6+
use Pitch\AdrBundle\Responder\ResponsePayloadEvent;
7+
use Symfony\Component\HttpFoundation\AcceptHeader;
8+
use Symfony\Component\HttpFoundation\Request;
9+
10+
class AcceptPriorityTraitTest extends TestCase
11+
{
12+
public function provideRequestSettings()
13+
{
14+
return [
15+
'default to 1' => [
16+
1,
17+
],
18+
'matching defaultContentType' => [
19+
1,
20+
['foo/bar', 'foo/baz'],
21+
'foo/baz',
22+
],
23+
'not matching defaultContentType' => [
24+
null,
25+
['foo/bar'],
26+
'foo/baz',
27+
],
28+
'matching accept' => [
29+
0.8,
30+
['foo/bar'],
31+
null,
32+
'foo/baz;q=1,foo/bar;q=0.8',
33+
],
34+
'not matching accept' => [
35+
null,
36+
['foo/bar'],
37+
'foo/bar',
38+
'foo/baz',
39+
],
40+
'accept any matching defaultContentType' => [
41+
0.8,
42+
['foo/bar'],
43+
'foo/bar',
44+
'foo/baz;q=1,foo/bar;q=0.1,*/*;q=0.8',
45+
],
46+
'accept any not matching defaultContentType' => [
47+
0,
48+
['foo/bar'],
49+
'foo/baz',
50+
'foo/baz,foo/bar;q=0.1,*/*;q=0.8',
51+
],
52+
];
53+
}
54+
55+
/**
56+
* @dataProvider provideRequestSettings
57+
*/
58+
public function testGetPriorityFromAcceptQuality(
59+
?float $expectedPriority,
60+
array $supportedContentTypes = [],
61+
?string $defaultContentType = null,
62+
?string $acceptHeader = null
63+
) {
64+
$handler = $this->getMockForTrait(AcceptPriorityTrait::class);
65+
$handler->method('getSupportedContentTypes')->willReturn($supportedContentTypes);
66+
/** @var AcceptPriorityTrait $handler */
67+
68+
$request = new Request();
69+
if ($acceptHeader) {
70+
$request->headers->set('accept', $acceptHeader);
71+
}
72+
if ($defaultContentType) {
73+
$request->attributes->set(
74+
'_' . DefaultContentType::class,
75+
new DefaultContentType($defaultContentType)
76+
);
77+
}
78+
79+
$event = new ResponsePayloadEvent(null, $request);
80+
81+
$this->assertSame($expectedPriority, $handler->getResponseHandlerPriority($event));
82+
}
83+
84+
public function testStoreAccessHeaderOnRequestAttributes()
85+
{
86+
$handler = $this->getMockForTrait(AcceptPriorityTrait::class);
87+
$handler->method('getSupportedContentTypes')->willReturn(['foo/baz']);
88+
/** @var AcceptPriorityTrait $handler */
89+
90+
$request = new Request();
91+
$request->headers->set('accept', 'foo/bar,foo/baz;q=0.2');
92+
$event = new ResponsePayloadEvent(null, $request);
93+
94+
$this->assertEquals(0.2, $handler->getResponseHandlerPriority($event));
95+
96+
/** @var AcceptHeader */
97+
$attr = $request->attributes->get(AcceptHeader::class);
98+
$this->assertInstanceOf(AcceptHeader::class, $attr);
99+
$this->assertEquals(0.2, $attr->get('foo/baz')->getQuality());
100+
101+
$request->attributes->set(AcceptHeader::class, AcceptHeader::fromString('foo/baz;q=0.5'));
102+
103+
$this->assertEquals(0.5, $handler->getResponseHandlerPriority($event));
104+
}
105+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
namespace Pitch\AdrBundle\Responder\Handler;
3+
4+
use PHPUnit\Framework\TestCase;
5+
use Pitch\AdrBundle\Responder\ResponsePayloadEvent;
6+
use stdClass;
7+
use Symfony\Component\HttpFoundation\JsonResponse;
8+
use Symfony\Component\HttpFoundation\Request;
9+
10+
class JsonResponderTest extends TestCase
11+
{
12+
public function testCreateJsonResponse()
13+
{
14+
$event = new ResponsePayloadEvent(['a' => 'b'], new Request());
15+
16+
(new JsonResponder())->handleResponsePayload($event);
17+
18+
$this->assertInstanceOf(JsonResponse::class, $event->payload);
19+
$this->assertEquals('{"a":"b"}', $event->payload->getContent());
20+
}
21+
22+
public function testCatchJsonExceptions()
23+
{
24+
$circular = new stdClass();
25+
$circular->foo = $circular;
26+
27+
$event = new ResponsePayloadEvent($circular, new Request());
28+
29+
(new JsonResponder)->handleResponsePayload($event);
30+
31+
$this->assertSame($circular, $event->payload);
32+
}
33+
}

0 commit comments

Comments
 (0)