Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 02d9428

Browse files
committed
[API] Denormalizer and Normalizer for command-based requests
1 parent 3b3e384 commit 02d9428

8 files changed

Lines changed: 273 additions & 4 deletions

File tree

features/account/registering_validation.feature

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ Feature: Account registration
1111
Scenario: Trying to register a new account with email that has been already used
1212
Given there is a user "[email protected]" identified by "heisenberg"
1313
When I want to register a new account
14+
And I specify the first name as "Saul"
15+
And I specify the last name as "Goodman"
1416
And I specify the email as "[email protected]"
1517
And I try to register this account
1618
Then I should be notified that the email is already used

src/Sylius/Behat/Context/Api/Shop/RegistrationContext.php

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,12 +158,9 @@ public function iShouldBeNotifiedThatTheFirstNameIsRequired(): void
158158
*/
159159
public function iShouldBeNotifiedThatFieldHaveToBeProvided(string ...$fields): void
160160
{
161+
$fields = $this->convertElementsToCamelCase($fields);
161162
$content = $this->getResponseContent();
162163

163-
foreach ($fields as $key => $field) {
164-
$fields[$key] = lcfirst(str_replace(" ", "", ucwords($field)));
165-
}
166-
167164
Assert::same(
168165
$content['message'],
169166
'Request does not have the following required fields specified: ' . implode(', ', $fields) . '.'
@@ -242,4 +239,13 @@ private function getResponseContent(): array
242239
{
243240
return json_decode($this->client->getResponse()->getContent(), true);
244241
}
242+
243+
private function convertElementsToCamelCase(array $fields): array
244+
{
245+
foreach ($fields as $key => $field) {
246+
$fields[$key] = lcfirst(str_replace(" ", "", ucwords($field)));
247+
}
248+
249+
return $fields;
250+
}
245251
}

src/Sylius/Bundle/ApiBundle/Resources/config/app/config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ api_platform:
2727
SM\SMException: 422
2828
Sylius\Bundle\ApiBundle\Exception\CannotRemoveCurrentlyLoggedInUser: 422
2929
Sylius\Bundle\ApiBundle\Exception\ShippingMethodCannotBeRemoved: 422
30+
Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException: 400
3031
collection:
3132
pagination:
3233
client_items_per_page: true

src/Sylius/Bundle/ApiBundle/Resources/config/services/serializers.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@
2323
<tag name="serializer.normalizer" priority="64" />
2424
</service>
2525

26+
<service id="Sylius\Bundle\ApiBundle\Serializer\CommandOperationNormalizer">
27+
<argument type="service" id="serializer.normalizer.object" />
28+
<tag name="serializer.normalizer" priority="64" />
29+
</service>
30+
31+
<service id="Sylius\Bundle\ApiBundle\Serializer\CommandOperationDenormalizer">
32+
<argument type="service" id="serializer.normalizer.object" />
33+
<tag name="serializer.normalizer" priority="64" />
34+
</service>
35+
2636
<service id="Sylius\Bundle\ApiBundle\Serializer\ProductVariantNormalizer">
2737
<argument type="service" id="sylius.calculator.product_variant_price" />
2838
<argument type="service" id="sylius.context.channel" />
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sylius\Bundle\ApiBundle\Serializer;
6+
7+
use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
8+
use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface;
9+
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
10+
11+
final class CommandOperationDenormalizer implements ContextAwareDenormalizerInterface
12+
{
13+
/** @var DenormalizerInterface */
14+
private $objectNormalizer;
15+
16+
public function __construct(DenormalizerInterface $objectNormalizer)
17+
{
18+
$this->objectNormalizer = $objectNormalizer;
19+
}
20+
21+
public function supportsDenormalization($data, $type, $format = null, array $context = []): bool
22+
{
23+
return isset($context['input']['class']);
24+
}
25+
26+
public function denormalize($data, $type, $format = null, array $context = [])
27+
{
28+
$parameters = (new \ReflectionClass($context['input']['class']))->getConstructor()->getParameters();
29+
30+
$missingFields = [];
31+
foreach ($parameters as $parameter) {
32+
if (!isset($data[$parameter->getName()]) && !$parameter->allowsNull()) {
33+
$missingFields[] = $parameter->getName();
34+
}
35+
}
36+
37+
if (count($missingFields) === 0) {
38+
return $this->objectNormalizer->denormalize($data, $this->getInputClassName($context), $format, $context);
39+
}
40+
41+
throw new MissingConstructorArgumentsException(
42+
sprintf('Request does not have the following required fields specified: %s.', implode(', ', $missingFields))
43+
);
44+
}
45+
46+
private function getInputClassName(array $context): ?string
47+
{
48+
return $context['input']['class'] ?? null;
49+
}
50+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sylius\Bundle\ApiBundle\Serializer;
6+
7+
use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
8+
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
9+
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
10+
11+
final class CommandOperationNormalizer implements ContextAwareNormalizerInterface
12+
{
13+
private const ALREADY_CALLED = 'command_operation_normalizer_already_called';
14+
15+
/** @var NormalizerInterface */
16+
private $objectNormalizer;
17+
18+
public function __construct(NormalizerInterface $objectNormalizer)
19+
{
20+
$this->objectNormalizer = $objectNormalizer;
21+
}
22+
23+
public function supportsNormalization($data, string $format = null, array $context = []): bool
24+
{
25+
if (isset($context[self::ALREADY_CALLED])) {
26+
return false;
27+
}
28+
29+
return method_exists($data, 'getClass') && $data->getClass() === MissingConstructorArgumentsException::class;
30+
}
31+
32+
public function normalize($object, string $format = null, array $context = [])
33+
{
34+
$context[self::ALREADY_CALLED] = true;
35+
$data = $this->objectNormalizer->normalize($object, $format, $context);
36+
37+
return [
38+
'code' => 400,
39+
'message' => $data['message'],
40+
];
41+
}
42+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Sylius package.
5+
*
6+
* (c) Paweł Jędrzejewski
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace spec\Sylius\Bundle\ApiBundle\Serializer;
15+
16+
use PhpSpec\ObjectBehavior;
17+
use Prophecy\Argument;
18+
use Sylius\Bundle\ApiBundle\Command\RegisterShopUser;
19+
use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
20+
use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface;
21+
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
22+
23+
final class CommandOperationDenormalizerSpec extends ObjectBehavior
24+
{
25+
function let(DenormalizerInterface $baseNormalizer): void
26+
{
27+
$this->beConstructedWith($baseNormalizer);
28+
}
29+
30+
function it_throws_exception_if_not_all_required_parameters_are_present_in_the_context(
31+
DenormalizerInterface $baseNormalizer
32+
): void {
33+
$baseNormalizer->denormalize(Argument::any())->shouldNotBeCalled();
34+
35+
$this
36+
->shouldThrow(new MissingConstructorArgumentsException(
37+
'Request does not have the following required fields specified: firstName, lastName.'
38+
))
39+
->during(
40+
'denormalize',
41+
[
42+
['email' => '[email protected]', 'password' => 'pa$$word'],
43+
'',
44+
null,
45+
['input' => ['class' => RegisterShopUser::class]]
46+
]
47+
)
48+
;
49+
}
50+
51+
function it_denormalizes_data_if_all_required_parameters_are_specified(
52+
DenormalizerInterface $baseNormalizer
53+
): void {
54+
$baseNormalizer
55+
->denormalize(
56+
['firstName' => 'John', 'lastName' => 'Doe', 'email' => '[email protected]', 'password' => 'pa$$word'],
57+
RegisterShopUser::class,
58+
null,
59+
['input' => ['class' => RegisterShopUser::class]]
60+
)
61+
->willReturn(['key' => 'value'])
62+
;
63+
64+
$this->denormalize(
65+
['firstName' => 'John', 'lastName' => 'Doe', 'email' => '[email protected]', 'password' => 'pa$$word'],
66+
'',
67+
null,
68+
['input' => ['class' => RegisterShopUser::class]]
69+
)->shouldReturn(['key' => 'value']);
70+
}
71+
72+
function it_implements_context_aware_denormalizer_interface(): void
73+
{
74+
$this->shouldImplement(ContextAwareDenormalizerInterface::class);
75+
}
76+
77+
function it_supports_denormalization_for_specified_input_class(): void
78+
{
79+
$this->supportsDenormalization(null, '', null, ['input' => ['class' => 'Class']])->shouldReturn(true);
80+
}
81+
82+
function it_does_not_support_denormalization_for_not_specified_input_class(): void
83+
{
84+
$this->supportsDenormalization(null, '', null, [])->shouldReturn(false);
85+
}
86+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Sylius package.
5+
*
6+
* (c) Paweł Jędrzejewski
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace spec\Sylius\Bundle\ApiBundle\Serializer;
15+
16+
use PhpSpec\ObjectBehavior;
17+
use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
18+
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
19+
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
20+
21+
final class CommandOperationNormalizerSpec extends ObjectBehavior
22+
{
23+
function let(NormalizerInterface $baseNormalizer): void
24+
{
25+
$this->beConstructedWith($baseNormalizer);
26+
}
27+
28+
function it_implements_context_aware_normalizer_interface(): void
29+
{
30+
$this->shouldImplement(ContextAwareNormalizerInterface::class);
31+
}
32+
33+
function it_supports_normalization_if_data_has_get_class_method_and_it_is_missing_constructor_arguments_exception(): void
34+
{
35+
$this->supportsNormalization(
36+
new class() { public function getClass(): string { return MissingConstructorArgumentsException::class; }}
37+
)->shouldReturn(true);
38+
}
39+
40+
function it_does_not_support_normalization_if_data_has_no_get_class_method(): void
41+
{
42+
$this->supportsNormalization(new \stdClass())->shouldReturn(false);
43+
}
44+
45+
function it_does_not_support_normalization_if_data_class_is_not_missing_constructor_arguments_exception(): void
46+
{
47+
$this
48+
->supportsNormalization(new class() { public function getClass(): string { return \Exception::class; }})
49+
->shouldReturn(false)
50+
;
51+
}
52+
53+
function it_does_not_support_normalization_if_normalizer_has_already_been_called(): void
54+
{
55+
$this
56+
->supportsNormalization(new \stdClass(), null, ['command_operation_normalizer_already_called' => true])
57+
->shouldReturn(false)
58+
;
59+
}
60+
61+
function it_normalizes_response_for_missing_constructor_arguments_exception(
62+
NormalizerInterface $baseNormalizer,
63+
\stdClass $object
64+
): void {
65+
$baseNormalizer
66+
->normalize($object, null, ['command_operation_normalizer_already_called' => true])
67+
->willReturn(['message' => 'Message'])
68+
;
69+
70+
$this->normalize($object)->shouldReturn(['code' => 400, 'message' => 'Message']);
71+
}
72+
}

0 commit comments

Comments
 (0)