Skip to content

Commit 6bf894f

Browse files
authored
fix(serializer): use attribute denormalization context for constructor arguments (#6821)
1 parent 523acbc commit 6bf894f

File tree

4 files changed

+57
-14
lines changed

4 files changed

+57
-14
lines changed

features/security/strong_typing.feature

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,19 @@ Feature: Handle properly invalid data submitted to the API
8989
And the response should be in JSON
9090
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
9191

92+
Scenario: Ignore date with wrong format
93+
When I add "Content-Type" header equal to "application/ld+json"
94+
And I send a "POST" request to "/dummies" with body:
95+
"""
96+
{
97+
"name": "Invalid date format",
98+
"dummyDateWithFormat": "2020-01-01T00:00:00+00:00"
99+
}
100+
"""
101+
Then the response status code should be 400
102+
And the response should be in JSON
103+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
104+
92105
Scenario: Send non-array data when an array is expected
93106
When I add "Content-Type" header equal to "application/ld+json"
94107
And I send a "POST" request to "/dummies" with body:

src/Serializer/AbstractItemNormalizer.php

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,8 @@ protected function instantiateObject(array &$data, string $class, array &$contex
317317
foreach ($constructorParameters as $constructorParameter) {
318318
$paramName = $constructorParameter->name;
319319
$key = $this->nameConverter ? $this->nameConverter->normalize($paramName, $class, $format, $context) : $paramName;
320+
$attributeContext = $this->getAttributeDenormalizationContext($class, $paramName, $context);
321+
$attributeContext['deserialization_path'] = $attributeContext['deserialization_path'] ?? $key;
320322

321323
$allowed = false === $allowedAttributes || (\is_array($allowedAttributes) && \in_array($paramName, $allowedAttributes, true));
322324
$ignored = !$this->isAllowedAttribute($class, $paramName, $format, $context);
@@ -329,10 +331,8 @@ protected function instantiateObject(array &$data, string $class, array &$contex
329331
$params[] = $data[$paramName];
330332
}
331333
} elseif ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) {
332-
$constructorContext = $context;
333-
$constructorContext['deserialization_path'] = $context['deserialization_path'] ?? $key;
334334
try {
335-
$params[] = $this->createConstructorArgument($data[$key], $key, $constructorParameter, $constructorContext, $format);
335+
$params[] = $this->createConstructorArgument($data[$key], $key, $constructorParameter, $attributeContext, $format);
336336
} catch (NotNormalizableValueException $exception) {
337337
if (!isset($context['not_normalizable_value_exceptions'])) {
338338
throw $exception;
@@ -351,7 +351,6 @@ protected function instantiateObject(array &$data, string $class, array &$contex
351351
$missingConstructorArguments[] = $constructorParameter->name;
352352
}
353353

354-
$attributeContext = $this->getAttributeDenormalizationContext($class, $paramName, $context);
355354
$constructorParameterType = 'unknown';
356355
$reflectionType = $constructorParameter->getType();
357356
if ($reflectionType instanceof \ReflectionNamedType) {
@@ -362,7 +361,7 @@ protected function instantiateObject(array &$data, string $class, array &$contex
362361
\sprintf('Failed to create object because the class misses the "%s" property.', $constructorParameter->name),
363362
null,
364363
[$constructorParameterType],
365-
$attributeContext['deserialization_path'] ?? null,
364+
$attributeContext['deserialization_path'],
366365
true
367366
);
368367
$context['not_normalizable_value_exceptions'][] = $exception;
@@ -405,7 +404,7 @@ protected function getClassDiscriminatorResolvedClass(array $data, string $class
405404
return $mappedClass;
406405
}
407406

408-
protected function createConstructorArgument($parameterData, string $key, \ReflectionParameter $constructorParameter, array &$context, ?string $format = null): mixed
407+
protected function createConstructorArgument($parameterData, string $key, \ReflectionParameter $constructorParameter, array $context, ?string $format = null): mixed
409408
{
410409
return $this->createAndValidateAttributeValue($constructorParameter->name, $parameterData, $format, $context);
411410
}

tests/Fixtures/TestBundle/Document/Dummy.php

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
use Doctrine\Common\Collections\ArrayCollection;
2121
use Doctrine\Common\Collections\Collection;
2222
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
23+
use Symfony\Component\Serializer\Attribute\Context;
24+
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
2325
use Symfony\Component\Validator\Constraints as Assert;
2426

2527
/**
@@ -28,9 +30,9 @@
2830
* @author Kévin Dunglas <[email protected]>
2931
* @author Alexandre Delplace <[email protected]>
3032
*/
31-
#[ApiResource(extraProperties: ['doctrine_mongodb' => ['execute_options' => ['allowDiskUse' => true]], 'standard_put' => false, 'rfc_7807_compliant_errors' => false], filters: ['my_dummy.mongodb.boolean', 'my_dummy.mongodb.date', 'my_dummy.mongodb.exists', 'my_dummy.mongodb.numeric', 'my_dummy.mongodb.order', 'my_dummy.mongodb.range', 'my_dummy.mongodb.search', 'my_dummy.property'])]
32-
#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy')], status: 200, filters: ['my_dummy.mongodb.boolean', 'my_dummy.mongodb.date', 'my_dummy.mongodb.exists', 'my_dummy.mongodb.numeric', 'my_dummy.mongodb.order', 'my_dummy.mongodb.range', 'my_dummy.mongodb.search', 'my_dummy.property'], operations: [new Get()])]
33-
#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy')], status: 200, filters: ['my_dummy.mongodb.boolean', 'my_dummy.mongodb.date', 'my_dummy.mongodb.exists', 'my_dummy.mongodb.numeric', 'my_dummy.mongodb.order', 'my_dummy.mongodb.range', 'my_dummy.mongodb.search', 'my_dummy.property'], operations: [new Get()])]
33+
#[ApiResource(extraProperties: ['doctrine_mongodb' => ['execute_options' => ['allowDiskUse' => true]], 'standard_put' => false, 'rfc_7807_compliant_errors' => false], filters: ['my_dummy.mongodb.boolean', 'my_dummy.mongodb.date', 'my_dummy.mongodb.exists', 'my_dummy.mongodb.numeric', 'my_dummy.mongodb.order', 'my_dummy.mongodb.range', 'my_dummy.mongodb.search', 'my_dummy.property'], normalizationContext: [AbstractNormalizer::IGNORED_ATTRIBUTES => ['dummyDateWithFormat']])]
34+
#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy')], status: 200, filters: ['my_dummy.mongodb.boolean', 'my_dummy.mongodb.date', 'my_dummy.mongodb.exists', 'my_dummy.mongodb.numeric', 'my_dummy.mongodb.order', 'my_dummy.mongodb.range', 'my_dummy.mongodb.search', 'my_dummy.property'], operations: [new Get()], normalizationContext: [AbstractNormalizer::IGNORED_ATTRIBUTES => ['dummyDateWithFormat']])]
35+
#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy')], status: 200, filters: ['my_dummy.mongodb.boolean', 'my_dummy.mongodb.date', 'my_dummy.mongodb.exists', 'my_dummy.mongodb.numeric', 'my_dummy.mongodb.order', 'my_dummy.mongodb.range', 'my_dummy.mongodb.search', 'my_dummy.property'], operations: [new Get()], normalizationContext: [AbstractNormalizer::IGNORED_ATTRIBUTES => ['dummyDateWithFormat']])]
3436
#[ODM\Document]
3537
class Dummy
3638
{
@@ -75,6 +77,13 @@ class Dummy
7577
#[ApiProperty(iris: ['https://schema.org/DateTime'])]
7678
#[ODM\Field(type: 'date', nullable: true)]
7779
public $dummyDate;
80+
/**
81+
* @var \DateTime|null A dummy date
82+
*/
83+
#[Context(denormalizationContext: ['datetime_format' => 'Y-m-d'])]
84+
#[ApiProperty(iris: ['https://schema.org/DateTime'])]
85+
#[ODM\Field(type: 'date', nullable: true)]
86+
private $dummyDateWithFormat;
7887
/**
7988
* @var float|null A dummy float
8089
*/
@@ -113,9 +122,10 @@ public static function staticMethod(): void
113122
{
114123
}
115124

116-
public function __construct()
125+
public function __construct(?\DateTime $dummyDateWithFormat = null)
117126
{
118127
$this->relatedDummies = new ArrayCollection();
128+
$this->dummyDateWithFormat = $dummyDateWithFormat;
119129
}
120130

121131
public function getId(): ?int
@@ -178,6 +188,11 @@ public function getDummyDate()
178188
return $this->dummyDate;
179189
}
180190

191+
public function getDummyDateWithFormat()
192+
{
193+
return $this->dummyDateWithFormat;
194+
}
195+
181196
public function setDummyPrice($dummyPrice)
182197
{
183198
$this->dummyPrice = $dummyPrice;

tests/Fixtures/TestBundle/Entity/Dummy.php

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,18 @@
2020
use Doctrine\Common\Collections\ArrayCollection;
2121
use Doctrine\Common\Collections\Collection;
2222
use Doctrine\ORM\Mapping as ORM;
23+
use Symfony\Component\Serializer\Attribute\Context;
24+
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
2325
use Symfony\Component\Validator\Constraints as Assert;
2426

2527
/**
2628
* Dummy.
2729
*
2830
* @author Kévin Dunglas <[email protected]>
2931
*/
30-
#[ApiResource(filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], extraProperties: ['standard_put' => false, 'rfc_7807_compliant_errors' => false])]
31-
#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy')], status: 200, filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], operations: [new Get()])]
32-
#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy')], status: 200, filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], operations: [new Get()])]
32+
#[ApiResource(filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], extraProperties: ['standard_put' => false, 'rfc_7807_compliant_errors' => false], normalizationContext: [AbstractNormalizer::IGNORED_ATTRIBUTES => ['dummyDateWithFormat']])]
33+
#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy')], status: 200, filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], operations: [new Get()], normalizationContext: [AbstractNormalizer::IGNORED_ATTRIBUTES => ['dummyDateWithFormat']])]
34+
#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy')], status: 200, filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], operations: [new Get()], normalizationContext: [AbstractNormalizer::IGNORED_ATTRIBUTES => ['dummyDateWithFormat']])]
3335
#[ORM\Entity]
3436
class Dummy
3537
{
@@ -87,6 +89,14 @@ class Dummy
8789
#[ORM\Column(type: 'datetime', nullable: true)]
8890
public $dummyDate;
8991

92+
/**
93+
* @var \DateTime|null A dummy date with format
94+
*/
95+
#[Context(denormalizationContext: ['datetime_format' => 'Y-m-d'])]
96+
#[ApiProperty(iris: ['https://schema.org/DateTime'])]
97+
#[ORM\Column(type: 'datetime', nullable: true)]
98+
private $dummyDateWithFormat;
99+
90100
/**
91101
* @var float|null A dummy float
92102
*/
@@ -140,9 +150,10 @@ public static function staticMethod(): void
140150
{
141151
}
142152

143-
public function __construct()
153+
public function __construct(?\DateTime $dummyDateWithFormat = null)
144154
{
145155
$this->relatedDummies = new ArrayCollection();
156+
$this->dummyDateWithFormat = $dummyDateWithFormat;
146157
}
147158

148159
public function getId()
@@ -209,6 +220,11 @@ public function getDummyDate()
209220
return $this->dummyDate;
210221
}
211222

223+
public function getDummyDateWithFormat()
224+
{
225+
return $this->dummyDateWithFormat;
226+
}
227+
212228
public function setDummyPrice($dummyPrice)
213229
{
214230
$this->dummyPrice = $dummyPrice;

0 commit comments

Comments
 (0)