From afce083267f74f2dc248544109f8fb66cbd33707 Mon Sep 17 00:00:00 2001 From: Arthur Mogliev Date: Mon, 25 Mar 2024 01:31:32 +0400 Subject: [PATCH] - allow mixed as consumer parsing result - new attribute for low level transfer --- docs/topics/consuming.md | 11 +- .../Attribute/AnonymousTransferSpec.php | 242 ++++++++++++++++++ .../Factory/AnonymousTransferSpec.php | 39 +++ .../Options/AnonymousTransferSpec.php | 66 +++++ .../PathHandler/Attribute/TransferSpec.php | 38 +-- .../Articus/PathHandler/Consumer/JsonSpec.php | 9 +- spec/Articus/PathHandler/MiddlewareSpec.php | 2 +- .../Attribute/AnonymousTransfer.php | 87 +++++++ .../Attribute/Factory/AnonymousTransfer.php | 39 +++ .../Attribute/Factory/PluginManager.php | 5 + .../Attribute/Options/AnonymousTransfer.php | 76 ++++++ .../Attribute/Options/Transfer.php | 1 - .../PathHandler/Attribute/Transfer.php | 19 +- .../Consumer/ConsumerInterface.php | 4 +- .../Consumer/Factory/PluginManager.php | 1 + src/Articus/PathHandler/Consumer/Internal.php | 2 +- src/Articus/PathHandler/Consumer/Json.php | 7 +- src/Articus/PathHandler/Middleware.php | 10 +- .../Producer/Factory/PluginManager.php | 1 + 19 files changed, 594 insertions(+), 65 deletions(-) create mode 100644 spec/Articus/PathHandler/Attribute/AnonymousTransferSpec.php create mode 100644 spec/Articus/PathHandler/Attribute/Factory/AnonymousTransferSpec.php create mode 100644 spec/Articus/PathHandler/Attribute/Options/AnonymousTransferSpec.php create mode 100644 src/Articus/PathHandler/Attribute/AnonymousTransfer.php create mode 100644 src/Articus/PathHandler/Attribute/Factory/AnonymousTransfer.php create mode 100644 src/Articus/PathHandler/Attribute/Options/AnonymousTransfer.php diff --git a/docs/topics/consuming.md b/docs/topics/consuming.md index c04f4f3..941e024 100644 --- a/docs/topics/consuming.md +++ b/docs/topics/consuming.md @@ -19,6 +19,7 @@ To use consumer for operation in your handler you just need to annotate operatio ```PHP namespace My; +use Articus\PathHandler\Middleware; use Articus\PathHandler\PhpAttribute as PHA; use Psr\Http\Message\ServerRequestInterface; @@ -29,7 +30,7 @@ class Handler #[PHA\Consumer("*/*", "Json")] public function handlePost(ServerRequestInterface $request) { - $data = $request->getParsedBody(); + $data = $request->getAttribute(Middleware::PARSED_BODY_ATTR_NAME)l } } ``` @@ -39,6 +40,7 @@ Each operation method can have several consumers. Just specify media range to de ```PHP namespace My; +use Articus\PathHandler\Middleware; use Articus\PathHandler\PhpAttribute as PHA; use Psr\Http\Message\ServerRequestInterface; @@ -50,7 +52,7 @@ class Handler #[PHA\Consumer("multipart/form-data", "Internal")] public function handlePost(ServerRequestInterface $request) { - $data = $request->getParsedBody(); + $data = $request->getAttribute(Middleware::PARSED_BODY_ATTR_NAME)l } } ``` @@ -60,6 +62,7 @@ If all operations in your handler need same consumer you can just annotate handl ```PHP namespace My; +use Articus\PathHandler\Middleware; use Articus\PathHandler\PhpAttribute as PHA; use Psr\Http\Message\ServerRequestInterface; @@ -70,12 +73,12 @@ class Handler #[PHA\Post()] public function handlePost(ServerRequestInterface $request) { - $data = $request->getParsedBody(); + $data = $request->getAttribute(Middleware::PARSED_BODY_ATTR_NAME)l } #[PHA\Patch()] public function handlePatch(ServerRequestInterface $request) { - $data = $request->getParsedBody(); + $data = $request->getAttribute(Middleware::PARSED_BODY_ATTR_NAME)l } } ``` diff --git a/spec/Articus/PathHandler/Attribute/AnonymousTransferSpec.php b/spec/Articus/PathHandler/Attribute/AnonymousTransferSpec.php new file mode 100644 index 0000000..d38006e --- /dev/null +++ b/spec/Articus/PathHandler/Attribute/AnonymousTransferSpec.php @@ -0,0 +1,242 @@ + 123]; + + $in->getQueryParams()->shouldBeCalledOnce()->willReturn($data); + $in->getAttribute($objectAttr)->shouldBeCalledOnce()->willReturn($object); + $in->withAttribute($objectAttr, $object)->shouldBeCalledOnce()->willReturn($out); + + $dt->transfer($data, Argument::any(), $object, $strategy, $strategy, $validator, $strategy)->shouldBeCalledOnce()->willReturn([]); + + $this->beConstructedWith($dt, $source, $strategy, $validator, $objectAttr, $errorAttr); + $this->__invoke($in)->shouldBe($out); + } + + public function it_transfers_data_from_parsed_body( + DT\Service $dt, + Request $in, + Request $out, + DT\Strategy\StrategyInterface $strategy, + DT\Validator\ValidatorInterface $validator, + stdClass $object + ) + { + $source = PH\Attribute\Transfer::SOURCE_POST; + $objectAttr = 'object'; + $errorAttr = null; + + $data = ['test' => 123]; + + $in->getAttribute(PH\Middleware::PARSED_BODY_ATTR_NAME)->shouldBeCalledOnce()->willReturn($data); + $in->getAttribute($objectAttr)->shouldBeCalledOnce()->willReturn($object); + $in->withAttribute($objectAttr, $object)->shouldBeCalledOnce()->willReturn($out); + + $dt->transfer($data, Argument::any(), $object, $strategy, $strategy, $validator, $strategy)->shouldBeCalledOnce()->willReturn([]); + + $this->beConstructedWith($dt, $source, $strategy, $validator, $objectAttr, $errorAttr); + $this->__invoke($in)->shouldBe($out); + } + + public function it_transfers_data_from_headers( + DT\Service $dt, + Request $in, + Request $out, + DT\Strategy\StrategyInterface $strategy, + DT\Validator\ValidatorInterface $validator, + stdClass $object + ) + { + $source = PH\Attribute\Transfer::SOURCE_HEADER; + $objectAttr = 'object'; + $errorAttr = null; + + $data = ['test1' => [123], 'test2' => [123, 456]]; + + $in->getHeaders()->shouldBeCalledOnce()->willReturn($data); + $in->getAttribute($objectAttr)->shouldBeCalledOnce()->willReturn($object); + $in->withAttribute($objectAttr, $object)->shouldBeCalledOnce()->willReturn($out); + + $dt->transfer($data, Argument::any(), $object, $strategy, $strategy, $validator, $strategy)->shouldBeCalledOnce()->willReturn([]); + + $this->beConstructedWith($dt, $source, $strategy, $validator, $objectAttr, $errorAttr); + $this->__invoke($in)->shouldBe($out); + } + + public function it_transfers_data_from_routing( + DT\Service $dt, + Request $in, + Request $out, + DT\Strategy\StrategyInterface $strategy, + DT\Validator\ValidatorInterface $validator, + RouteResult $routing, + stdClass $object + ) + { + $source = PH\Attribute\Transfer::SOURCE_ROUTE; + $objectAttr = 'object'; + $errorAttr = null; + + $data = ['test' => 123]; + + $routing->getMatchedParams()->shouldBeCalledOnce()->willReturn($data); + + $in->getAttribute(RouteResult::class)->shouldBeCalledOnce()->willReturn($routing); + $in->getAttribute($objectAttr)->shouldBeCalledOnce()->willReturn($object); + $in->withAttribute($objectAttr, $object)->shouldBeCalledOnce()->willReturn($out); + + $dt->transfer($data, Argument::any(), $object, $strategy, $strategy, $validator, $strategy)->shouldBeCalledOnce()->willReturn([]); + + $this->beConstructedWith($dt, $source, $strategy, $validator, $objectAttr, $errorAttr); + $this->__invoke($in)->shouldBe($out); + } + + public function it_throws_on_data_transfer_from_routing_if_there_is_no_routing_result( + DT\Service $dt, + Request $in, + DT\Strategy\StrategyInterface $strategy, + DT\Validator\ValidatorInterface $validator, + stdClass $object + ) + { + $source = PH\Attribute\Transfer::SOURCE_ROUTE; + $objectAttr = 'object'; + $errorAttr = null; + + $data = ['test' => 123]; + + $in->getAttribute(RouteResult::class)->shouldBeCalledOnce()->willReturn(null); + + $dt->transfer($data, Argument::any(), $object, $strategy, $strategy, $validator, $strategy)->shouldNotBeCalled(); + + $this->beConstructedWith($dt, $source, $strategy, $validator, $objectAttr, $errorAttr); + $this->shouldThrow(LogicException::class)->during('__invoke', [$in]); + } + + public function it_saves_object_to_attribute_with_custom_name( + DT\Service $dt, + Request $in, + Request $out, + DT\Strategy\StrategyInterface $strategy, + DT\Validator\ValidatorInterface $validator, + stdClass $object + ) + { + $source = PH\Attribute\Transfer::SOURCE_POST; + $objectAttr = 'test'; + $errorAttr = null; + + $data = ['test' => 123]; + + $in->getAttribute(PH\Middleware::PARSED_BODY_ATTR_NAME)->shouldBeCalledOnce()->willReturn($data); + $in->getAttribute($objectAttr)->shouldBeCalledOnce()->willReturn($object); + $in->withAttribute($objectAttr, $object)->shouldBeCalledOnce()->willReturn($out); + + $dt->transfer($data, Argument::any(), $object, $strategy, $strategy, $validator, $strategy)->shouldBeCalledOnce()->willReturn([]); + + $this->beConstructedWith($dt, $source, $strategy, $validator, $objectAttr, $errorAttr); + $this->__invoke($in)->shouldBe($out); + } + + public function it_transfers_to_null_if_object_attribute_is_empty( + DT\Service $dt, + Request $in, + Request $out, + DT\Strategy\StrategyInterface $strategy, + DT\Validator\ValidatorInterface $validator + ) + { + $source = PH\Attribute\Transfer::SOURCE_POST; + $objectAttr = 'object'; + $errorAttr = null; + + $data = ['test' => 123]; + $object = null; + + $in->getAttribute(PH\Middleware::PARSED_BODY_ATTR_NAME)->shouldBeCalledOnce()->willReturn($data); + $in->getAttribute($objectAttr)->shouldBeCalledOnce()->willReturn(null); + $in->withAttribute($objectAttr, $object)->shouldBeCalledOnce()->willReturn($out); + + $dt->transfer($data, Argument::any(), $object, $strategy, $strategy, $validator, $strategy)->shouldBeCalledOnce()->willReturn([]); + + $this->beConstructedWith($dt, $source, $strategy, $validator, $objectAttr, $errorAttr); + $this->__invoke($in)->shouldBe($out); + } + + public function it_throws_on_transfer_error_if_error_attribute_name_is_not_set( + DT\Service $dt, + Request $in, + DT\Strategy\StrategyInterface $strategy, + DT\Validator\ValidatorInterface $validator, + stdClass $object + ) + { + $source = PH\Attribute\Transfer::SOURCE_POST; + $objectAttr = 'object'; + $errorAttr = null; + + $data = ['test' => 123]; + $error = ['wrong' => 456]; + $exception = new PH\Exception\UnprocessableEntity($error); + + $in->getAttribute(PH\Middleware::PARSED_BODY_ATTR_NAME)->shouldBeCalledOnce()->willReturn($data); + $in->getAttribute($objectAttr)->shouldBeCalledOnce()->willReturn($object); + + $dt->transfer($data, Argument::any(), $object, $strategy, $strategy, $validator, $strategy)->shouldBeCalledOnce()->willReturn($error); + + $this->beConstructedWith($dt, $source, $strategy, $validator, $objectAttr, $errorAttr); + $this->shouldThrow($exception)->during('__invoke', [$in]); + } + + public function it_saves_transfer_error_to_error_attribute_if_its_name_is_set( + DT\Service $dt, + Request $in, + Request $out, + DT\Strategy\StrategyInterface $strategy, + DT\Validator\ValidatorInterface $validator, + stdClass $object + ) + { + $source = PH\Attribute\Transfer::SOURCE_POST; + $objectAttr = 'object'; + $errorAttr = 'test'; + + $data = ['test' => 123]; + $error = ['wrong' => 456]; + + $in->getAttribute(PH\Middleware::PARSED_BODY_ATTR_NAME)->shouldBeCalledOnce()->willReturn($data); + $in->getAttribute($objectAttr)->shouldBeCalledOnce()->willReturn($object); + $in->withAttribute($errorAttr, $error)->shouldBeCalledOnce()->willReturn($out); + + $dt->transfer($data, Argument::any(), $object, $strategy, $strategy, $validator, $strategy)->shouldBeCalledOnce()->willReturn($error); + + $this->beConstructedWith($dt, $source, $strategy, $validator, $objectAttr, $errorAttr); + $this->__invoke($in)->shouldBe($out); + } +} diff --git a/spec/Articus/PathHandler/Attribute/Factory/AnonymousTransferSpec.php b/spec/Articus/PathHandler/Attribute/Factory/AnonymousTransferSpec.php new file mode 100644 index 0000000..d7a407c --- /dev/null +++ b/spec/Articus/PathHandler/Attribute/Factory/AnonymousTransferSpec.php @@ -0,0 +1,39 @@ + 111]; + $validatorName = 'test_validator_name'; + $validatorOptions = ['test_validator_option' => 222]; + $options = [ + 'strategy' => [$strategyName, $strategyOptions], + 'validator' => [$validatorName, $validatorOptions], + ]; + $container->get(DT\Service::class)->shouldBeCalledOnce()->willReturn($dt); + $container->get(DT\Options::DEFAULT_STRATEGY_PLUGIN_MANAGER)->shouldBeCalledOnce()->willReturn($strategyManager); + $container->get(DT\Options::DEFAULT_VALIDATOR_PLUGIN_MANAGER)->shouldBeCalledOnce()->willReturn($validatorManager); + $strategyManager->__invoke($strategyName, $strategyOptions)->shouldBeCalledOnce()->willReturn($strategy); + $validatorManager->__invoke($validatorName, $validatorOptions)->shouldBeCalledOnce()->willReturn($validator); + $attribute = $this->__invoke($container, 'test', $options); + $attribute->shouldHaveProperty('strategy', $strategy); + $attribute->shouldHaveProperty('validator', $validator); + } +} diff --git a/spec/Articus/PathHandler/Attribute/Options/AnonymousTransferSpec.php b/spec/Articus/PathHandler/Attribute/Options/AnonymousTransferSpec.php new file mode 100644 index 0000000..e87b346 --- /dev/null +++ b/spec/Articus/PathHandler/Attribute/Options/AnonymousTransferSpec.php @@ -0,0 +1,66 @@ + 111]]; + $validator = ['test_validator_name', ['bbb' => 222]]; + $objectAttr = 'test_object_attr'; + $errorAttr = 'test_error_attr'; + $options = [ + 'source' => $source, + 'strategy' => $strategy, + 'validator' => $validator, + 'objectAttr' => $objectAttr, + 'errorAttr' => $errorAttr, + ]; + $this->beConstructedWith($options); + $this->shouldhaveProperty('source', $source); + $this->shouldhaveProperty('strategy', $strategy); + $this->shouldhaveProperty('validator', $validator); + $this->shouldhaveProperty('objectAttr', $objectAttr); + $this->shouldhaveProperty('errorAttr', $errorAttr); + } + + public function it_constructs_with_snake_case_option_names() + { + $source = PH\Attribute\Transfer::SOURCE_ROUTE; + $strategy = ['test_strategy_name', ['aaa' => 111]]; + $validator = ['test_validator_name', ['bbb' => 222]]; + $objectAttr = 'test_object_attr'; + $errorAttr = 'test_error_attr'; + $options = [ + 'source' => $source, + 'strategy' => $strategy, + 'validator' => $validator, + 'object_attr' => $objectAttr, + 'error_attr' => $errorAttr, + ]; + $this->beConstructedWith($options); + $this->shouldhaveProperty('source', $source); + $this->shouldhaveProperty('strategy', $strategy); + $this->shouldhaveProperty('validator', $validator); + $this->shouldhaveProperty('objectAttr', $objectAttr); + $this->shouldhaveProperty('errorAttr', $errorAttr); + } + + public function it_throws_on_unknown_source() + { + $options = [ + 'source' => 'unknown_source', + ]; + $exception = new LogicException('Value "unknown_source" for option "source" is not supported.'); + + $this->beConstructedWith($options); + $this->shouldThrow($exception)->duringInstantiation(); + } +} diff --git a/spec/Articus/PathHandler/Attribute/TransferSpec.php b/spec/Articus/PathHandler/Attribute/TransferSpec.php index 58adb41..c5e2dcf 100644 --- a/spec/Articus/PathHandler/Attribute/TransferSpec.php +++ b/spec/Articus/PathHandler/Attribute/TransferSpec.php @@ -46,7 +46,7 @@ public function it_transfers_data_from_parsed_body(DTService $dt, Request $in, R $data = ['test' => 123]; - $in->getParsedBody()->shouldBeCalledOnce()->willReturn($data); + $in->getAttribute(PH\Middleware::PARSED_BODY_ATTR_NAME)->shouldBeCalledOnce()->willReturn($data); $in->getAttribute($objectAttr)->shouldBeCalledOnce()->willReturn($object); $in->withAttribute($objectAttr, $object)->shouldBeCalledOnce()->willReturn($out); @@ -66,13 +66,12 @@ public function it_transfers_data_from_headers(DTService $dt, Request $in, Reque $errorAttr = null; $data = ['test1' => [123], 'test2' => [123, 456]]; - $transferData = ['test1' => 123, 'test2' => [123, 456]]; $in->getHeaders()->shouldBeCalledOnce()->willReturn($data); $in->getAttribute($objectAttr)->shouldBeCalledOnce()->willReturn($object); $in->withAttribute($objectAttr, $object)->shouldBeCalledOnce()->willReturn($out); - $dt->transferToTypedData($transferData, $object, $subset)->shouldBeCalledOnce()->willReturn([]); + $dt->transferToTypedData($data, $object, $subset)->shouldBeCalledOnce()->willReturn([]); $this->beConstructedWith($dt, $source, $type, $subset, $objectAttr, $instanciator, $instanciatorArgAttrs, $errorAttr); $this->__invoke($in)->shouldBe($out); @@ -120,27 +119,6 @@ public function it_throws_on_data_transfer_from_routing_if_there_is_no_routing_r $this->shouldThrow(LogicException::class)->during('__invoke', [$in]); } - public function it_transfers_data_from_attributes(DTService $dt, Request $in, Request $out, Invokable $instanciator, stdClass $object) - { - $source = PH\Attribute\Transfer::SOURCE_ATTRIBUTE; - $type = stdClass::class; - $subset = ''; - $objectAttr = 'object'; - $instanciatorArgAttrs = []; - $errorAttr = null; - - $data = ['test' => 123]; - - $in->getAttributes()->shouldBeCalledOnce()->willReturn($data); - $in->getAttribute($objectAttr)->shouldBeCalledOnce()->willReturn($object); - $in->withAttribute($objectAttr, $object)->shouldBeCalledOnce()->willReturn($out); - - $dt->transferToTypedData($data, $object, $subset)->shouldBeCalledOnce()->willReturn([]); - - $this->beConstructedWith($dt, $source, $type, $subset, $objectAttr, $instanciator, $instanciatorArgAttrs, $errorAttr); - $this->__invoke($in)->shouldBe($out); - } - public function it_transfers_custom_subset_of_data(DTService $dt, Request $in, Request $out, Invokable $instanciator, stdClass $object) { $source = PH\Attribute\Transfer::SOURCE_POST; @@ -152,7 +130,7 @@ public function it_transfers_custom_subset_of_data(DTService $dt, Request $in, R $data = ['test' => 123]; - $in->getParsedBody()->shouldBeCalledOnce()->willReturn($data); + $in->getAttribute(PH\Middleware::PARSED_BODY_ATTR_NAME)->shouldBeCalledOnce()->willReturn($data); $in->getAttribute($objectAttr)->shouldBeCalledOnce()->willReturn($object); $in->withAttribute($objectAttr, $object)->shouldBeCalledOnce()->willReturn($out); @@ -173,7 +151,7 @@ public function it_saves_object_to_attribute_with_custom_name(DTService $dt, Req $data = ['test' => 123]; - $in->getParsedBody()->shouldBeCalledOnce()->willReturn($data); + $in->getAttribute(PH\Middleware::PARSED_BODY_ATTR_NAME)->shouldBeCalledOnce()->willReturn($data); $in->getAttribute($objectAttr)->shouldBeCalledOnce()->willReturn($object); $in->withAttribute($objectAttr, $object)->shouldBeCalledOnce()->willReturn($out); @@ -194,7 +172,7 @@ public function it_creates_object_with_instanciator_passing_request_if_object_at $data = ['test' => 123]; - $in->getParsedBody()->shouldBeCalledOnce()->willReturn($data); + $in->getAttribute(PH\Middleware::PARSED_BODY_ATTR_NAME)->shouldBeCalledOnce()->willReturn($data); $in->getAttribute($objectAttr)->shouldBeCalledOnce()->willReturn(null); $instanciator->__invoke($type, $in)->shouldBeCalledOnce()->willReturn($object); $in->withAttribute($objectAttr, $object)->shouldBeCalledOnce()->willReturn($out); @@ -217,7 +195,7 @@ public function it_creates_object_with_instanciator_passing_specified_attributes $data = ['test' => 123]; $instanciatorArgAttrValues = ['value1', 'value2']; - $in->getParsedBody()->shouldBeCalledOnce()->willReturn($data); + $in->getAttribute(PH\Middleware::PARSED_BODY_ATTR_NAME)->shouldBeCalledOnce()->willReturn($data); $in->getAttribute($objectAttr)->shouldBeCalledOnce()->willReturn(null); $in->getAttribute($instanciatorArgAttrs[0])->shouldBeCalledOnce()->willReturn($instanciatorArgAttrValues[0]); $in->getAttribute($instanciatorArgAttrs[1])->shouldBeCalledOnce()->willReturn($instanciatorArgAttrValues[1]); @@ -243,7 +221,7 @@ public function it_throws_on_transfer_error_if_error_attribute_name_is_not_set(D $error = ['wrong' => 456]; $exception = new PH\Exception\UnprocessableEntity($error); - $in->getParsedBody()->shouldBeCalledOnce()->willReturn($data); + $in->getAttribute(PH\Middleware::PARSED_BODY_ATTR_NAME)->shouldBeCalledOnce()->willReturn($data); $in->getAttribute($objectAttr)->shouldBeCalledOnce()->willReturn($object); $dt->transferToTypedData($data, $object, $subset)->shouldBeCalledOnce()->willReturn($error); @@ -264,7 +242,7 @@ public function it_saves_transfer_error_to_error_attribute_if_its_name_is_set(DT $data = ['test' => 123]; $error = ['wrong' => 456]; - $in->getParsedBody()->shouldBeCalledOnce()->willReturn($data); + $in->getAttribute(PH\Middleware::PARSED_BODY_ATTR_NAME)->shouldBeCalledOnce()->willReturn($data); $in->getAttribute($objectAttr)->shouldBeCalledOnce()->willReturn($object); $in->withAttribute($errorAttr, $error)->shouldBeCalledOnce()->willReturn($out); diff --git a/spec/Articus/PathHandler/Consumer/JsonSpec.php b/spec/Articus/PathHandler/Consumer/JsonSpec.php index 33aae28..6ae5da7 100644 --- a/spec/Articus/PathHandler/Consumer/JsonSpec.php +++ b/spec/Articus/PathHandler/Consumer/JsonSpec.php @@ -25,25 +25,28 @@ public function it_parses_valid_json_null_from_body(StreamInterface $body) public function it_throws_on_valid_json_int_in_body(StreamInterface $body) { $this->beConstructedWith(JSON_OBJECT_AS_ARRAY, 512); + $data = 123; $json = '123'; $body->getContents()->shouldBeCalledOnce()->willReturn($json); - $this->shouldThrow(PH\Exception\UnprocessableEntity::class)->during('parse', [$body, null, 'mime/test', []]); + $this->parse($body, null, 'mime/test', [])->shouldBe($data); } public function it_throws_on_valid_json_float_in_body(StreamInterface $body) { $this->beConstructedWith(JSON_OBJECT_AS_ARRAY, 512); + $data = 123.456; $json = '123.456'; $body->getContents()->shouldBeCalledOnce()->willReturn($json); - $this->shouldThrow(PH\Exception\UnprocessableEntity::class)->during('parse', [$body, null, 'mime/test', []]); + $this->parse($body, null, 'mime/test', [])->shouldBe($data); } public function it_throws_on_valid_json_string_in_body(StreamInterface $body) { $this->beConstructedWith(JSON_OBJECT_AS_ARRAY, 512); + $data = 'qwer'; $json = '"qwer"'; $body->getContents()->shouldBeCalledOnce()->willReturn($json); - $this->shouldThrow(PH\Exception\UnprocessableEntity::class)->during('parse', [$body, null, 'mime/test', []]); + $this->parse($body, null, 'mime/test', [])->shouldBe($data); } public function it_parses_valid_json_array_from_body(StreamInterface $body) diff --git a/spec/Articus/PathHandler/MiddlewareSpec.php b/spec/Articus/PathHandler/MiddlewareSpec.php index 3e8f024..6628293 100644 --- a/spec/Articus/PathHandler/MiddlewareSpec.php +++ b/spec/Articus/PathHandler/MiddlewareSpec.php @@ -875,7 +875,7 @@ public function it_parses_body_with_first_consumer_that_matches_content_type( $request->getHeaderLine('Content-Type')->shouldBeCalledOnce()->willReturn($consumerMetadata[1][0]); $request->getBody()->shouldBeCalledOnce()->willReturn($requestBody); $request->getParsedBody()->shouldBeCalledOnce()->willReturn($requestData); - $request->withParsedBody($consumerData)->shouldBeCalledOnce()->willReturn($request); + $request->withAttribute(PH\Middleware::PARSED_BODY_ATTR_NAME, $consumerData)->shouldBeCalledOnce()->willReturn($request); $consumer->parse($requestBody, $requestData, $consumerMetadata[1][0], [])->shouldBeCalledOnce()->willReturn($consumerData); diff --git a/src/Articus/PathHandler/Attribute/AnonymousTransfer.php b/src/Articus/PathHandler/Attribute/AnonymousTransfer.php new file mode 100644 index 0000000..ae33e5f --- /dev/null +++ b/src/Articus/PathHandler/Attribute/AnonymousTransfer.php @@ -0,0 +1,87 @@ +noopExtractor = new class () implements ExtractorInterface + { + public function extract($from) + { + return $from; + } + }; + } + + /** + * @inheritdoc + * @throws Exception\UnprocessableEntity + */ + public function __invoke(Request $request): Request + { + $data = $this->getData($request); + $object = $request->getAttribute($this->objectAttr); + $error = $this->dtService->transfer($data, $this->noopExtractor, $object, $this->strategy, $this->strategy, $this->validator, $this->strategy); + if (empty($error)) + { + $request = $request->withAttribute($this->objectAttr, $object); + } + elseif (empty($this->errorAttr)) + { + throw new Exception\UnprocessableEntity($error); + } + else + { + $request = $request->withAttribute($this->errorAttr, $error); + } + + return $request; + } + + protected function getData(Request $request): mixed + { + return match($this->source) + { + Transfer::SOURCE_GET => $request->getQueryParams(), + Transfer::SOURCE_POST => $request->getAttribute(Middleware::PARSED_BODY_ATTR_NAME), + Transfer::SOURCE_ROUTE => $this->getRouteData($request), + Transfer::SOURCE_HEADER => $request->getHeaders(), + }; + } + + protected function getRouteData(Request $request): array + { + $routeResult = $request->getAttribute(RouteResult::class); + if (!($routeResult instanceof RouteResult)) + { + throw new LogicException('Failed to find routing result.'); + } + return $routeResult->getMatchedParams(); + } +} diff --git a/src/Articus/PathHandler/Attribute/Factory/AnonymousTransfer.php b/src/Articus/PathHandler/Attribute/Factory/AnonymousTransfer.php new file mode 100644 index 0000000..c4817cf --- /dev/null +++ b/src/Articus/PathHandler/Attribute/Factory/AnonymousTransfer.php @@ -0,0 +1,39 @@ +strategy); + $validator = self::getValidatorManager($container)(...$parsedOptions->validator); + $result = new Attribute\AnonymousTransfer( + $container->get(DT\Service::class), + $parsedOptions->source, + $strategy, + $validator, + $parsedOptions->objectAttr, + $parsedOptions->errorAttr + ); + return $result; + } + + protected static function getStrategyManager(ContainerInterface $container): PluginManagerInterface + { + return $container->get(DT\Options::DEFAULT_STRATEGY_PLUGIN_MANAGER); + } + + protected static function getValidatorManager(ContainerInterface $container): PluginManagerInterface + { + return $container->get(DT\Options::DEFAULT_VALIDATOR_PLUGIN_MANAGER); + } +} diff --git a/src/Articus/PathHandler/Attribute/Factory/PluginManager.php b/src/Articus/PathHandler/Attribute/Factory/PluginManager.php index d43aba4..699a08a 100644 --- a/src/Articus/PathHandler/Attribute/Factory/PluginManager.php +++ b/src/Articus/PathHandler/Attribute/Factory/PluginManager.php @@ -7,6 +7,7 @@ use Articus\PathHandler\RouteInjectionFactory; use Articus\PluginManager as PM; use Psr\Container\ContainerInterface; +use function array_merge_recursive; class PluginManager extends PM\Factory\Simple { @@ -22,6 +23,7 @@ protected function getServiceConfig(ContainerInterface $container): array Attribute\IdentifiableValueLoad::class => IdentifiableValueLoad::class, Attribute\IdentifiableValueListLoad::class => IdentifiableValueListLoad::class, Attribute\Transfer::class => Transfer::class, + Attribute\AnonymousTransfer::class => AnonymousTransfer::class, ], 'aliases' => [ 'IdentifiableValueLoad' => Attribute\IdentifiableValueLoad::class, @@ -34,11 +36,14 @@ protected function getServiceConfig(ContainerInterface $container): array 'loadByIds' => Attribute\IdentifiableValueListLoad::class, 'Transfer' => Attribute\Transfer::class, 'transfer' => Attribute\Transfer::class, + 'TransferAnon' => Attribute\AnonymousTransfer::class, + 'transferAnon' => Attribute\AnonymousTransfer::class, ], 'shares' => [ Attribute\IdentifiableValueLoad::class => true, Attribute\IdentifiableValueListLoad::class => true, Attribute\Transfer::class => true, + Attribute\AnonymousTransfer::class => true, ], ]; diff --git a/src/Articus/PathHandler/Attribute/Options/AnonymousTransfer.php b/src/Articus/PathHandler/Attribute/Options/AnonymousTransfer.php new file mode 100644 index 0000000..1b410b4 --- /dev/null +++ b/src/Articus/PathHandler/Attribute/Options/AnonymousTransfer.php @@ -0,0 +1,76 @@ + $value) + { + switch ($key) + { + case 'source': + switch ($value) + { + case Attribute\Transfer::SOURCE_GET: + case Attribute\Transfer::SOURCE_POST: + case Attribute\Transfer::SOURCE_ROUTE: + case Attribute\Transfer::SOURCE_HEADER: + $this->source = $value; + break; + default: + throw new LogicException(sprintf('Value "%s" for option "source" is not supported.', $value)); + } + break; + case 'strategy': + $this->strategy = $value; + break; + case 'validator': + $this->validator = $value; + break; + case 'objectAttr': + case 'object_attr': + $this->objectAttr = $value; + break; + case 'errorAttr': + case 'error_attr': + $this->errorAttr = $value; + break; + } + } + } +} diff --git a/src/Articus/PathHandler/Attribute/Options/Transfer.php b/src/Articus/PathHandler/Attribute/Options/Transfer.php index 5422a20..add575a 100644 --- a/src/Articus/PathHandler/Attribute/Options/Transfer.php +++ b/src/Articus/PathHandler/Attribute/Options/Transfer.php @@ -65,7 +65,6 @@ public function __construct(iterable $options) case Attribute\Transfer::SOURCE_POST: case Attribute\Transfer::SOURCE_ROUTE: case Attribute\Transfer::SOURCE_HEADER: - case Attribute\Transfer::SOURCE_ATTRIBUTE: $this->source = $value; break; default: diff --git a/src/Articus/PathHandler/Attribute/Transfer.php b/src/Articus/PathHandler/Attribute/Transfer.php index e217708..3c1b9f3 100644 --- a/src/Articus/PathHandler/Attribute/Transfer.php +++ b/src/Articus/PathHandler/Attribute/Transfer.php @@ -5,10 +5,10 @@ use Articus\DataTransfer\Service as DTService; use Articus\PathHandler\Exception; +use Articus\PathHandler\Middleware; use LogicException; use Mezzio\Router\RouteResult; use Psr\Http\Message\ServerRequestInterface as Request; -use function count; /** * Simple attribute that transfer data from specified source to newly created or existing object. @@ -21,8 +21,6 @@ class Transfer implements AttributeInterface public const SOURCE_POST = 'post'; public const SOURCE_ROUTE = 'route'; public const SOURCE_HEADER = 'header'; - public const SOURCE_ATTRIBUTE = 'attribute'; - /** * @var Instanciator */ @@ -88,10 +86,9 @@ protected function getData(Request $request): mixed return match($this->source) { self::SOURCE_GET => $request->getQueryParams(), - self::SOURCE_POST => $request->getParsedBody(), + self::SOURCE_POST => $request->getAttribute(Middleware::PARSED_BODY_ATTR_NAME), self::SOURCE_ROUTE => $this->getRouteData($request), - self::SOURCE_HEADER => $this->getHeaderData($request), - self::SOURCE_ATTRIBUTE => $request->getAttributes(), + self::SOURCE_HEADER => $request->getHeaders(), }; } @@ -105,16 +102,6 @@ protected function getRouteData(Request $request): array return $routeResult->getMatchedParams(); } - protected function getHeaderData(Request $request): array - { - $data = []; - foreach ($request->getHeaders() as $name => $values) - { - $data[$name] = (count($values) === 1) ? $values[0] : $values; - } - return $data; - } - protected function getObject(Request $request): object { $result = $request->getAttribute($this->objectAttr); diff --git a/src/Articus/PathHandler/Consumer/ConsumerInterface.php b/src/Articus/PathHandler/Consumer/ConsumerInterface.php index 1af7ae1..7ffe638 100644 --- a/src/Articus/PathHandler/Consumer/ConsumerInterface.php +++ b/src/Articus/PathHandler/Consumer/ConsumerInterface.php @@ -16,7 +16,7 @@ interface ConsumerInterface * @param null|array|object $preParsedBody content of the request body that was parsed before the consumer (for some content types it is done internally) * @param string $mediaType media type supplied in Content-Type header of the request * @param array $parameters parameters supplied in Content-Type header of the request - * @return null|array|object parsed content of the request body + * @return mixed parsed content of the request body */ - public function parse(StreamInterface $body, null|array|object $preParsedBody, string $mediaType, array $parameters): null|array|object; + public function parse(StreamInterface $body, null|array|object $preParsedBody, string $mediaType, array $parameters): mixed; } \ No newline at end of file diff --git a/src/Articus/PathHandler/Consumer/Factory/PluginManager.php b/src/Articus/PathHandler/Consumer/Factory/PluginManager.php index 26e8a26..21e1a77 100644 --- a/src/Articus/PathHandler/Consumer/Factory/PluginManager.php +++ b/src/Articus/PathHandler/Consumer/Factory/PluginManager.php @@ -7,6 +7,7 @@ use Articus\PathHandler\RouteInjectionFactory; use Articus\PluginManager as PM; use Psr\Container\ContainerInterface; +use function array_merge_recursive; class PluginManager extends PM\Factory\Simple { diff --git a/src/Articus/PathHandler/Consumer/Internal.php b/src/Articus/PathHandler/Consumer/Internal.php index bb294c8..ad21d17 100644 --- a/src/Articus/PathHandler/Consumer/Internal.php +++ b/src/Articus/PathHandler/Consumer/Internal.php @@ -13,7 +13,7 @@ class Internal implements ConsumerInterface /** * @inheritdoc */ - public function parse(StreamInterface $body, null|array|object $preParsedBody, string $mediaType, array $parameters): null|array|object + public function parse(StreamInterface $body, null|array|object $preParsedBody, string $mediaType, array $parameters): mixed { return $preParsedBody; } diff --git a/src/Articus/PathHandler/Consumer/Json.php b/src/Articus/PathHandler/Consumer/Json.php index 23fb8de..38372d9 100644 --- a/src/Articus/PathHandler/Consumer/Json.php +++ b/src/Articus/PathHandler/Consumer/Json.php @@ -28,19 +28,14 @@ public function __construct( /** * @inheritdoc * @throws Exception\BadRequest - * @throws Exception\UnprocessableEntity */ - public function parse(StreamInterface $body, null|array|object $preParsedBody, string $mediaType, array $parameters): null|array|object + public function parse(StreamInterface $body, null|array|object $preParsedBody, string $mediaType, array $parameters): mixed { $result = json_decode($body->getContents(), depth: $this->depth, flags: $this->decodeFlags); if (($result === null) && (json_last_error() !== JSON_ERROR_NONE)) { throw new Exception\BadRequest('Malformed JSON: failed to decode'); } - if (!(($result === null) || is_array($result) || is_object($result))) - { - throw new Exception\UnprocessableEntity(DTException\InvalidData::DEFAULT_VIOLATION); - } return $result; } } diff --git a/src/Articus/PathHandler/Middleware.php b/src/Articus/PathHandler/Middleware.php index 9db6331..2c1b382 100644 --- a/src/Articus/PathHandler/Middleware.php +++ b/src/Articus/PathHandler/Middleware.php @@ -17,6 +17,13 @@ class Middleware implements MiddlewareInterface, RequestHandlerInterface { + /** + * Attribute name for parsed body so consumers may return data other than null, object or array. + * Workaround for PSR-7 restriction on \Psr\Http\Message\ServerRequestInterface::getParsedBody return type + * that is enforced by most PSR-7 implementations. + */ + public const PARSED_BODY_ATTR_NAME = 'Articus\PathHandler\ParsedBody'; + /** * @param string $handlerName * @param MetadataProviderInterface $metadataProvider @@ -114,7 +121,8 @@ public function handle(Request $request): Response { throw new LogicException(sprintf('Invalid consumer %s.', $name)); } - $request = $request->withParsedBody( + $request = $request->withAttribute( + self::PARSED_BODY_ATTR_NAME, $consumer->parse( $request->getBody(), $request->getParsedBody(), diff --git a/src/Articus/PathHandler/Producer/Factory/PluginManager.php b/src/Articus/PathHandler/Producer/Factory/PluginManager.php index 268b11a..7bd4cc6 100644 --- a/src/Articus/PathHandler/Producer/Factory/PluginManager.php +++ b/src/Articus/PathHandler/Producer/Factory/PluginManager.php @@ -7,6 +7,7 @@ use Articus\PathHandler\RouteInjectionFactory; use Articus\PluginManager as PM; use Psr\Container\ContainerInterface; +use function array_merge_recursive; class PluginManager extends PM\Factory\Simple {