Skip to content

Commit

Permalink
feat: add JsonResponder
Browse files Browse the repository at this point in the history
  • Loading branch information
ph-fritsche committed May 28, 2021
1 parent 4cd8dd1 commit 184bea1
Show file tree
Hide file tree
Showing 6 changed files with 279 additions and 0 deletions.
3 changes: 3 additions & 0 deletions src/Resources/config/handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
use Pitch\AdrBundle\DependencyInjection\Compiler\ResponseHandlerPass;
use Pitch\AdrBundle\Responder\Handler\ObjectHandler;
use Pitch\AdrBundle\Responder\Handler\ScalarHandler;
use Pitch\AdrBundle\Responder\Handler\JsonResponder;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $container) {
Expand All @@ -14,5 +15,7 @@
->tag(ResponseHandlerPass::TAG, ['priority' => -1024])
->set(ObjectHandler::class)
->tag(ResponseHandlerPass::TAG, ['priority' => -1024])
->set(JsonResponder::class)
->tag(ResponseHandlerPass::TAG, ['priority' => -8192])
;
};
68 changes: 68 additions & 0 deletions src/Responder/Handler/AcceptPriorityTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php
namespace Pitch\AdrBundle\Responder\Handler;

use Pitch\AdrBundle\Configuration\DefaultContentType;
use Pitch\AdrBundle\Responder\ResponsePayloadEvent;
use Symfony\Component\HttpFoundation\AcceptHeader;
use Symfony\Component\HttpFoundation\Request;

trait AcceptPriorityTrait
{
public function getResponseHandlerPriority(ResponsePayloadEvent $event): ?float
{
return $this->getAcceptPriority($event->request);
}

/**
* @return string[]
*/
abstract protected function getSupportedContentTypes(): array;

protected function getAcceptPriority(
Request $request
): ?float {
$accept = $this->getRequestAcceptHeader($request);

if ($accept) {
foreach ($accept->all() as $a) {
$v = $a->getValue();
if ($v === '*/*') {
return $this->supportsDefaultContentType($request) ? $a->getQuality() : 0;
} elseif (\in_array($v, $this->getSupportedContentTypes())) {
return $a->getQuality();
}
}
return null;
}

return $this->supportsDefaultContentType($request) ? 1 : null;
}

private function supportsDefaultContentType(
Request $request
): bool {
$defaultType = $request->attributes->has('_' . DefaultContentType::class)
? $request->attributes->get('_' . DefaultContentType::class)
: null;

return $defaultType instanceof DefaultContentType
? \in_array($defaultType->value, $this->getSupportedContentTypes())
: true;
}

private function getRequestAcceptHeader(
Request $request
): ?AcceptHeader {
if ($request->attributes->has(AcceptHeader::class)) {
$accept = $request->attributes->get(AcceptHeader::class);
} else {
$accept = $request->headers->has('accept')
? AcceptHeader::fromString($request->headers->get('accept'))
: null;

$request->attributes->set(AcceptHeader::class, $accept);
}

return $accept;
}
}
42 changes: 42 additions & 0 deletions src/Responder/Handler/JsonResponder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php
namespace Pitch\AdrBundle\Responder\Handler;

use JsonException;
use Pitch\AdrBundle\Responder\PrioritisedResponseHandlerInterface;
use Pitch\AdrBundle\Responder\ResponsePayloadEvent;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;

class JsonResponder implements PrioritisedResponseHandlerInterface
{
use AcceptPriorityTrait;

public function getSupportedPayloadTypes(): array
{
return [
'array',
'object',
];
}

protected function getSupportedContentTypes(): array
{
return ['application/json'];
}

public function handleResponsePayload(ResponsePayloadEvent $payloadEvent)
{
if (!($payloadEvent->payload instanceof Response)) {
try {
$payloadEvent->payload = new JsonResponse(
\json_encode($payloadEvent->payload, \JSON_THROW_ON_ERROR),
$payloadEvent->httpStatus ?? 200,
$payloadEvent->httpHeaders->all(),
true,
);
$payloadEvent->stopPropagation = true;
} catch (JsonException $e) {
}
}
}
}
28 changes: 28 additions & 0 deletions test/PitchAdrBundleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -175,4 +175,32 @@ public function testCustomResponseHandler()

$this->assertEquals(['value' => 'foo'], $event->getControllerResult());
}

public function testDefaultJsonResponse()
{
static::$containerConfigurator = function (LoaderInterface $loader) {
$loader->load(function (ContainerBuilder $containerBuilder) {
$containerBuilder->setParameter('pitch_adr.defaultContentType', null);
});
};

$this->boot();

$event = $this->dispatchViewEvent('foo');

$this->assertTrue($event->hasResponse());
$this->assertEquals('{"value":"foo"}', $event->getResponse()->getContent());
}

