Skip to content

Commit e1733fd

Browse files
committed
test relations with writable constructor parameters
implement constructor params relation denormalization drop identifier getter typehint because of doctrine listeners problem tests for cyclic relationship drop constructor cycle deps (impossible to handle), add iri and nullable param test test put method remove return types from test entities to pass low deps test fix cs fix phpstan adjust to changes in symfony/serializer PR
1 parent 3fdb58c commit e1733fd

File tree

8 files changed

+668
-22
lines changed

8 files changed

+668
-22
lines changed
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
Feature: Value object as ApiResource
2+
In order to keep ApiResource immutable
3+
As a client software developer
4+
I need to be able to use class without setters as ApiResource
5+
6+
@createSchema
7+
Scenario: Create Value object resource
8+
When I add "Content-Type" header equal to "application/ld+json"
9+
And I send a "POST" request to "/vo_dummy_cars" with body:
10+
"""
11+
{
12+
"mileage": 1500,
13+
"bodyType": "suv",
14+
"make": "CustomCar",
15+
"insuranceCompany": {
16+
"name": "Safe Drive Company"
17+
},
18+
"drivers": [
19+
{
20+
"firstName": "John",
21+
"lastName": "Doe"
22+
}
23+
]
24+
}
25+
"""
26+
Then the response status code should be 201
27+
And the JSON should be equal to:
28+
"""
29+
{
30+
"@context": "/contexts/VoDummyCar",
31+
"@id": "/vo_dummy_cars/1",
32+
"@type": "VoDummyCar",
33+
"mileage": 1500,
34+
"bodyType": "suv",
35+
"inspections": [],
36+
"make": "CustomCar",
37+
"insuranceCompany": {
38+
"@id": "/vo_dummy_insurance_companies/1",
39+
"@type": "VoDummyInsuranceCompany",
40+
"name": "Safe Drive Company"
41+
},
42+
"drivers": [
43+
{
44+
"@id": "/vo_dummy_drivers/1",
45+
"@type": "VoDummyDriver",
46+
"firstName": "John",
47+
"lastName": "Doe"
48+
}
49+
]
50+
}
51+
"""
52+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
53+
54+
Scenario: Create Value object with IRI and nullable parameter
55+
When I add "Content-Type" header equal to "application/ld+json"
56+
And I send a "POST" request to "/vo_dummy_inspections" with body:
57+
"""
58+
{
59+
"accepted": true,
60+
"car": "/vo_dummy_cars/1"
61+
}
62+
"""
63+
Then the response status code should be 201
64+
And the JSON should be valid according to this schema:
65+
"""
66+
{
67+
"type": "object",
68+
"required": ["accepted", "performed", "car"],
69+
"properties": {
70+
"accepted": {
71+
"enum":[true]
72+
},
73+
"performed": {
74+
"format": "date-time"
75+
},
76+
"car": {
77+
"enum": ["/vo_dummy_cars/1"]
78+
}
79+
}
80+
}
81+
"""
82+
83+
Scenario: Update Value object with writable and non writable property
84+
When I add "Content-Type" header equal to "application/ld+json"
85+
And I send a "PUT" request to "/vo_dummy_inspections/1" with body:
86+
"""
87+
{
88+
"performed": "2018-08-24 00:00:00",
89+
"accepted": false
90+
}
91+
"""
92+
Then the response status code should be 200
93+
And the JSON should be equal to:
94+
"""
95+
{
96+
"@context": "/contexts/VoDummyInspection",
97+
"@id": "/vo_dummy_inspections/1",
98+
"@type": "VoDummyInspection",
99+
"accepted": true,
100+
"car": "/vo_dummy_cars/1",
101+
"performed": "2018-08-24T00:00:00+00:00",
102+
"id": 1
103+
}
104+
"""
105+
106+
@createSchema
107+
Scenario: Create Value object without required params
108+
When I add "Content-Type" header equal to "application/ld+json"
109+
And I send a "POST" request to "/vo_dummy_cars" with body:
110+
"""
111+
{
112+
"mileage": 1500,
113+
"make": "CustomCar",
114+
"insuranceCompany": {
115+
"name": "Safe Drive Company"
116+
}
117+
}
118+
"""
119+
Then the response status code should be 400
120+
And the JSON should be valid according to this schema:
121+
"""
122+
{
123+
"type": "object",
124+
"properties": {
125+
"@context": {
126+
"enum": ["/contexts/Error"]
127+
},
128+
"type": {
129+
"enum": ["hydra:Error"]
130+
},
131+
"hydra:title": {
132+
"enum": ["An error occurred"]
133+
},
134+
"hydra:description": {
135+
"enum": ["Cannot create an instance of ApiPlatform\\Core\\Tests\\Fixtures\\TestBundle\\Entity\\VoDummyCar from serialized data because its constructor requires parameter \"drivers\" to be present."]
136+
}
137+
}
138+
}
139+
"""
140+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
141+
142+
@createSchema
143+
Scenario: Create Value object without default param
144+
When I add "Content-Type" header equal to "application/ld+json"
145+
And I send a "POST" request to "/vo_dummy_cars" with body:
146+
"""
147+
{
148+
"mileage": 1500,
149+
"make": "CustomCar",
150+
"insuranceCompany": {
151+
"name": "Safe Drive Company"
152+
},
153+
"drivers": [
154+
{
155+
"firstName": "John",
156+
"lastName": "Doe"
157+
}
158+
]
159+
}
160+
"""
161+
Then the response status code should be 201
162+
And the JSON should be equal to:
163+
"""
164+
{
165+
"@context": "/contexts/VoDummyCar",
166+
"@id": "/vo_dummy_cars/1",
167+
"@type": "VoDummyCar",
168+
"mileage": 1500,
169+
"bodyType": "coupe",
170+
"inspections": [],
171+
"make": "CustomCar",
172+
"insuranceCompany": {
173+
"@id": "/vo_dummy_insurance_companies/1",
174+
"@type": "VoDummyInsuranceCompany",
175+
"name": "Safe Drive Company"
176+
},
177+
"drivers": [
178+
{
179+
"@id": "/vo_dummy_drivers/1",
180+
"@type": "VoDummyDriver",
181+
"firstName": "John",
182+
"lastName": "Doe"
183+
}
184+
]
185+
}
186+
"""
187+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"

