From 184bea1452e9e853b72f8345861d247257e812a0 Mon Sep 17 00:00:00 2001 From: Philipp Fritsche Date: Fri, 28 May 2021 19:24:20 +0200 Subject: [PATCH] feat: add JsonResponder --- src/Resources/config/handler.php | 3 + src/Responder/Handler/AcceptPriorityTrait.php | 68 ++++++++++++ src/Responder/Handler/JsonResponder.php | 42 +++++++ test/PitchAdrBundleTest.php | 28 +++++ .../Handler/AcceptPriorityTraitTest.php | 105 ++++++++++++++++++ test/Responder/Handler/JsonResponderTest.php | 33 ++++++ 6 files changed, 279 insertions(+) create mode 100644 src/Responder/Handler/AcceptPriorityTrait.php create mode 100644 src/Responder/Handler/JsonResponder.php create mode 100644 test/Responder/Handler/AcceptPriorityTraitTest.php create mode 100644 test/Responder/Handler/JsonResponderTest.php diff --git a/src/Resources/config/handler.php b/src/Resources/config/handler.php index 0cc16db..ff7bdf5 100644 --- a/src/Resources/config/handler.php +++ b/src/Resources/config/handler.php @@ -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) { @@ -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]) ; }; diff --git a/src/Responder/Handler/AcceptPriorityTrait.php b/src/Responder/Handler/AcceptPriorityTrait.php new file mode 100644 index 0000000..4d0b2ef --- /dev/null +++ b/src/Responder/Handler/AcceptPriorityTrait.php @@ -0,0 +1,68 @@ +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; + } +} diff --git a/src/Responder/Handler/JsonResponder.php b/src/Responder/Handler/JsonResponder.php new file mode 100644 index 0000000..3dab9a2 --- /dev/null +++ b/src/Responder/Handler/JsonResponder.php @@ -0,0 +1,42 @@ +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) { + } + } + } +} diff --git a/test/PitchAdrBundleTest.php b/test/PitchAdrBundleTest.php index ce86a31..5862c13 100644 --- a/test/PitchAdrBundleTest.php +++ b/test/PitchAdrBundleTest.php @@ -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()); + } } diff --git a/test/Responder/Handler/AcceptPriorityTraitTest.php b/test/Responder/Handler/AcceptPriorityTraitTest.php new file mode 100644 index 0000000..2b41f51 --- /dev/null +++ b/test/Responder/Handler/AcceptPriorityTraitTest.php @@ -0,0 +1,105 @@ + [ + 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)); + } +} diff --git a/test/Responder/Handler/JsonResponderTest.php b/test/Responder/Handler/JsonResponderTest.php new file mode 100644 index 0000000..53ba8f1 --- /dev/null +++ b/test/Responder/Handler/JsonResponderTest.php @@ -0,0 +1,33 @@ + '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); + } +}