public function testNegotiatedJsonResponse()
{
$this->boot();

$request = new Request();
$request->headers->set('accept', 'text/plain, application/json;q=0.5');
$event = $this->dispatchViewEvent('foo', $request);

$this->assertTrue($event->hasResponse());
$this->assertEquals('{"value":"foo"}', $event->getResponse()->getContent());
}
}
105 changes: 105 additions & 0 deletions test/Responder/Handler/AcceptPriorityTraitTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php
namespace Pitch\AdrBundle\Responder\Handler;

use PHPUnit\Framework\TestCase;
use Pitch\AdrBundle\Configuration\DefaultContentType;
use Pitch\AdrBundle\Responder\ResponsePayloadEvent;
use Symfony\Component\HttpFoundation\AcceptHeader;
use Symfony\Component\HttpFoundation\Request;

class AcceptPriorityTraitTest extends TestCase
{
public function provideRequestSettings()
{
return [
'default to 1' => [
1,
],
'matching defaultContentType' => [
1,
['foo/bar', 'foo/baz'],
'foo/baz',
],
'not matching defaultContentType' => [
null,
['foo/bar'],
'foo/baz',
],
'matching accept' => [
0.8,
['foo/bar'],
null,
'foo/baz;q=1,foo/bar;q=0.8',
],
'not matching accept' => [
null,
['foo/bar'],
'foo/bar',
'foo/baz',
],
'accept any matching defaultContentType' => [
0.8,
['foo/bar'],
'foo/bar',
'foo/baz;q=1,foo/bar;q=0.1,*/*;q=0.8',
],
'accept any not matching defaultContentType' => [
0,
['foo/bar'],
'foo/baz',
'foo/baz,foo/bar;q=0.1,*/*;q=0.8',
],
];
}

/**
* @dataProvider provideRequestSettings
*/
public function testGetPriorityFromAcceptQuality(
?float $expectedPriority,
array $supportedContentTypes = [],
?string $defaultContentType = null,
?string $acceptHeader = null
) {
$handler = $this->getMockForTrait(AcceptPriorityTrait::class);
$handler->method('getSupportedContentTypes')->willReturn($supportedContentTypes);
/** @var AcceptPriorityTrait $handler */

$request = new Request();
if ($acceptHeader) {
$request->headers->set('accept', $acceptHeader);
}
if ($defaultContentType) {
$request->attributes->set(
'_' . DefaultContentType::class,
new DefaultContentType($defaultContentType)
);
}

$event = new ResponsePayloadEvent(null, $request);

$this->assertSame($expectedPriority, $handler->getResponseHandlerPriority($event));
}

public function testStoreAccessHeaderOnRequestAttributes()
{
$handler = $this->getMockForTrait(AcceptPriorityTrait::class);
$handler->method('getSupportedContentTypes')->willReturn(['foo/baz']);
/** @var AcceptPriorityTrait $handler */

$request = new Request();
$request->headers->set('accept', 'foo/bar,foo/baz;q=0.2');
$event = new ResponsePayloadEvent(null, $request);

$this->assertEquals(0.2, $handler->getResponseHandlerPriority($event));

/** @var AcceptHeader */
$attr = $request->attributes->get(AcceptHeader::class);
$this->assertInstanceOf(AcceptHeader::class, $attr);
$this->assertEquals(0.2, $attr->get('foo/baz')->getQuality());

$request->attributes->set(AcceptHeader::class, AcceptHeader::fromString('foo/baz;q=0.5'));

$this->assertEquals(0.5, $handler->getResponseHandlerPriority($event));
}
}
33 changes: 33 additions & 0 deletions test/Responder/Handler/JsonResponderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php
namespace Pitch\AdrBundle\Responder\Handler;

use PHPUnit\Framework\TestCase;
use Pitch\AdrBundle\Responder\ResponsePayloadEvent;
use stdClass;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;

class JsonResponderTest extends TestCase
{
public function testCreateJsonResponse()
{
$event = new ResponsePayloadEvent(['a' => 'b'], new Request());

(new JsonResponder())->handleResponsePayload($event);

$this->assertInstanceOf(JsonResponse::class, $event->payload);
$this->assertEquals('{"a":"b"}', $event->payload->getContent());
}

public function testCatchJsonExceptions()
{
$circular = new stdClass();
$circular->foo = $circular;

$event = new ResponsePayloadEvent($circular, new Request());

(new JsonResponder)->handleResponsePayload($event);

$this->assertSame($circular, $event->payload);
}
}

0 comments on commit 184bea1

Please sign in to comment.