Skip to content

Commit 5275c5a

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
1 parent 3fdb58c commit 5275c5a

File tree

8 files changed

+669
-22
lines changed

8 files changed

+669
-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: 100 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,92 @@ 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, $constructorParameter, $context, $format);
181+
} elseif (isset($context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key])) {
182+
$params[] = $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key];
183+
} elseif ($constructorParameter->isDefaultValueAvailable()) {
184+
$params[] = $constructorParameter->getDefaultValue();
185+
} else {
186+
throw new MissingConstructorArgumentsException(
187+
sprintf(
188+
'Cannot create an instance of %s from serialized data because its constructor requires parameter "%s" to be present.',
189+
$class,
190+
$constructorParameter->name
191+
)
192+
);
193+
}
194+
}
195+
196+
if ($constructor->isConstructor()) {
197+
return $reflectionClass->newInstanceArgs($params);
198+
}
199+
200+
return $constructor->invokeArgs(null, $params);
201+
}
202+
203+
return new $class();
204+
}
205+
206+
/**
207+
* {@inheritdoc}
208+
*/
209+
protected function createConstructorArgument(array &$data, string $key, \ReflectionParameter $constructorParameter, array &$context, string $format = null)
210+
{
211+
$argument = $this->createAttributeValue($constructorParameter->name, $data[$key], $format, $context);
212+
// Don't run set for a parameter passed to the constructor
213+
unset($data[$key]);
214+
215+
return $argument;
216+
}
217+
130218
/**
131219
* {@inheritdoc}
132220
*
@@ -167,6 +255,11 @@ protected function getAllowedAttributes($classOrObject, array $context, $attribu
167255
* {@inheritdoc}
168256
*/
169257
protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = [])
258+
{
259+
$this->setValue($object, $attribute, $this->createAttributeValue($attribute, $value, $format, $context));
260+
}
261+
262+
private function createAttributeValue($attribute, $value, $format = null, array $context = [])
170263
{
171264
if (!\is_string($attribute)) {
172265
throw new InvalidValueException('Invalid value provided (invalid IRI?).');
@@ -176,44 +269,29 @@ protected function setAttributeValue($object, $attribute, $value, $format = null
176269
$type = $propertyMetadata->getType();
177270

178271
if (null === $type) {
179-
// No type provided, blindly set the value
180-
$this->setValue($object, $attribute, $value);
181-
182-
return;
272+
// No type provided, blindly return the value
273+
return $value;
183274
}
184275

185276
if (null === $value && $type->isNullable()) {
186-
$this->setValue($object, $attribute, $value);
187-
188-
return;
277+
return $value;
189278
}
190279

191280
if (
192281
$type->isCollection() &&
193282
null !== ($collectionValueType = $type->getCollectionValueType()) &&
194283
null !== $className = $collectionValueType->getClassName()
195284
) {
196-
$this->setValue(
197-
$object,
198-
$attribute,
199-
$this->denormalizeCollection($attribute, $propertyMetadata, $type, $className, $value, $format, $context)
200-
);
201-
202-
return;
285+
return $this->denormalizeCollection($attribute, $propertyMetadata, $type, $className, $value, $format, $context);
203286
}
204287

205288
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;
289+
return $this->denormalizeRelation($attribute, $propertyMetadata, $className, $value, $format, $this->createChildContext($context, $attribute));
213290
}
214291

215292
$this->validateType($attribute, $type, $value, $format);
216-
$this->setValue($object, $attribute, $value);
293+
294+
return $value;
217295
}
218296

219297
/**

0 commit comments

Comments
 (0)