src/Serializer/AbstractItemNormalizer.php

Lines changed: 99 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
use Symfony\Component\PropertyAccess\PropertyAccess;
2828
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
2929
use Symfony\Component\PropertyInfo\Type;
30+
use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
31+
use Symfony\Component\Serializer\Exception\RuntimeException;
3032
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
3133
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
3234
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
@@ -127,6 +129,91 @@ public function denormalize($data, $class, $format = null, array $context = [])
127129
return parent::denormalize($data, $class, $format, $context);
128130
}
129131

132+
/**
133+
* Method copy-pasted from symfony/serializer.
134+
* Remove it after symfony/serializer version update @link https://github.com/symfony/symfony/pull/28263.
135+
*
136+
* {@inheritdoc}
137+
*/
138+
protected function instantiateObject(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes, string $format = null)
139+
{
140+
if ($this->classDiscriminatorResolver && $mapping = $this->classDiscriminatorResolver->getMappingForClass($class)) {
141+
if (!isset($data[$mapping->getTypeProperty()])) {
142+
throw new RuntimeException(sprintf('Type property "%s" not found for the abstract object "%s"', $mapping->getTypeProperty(), $class));
143+
}
144+
145+
$type = $data[$mapping->getTypeProperty()];
146+
if (null === ($mappedClass = $mapping->getClassForType($type))) {
147+
throw new RuntimeException(sprintf('The type "%s" has no mapped class for the abstract object "%s"', $type, $class));
148+
}
149+
150+
$class = $mappedClass;
151+
$reflectionClass = new \ReflectionClass($class);
152+
}
153+
154+
if (null !== $object = $this->extractObjectToPopulate($class, $context, static::OBJECT_TO_POPULATE)) {
155+
unset($context[static::OBJECT_TO_POPULATE]);
156+
157+
return $object;
158+
}
159+
160+
$constructor = $this->getConstructor($data, $class, $context, $reflectionClass, $allowedAttributes);
161+
if ($constructor) {
162+
$constructorParameters = $constructor->getParameters();
163+
164+
$params = [];
165+
foreach ($constructorParameters as $constructorParameter) {
166+
$paramName = $constructorParameter->name;
167+
$key = $this->nameConverter ? $this->nameConverter->normalize($paramName) : $paramName;
168+
169+
$allowed = false === $allowedAttributes || \in_array($paramName, $allowedAttributes, true);
170+
$ignored = !$this->isAllowedAttribute($class, $paramName, $format, $context);
171+
if ($constructorParameter->isVariadic()) {
172+
if ($allowed && !$ignored && (isset($data[$key]) || array_key_exists($key, $data))) {
173+
if (!\is_array($data[$paramName])) {
174+
throw new RuntimeException(sprintf('Cannot create an instance of %s from serialized data because the variadic parameter %s can only accept an array.', $class, $constructorParameter->name));
175+
}
176+
177+
$params = array_merge($params, $data[$paramName]);
178+
}
179+
} elseif ($allowed && !$ignored && (isset($data[$key]) || array_key_exists($key, $data))) {
180+
$params[] = $this->createConstructorArgument($data[$key], $key, $constructorParameter, $context, $format);
181+
182+
// Don't run set for a parameter passed to the constructor
183+
unset($data[$key]);
184+
} elseif (isset($context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key])) {
185+
$params[] = $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key];
186+
} elseif ($constructorParameter->isDefaultValueAvailable()) {
187+
$params[] = $constructorParameter->getDefaultValue();
188+
} else {
189+
throw new MissingConstructorArgumentsException(
190+
sprintf(
191+
'Cannot create an instance of %s from serialized data because its constructor requires parameter "%s" to be present.',
192+
$class,
193+
$constructorParameter->name
194+
)
195+
);
196+
}
197+
}
198+
199+
if ($constructor->isConstructor()) {
200+
return $reflectionClass->newInstanceArgs($params);
201+
}
202+
203+
return $constructor->invokeArgs(null, $params);
204+
}
205+
206+
return new $class();
207+
}
208+
209+
/**
210+
* {@inheritdoc}
211+
*/
212+
protected function createConstructorArgument($parameterData, string $key, \ReflectionParameter $constructorParameter, array &$context, string $format = null)
213+
{
214+
return $this->createAttributeValue($constructorParameter->name, $parameterData, $format, $context);
215+
}
216+
130217
/**
131218
* {@inheritdoc}
132219
*
@@ -167,6 +254,11 @@ protected function getAllowedAttributes($classOrObject, array $context, $attribu
167254
* {@inheritdoc}
168255
*/
169256
protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = [])
257+
{
258+
$this->setValue($object, $attribute, $this->createAttributeValue($attribute, $value, $format, $context));
259+
}
260+
261+
private function createAttributeValue($attribute, $value, $format = null, array $context = [])
170262
{
171263
if (!\is_string($attribute)) {
172264
throw new InvalidValueException('Invalid value provided (invalid IRI?).');
@@ -176,44 +268,29 @@ protected function setAttributeValue($object, $attribute, $value, $format = null
176268
$type = $propertyMetadata->getType();
177269

178270
if (null === $type) {
179-
// No type provided, blindly set the value
180-
$this->setValue($object, $attribute, $value);
181-
182-
return;
271+
// No type provided, blindly return the value
272+
return $value;
183273
}
184274

185275
if (null === $value && $type->isNullable()) {
186-
$this->setValue($object, $attribute, $value);
187-
188-
return;
276+
return $value;
189277
}
190278

191279
if (
192280
$type->isCollection() &&
193281
null !== ($collectionValueType = $type->getCollectionValueType()) &&
194282
null !== $className = $collectionValueType->getClassName()
195283
) {
196-
$this->setValue(
197-
$object,
198-
$attribute,
199-
$this->denormalizeCollection($attribute, $propertyMetadata, $type, $className, $value, $format, $context)
200-
);
201-
202-
return;
284+
return $this->denormalizeCollection($attribute, $propertyMetadata, $type, $className, $value, $format, $context);
203285
}
204286

205287
if (null !== $className = $type->getClassName()) {
206-
$this->setValue(
207-
$object,
208-
$attribute,
209-
$this->denormalizeRelation($attribute, $propertyMetadata, $className, $value, $format, $this->createChildContext($context, $attribute))
210-
);
211-
212-
return;
288+
return $this->denormalizeRelation($attribute, $propertyMetadata, $className, $value, $format, $this->createChildContext($context, $attribute));
213289
}
214290

215291
$this->validateType($attribute, $type, $value, $format);
216-
$this->setValue($object, $attribute, $value);
292+
293+
return $value;
217294
}
218295

219296
/**

0 commit comments

Comments
 (0)