-
-
Notifications
You must be signed in to change notification settings - Fork 921
Constructor relations writtable #2178
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e1733fd
4309ffe
4a80efa
508e6ee
34cd246
a143f0b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
Feature: Value object as ApiResource | ||
In order to keep ApiResource immutable | ||
As a client software developer | ||
I need to be able to use class without setters as ApiResource | ||
|
||
@createSchema | ||
Scenario: Create Value object resource | ||
When I add "Content-Type" header equal to "application/ld+json" | ||
And I send a "POST" request to "/vo_dummy_cars" with body: | ||
""" | ||
{ | ||
"mileage": 1500, | ||
"bodyType": "suv", | ||
"make": "CustomCar", | ||
"insuranceCompany": { | ||
"name": "Safe Drive Company" | ||
}, | ||
"drivers": [ | ||
{ | ||
"firstName": "John", | ||
"lastName": "Doe" | ||
} | ||
] | ||
} | ||
""" | ||
Then the response status code should be 201 | ||
And the JSON should be equal to: | ||
""" | ||
{ | ||
"@context": "/contexts/VoDummyCar", | ||
"@id": "/vo_dummy_cars/1", | ||
"@type": "VoDummyCar", | ||
"mileage": 1500, | ||
"bodyType": "suv", | ||
"inspections": [], | ||
"make": "CustomCar", | ||
"insuranceCompany": { | ||
"@id": "/vo_dummy_insurance_companies/1", | ||
"@type": "VoDummyInsuranceCompany", | ||
"name": "Safe Drive Company" | ||
}, | ||
"drivers": [ | ||
{ | ||
"@id": "/vo_dummy_drivers/1", | ||
"@type": "VoDummyDriver", | ||
"firstName": "John", | ||
"lastName": "Doe" | ||
} | ||
] | ||
} | ||
""" | ||
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" | ||
|
||
Scenario: Create Value object with IRI and nullable parameter | ||
When I add "Content-Type" header equal to "application/ld+json" | ||
And I send a "POST" request to "/vo_dummy_inspections" with body: | ||
""" | ||
{ | ||
"accepted": true, | ||
"car": "/vo_dummy_cars/1" | ||
} | ||
""" | ||
Then the response status code should be 201 | ||
And the JSON should be valid according to this schema: | ||
""" | ||
{ | ||
"type": "object", | ||
"required": ["accepted", "performed", "car"], | ||
"properties": { | ||
"accepted": { | ||
"enum":[true] | ||
}, | ||
"performed": { | ||
"format": "date-time" | ||
}, | ||
"car": { | ||
"enum": ["/vo_dummy_cars/1"] | ||
} | ||
} | ||
} | ||
""" | ||
|
||
Scenario: Update Value object with writable and non writable property | ||
When I add "Content-Type" header equal to "application/ld+json" | ||
And I send a "PUT" request to "/vo_dummy_inspections/1" with body: | ||
""" | ||
{ | ||
"performed": "2018-08-24 00:00:00", | ||
"accepted": false | ||
} | ||
""" | ||
Then the response status code should be 200 | ||
And the JSON should be equal to: | ||
""" | ||
{ | ||
"@context": "/contexts/VoDummyInspection", | ||
"@id": "/vo_dummy_inspections/1", | ||
"@type": "VoDummyInspection", | ||
"accepted": true, | ||
"car": "/vo_dummy_cars/1", | ||
"performed": "2018-08-24T00:00:00+00:00", | ||
"id": 1 | ||
} | ||
""" | ||
|
||
@createSchema | ||
Scenario: Create Value object without required params | ||
When I add "Content-Type" header equal to "application/ld+json" | ||
And I send a "POST" request to "/vo_dummy_cars" with body: | ||
""" | ||
{ | ||
"mileage": 1500, | ||
"make": "CustomCar", | ||
"insuranceCompany": { | ||
"name": "Safe Drive Company" | ||
} | ||
} | ||
""" | ||
Then the response status code should be 400 | ||
And the JSON should be valid according to this schema: | ||
""" | ||
{ | ||
"type": "object", | ||
"properties": { | ||
"@context": { | ||
"enum": ["/contexts/Error"] | ||
}, | ||
"type": { | ||
"enum": ["hydra:Error"] | ||
}, | ||
"hydra:title": { | ||
"enum": ["An error occurred"] | ||
}, | ||
"hydra:description": { | ||
"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."] | ||
} | ||
} | ||
} | ||
""" | ||
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" | ||
|
||
@createSchema | ||
Scenario: Create Value object without default param | ||
When I add "Content-Type" header equal to "application/ld+json" | ||
And I send a "POST" request to "/vo_dummy_cars" with body: | ||
""" | ||
{ | ||
"mileage": 1500, | ||
"make": "CustomCar", | ||
"insuranceCompany": { | ||
"name": "Safe Drive Company" | ||
}, | ||
"drivers": [ | ||
{ | ||
"firstName": "John", | ||
"lastName": "Doe" | ||
} | ||
] | ||
} | ||
""" | ||
Then the response status code should be 201 | ||
And the JSON should be equal to: | ||
""" | ||
{ | ||
"@context": "/contexts/VoDummyCar", | ||
"@id": "/vo_dummy_cars/1", | ||
"@type": "VoDummyCar", | ||
"mileage": 1500, | ||
"bodyType": "coupe", | ||
"inspections": [], | ||
"make": "CustomCar", | ||
"insuranceCompany": { | ||
"@id": "/vo_dummy_insurance_companies/1", | ||
"@type": "VoDummyInsuranceCompany", | ||
"name": "Safe Drive Company" | ||
}, | ||
"drivers": [ | ||
{ | ||
"@id": "/vo_dummy_drivers/1", | ||
"@type": "VoDummyDriver", | ||
"firstName": "John", | ||
"lastName": "Doe" | ||
} | ||
] | ||
} | ||
""" | ||
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,6 +27,8 @@ | |
use Symfony\Component\PropertyAccess\PropertyAccess; | ||
use Symfony\Component\PropertyAccess\PropertyAccessorInterface; | ||
use Symfony\Component\PropertyInfo\Type; | ||
use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException; | ||
use Symfony\Component\Serializer\Exception\RuntimeException; | ||
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; | ||
use Symfony\Component\Serializer\NameConverter\NameConverterInterface; | ||
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; | ||
|
@@ -127,6 +129,93 @@ public function denormalize($data, $class, $format = null, array $context = []) | |
return parent::denormalize($data, $class, $format, $context); | ||
} | ||
|
||
/** | ||
* Method copy-pasted from symfony/serializer. | ||
* Remove it after symfony/serializer version update @link https://github.com/symfony/symfony/pull/28263. | ||
* | ||
* {@inheritdoc} | ||
* | ||
* @internal | ||
*/ | ||
protected function instantiateObject(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes, string $format = null) | ||
{ | ||
if ($this->classDiscriminatorResolver && $mapping = $this->classDiscriminatorResolver->getMappingForClass($class)) { | ||
if (!isset($data[$mapping->getTypeProperty()])) { | ||
throw new RuntimeException(sprintf('Type property "%s" not found for the abstract object "%s"', $mapping->getTypeProperty(), $class)); | ||
} | ||
|
||
$type = $data[$mapping->getTypeProperty()]; | ||
if (null === ($mappedClass = $mapping->getClassForType($type))) { | ||
throw new RuntimeException(sprintf('The type "%s" has no mapped class for the abstract object "%s"', $type, $class)); | ||
} | ||
|
||
$class = $mappedClass; | ||
$reflectionClass = new \ReflectionClass($class); | ||
} | ||
|
||
if (null !== $object = $this->extractObjectToPopulate($class, $context, static::OBJECT_TO_POPULATE)) { | ||
unset($context[static::OBJECT_TO_POPULATE]); | ||
|
||
return $object; | ||
} | ||
|
||
$constructor = $this->getConstructor($data, $class, $context, $reflectionClass, $allowedAttributes); | ||
if ($constructor) { | ||
$constructorParameters = $constructor->getParameters(); | ||
|
||
$params = []; | ||
foreach ($constructorParameters as $constructorParameter) { | ||
$paramName = $constructorParameter->name; | ||
$key = $this->nameConverter ? $this->nameConverter->normalize($paramName) : $paramName; | ||
|
||
$allowed = false === $allowedAttributes || \in_array($paramName, $allowedAttributes, true); | ||
$ignored = !$this->isAllowedAttribute($class, $paramName, $format, $context); | ||
if ($constructorParameter->isVariadic()) { | ||
if ($allowed && !$ignored && (isset($data[$key]) || array_key_exists($key, $data))) { | ||
if (!\is_array($data[$paramName])) { | ||
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)); | ||
} | ||
|
||
$params = array_merge($params, $data[$paramName]); | ||
} | ||
} elseif ($allowed && !$ignored && (isset($data[$key]) || array_key_exists($key, $data))) { | ||
$params[] = $this->createConstructorArgument($data[$key], $key, $constructorParameter, $context, $format); | ||
|
||
// Don't run set for a parameter passed to the constructor | ||
unset($data[$key]); | ||
} elseif (isset($context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key])) { | ||
$params[] = $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key]; | ||
} elseif ($constructorParameter->isDefaultValueAvailable()) { | ||
$params[] = $constructorParameter->getDefaultValue(); | ||
} else { | ||
throw new MissingConstructorArgumentsException( | ||
sprintf( | ||
'Cannot create an instance of %s from serialized data because its constructor requires parameter "%s" to be present.', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can this error be more consumer friendly on serialization? it's exposing implementation detail to the consumer which they don't care about: the class name, overly verbose message etc... all the consumer needs to know is that they didn't provide the some parameter. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This code comes from |
||
$class, | ||
$constructorParameter->name | ||
) | ||
); | ||
} | ||
} | ||
|
||
if ($constructor->isConstructor()) { | ||
return $reflectionClass->newInstanceArgs($params); | ||
} | ||
|
||
return $constructor->invokeArgs(null, $params); | ||
} | ||
|
||
return new $class(); | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
protected function createConstructorArgument($parameterData, string $key, \ReflectionParameter $constructorParameter, array &$context, string $format = null) | ||
{ | ||
return $this->createAttributeValue($constructorParameter->name, $parameterData, $format, $context); | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
* | ||
|
@@ -167,6 +256,11 @@ protected function getAllowedAttributes($classOrObject, array $context, $attribu | |
* {@inheritdoc} | ||
*/ | ||
protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = []) | ||
{ | ||
$this->setValue($object, $attribute, $this->createAttributeValue($attribute, $value, $format, $context)); | ||
} | ||
|
||
private function createAttributeValue($attribute, $value, $format = null, array $context = []) | ||
{ | ||
if (!\is_string($attribute)) { | ||
throw new InvalidValueException('Invalid value provided (invalid IRI?).'); | ||
|
@@ -176,44 +270,29 @@ protected function setAttributeValue($object, $attribute, $value, $format = null | |
$type = $propertyMetadata->getType(); | ||
|
||
if (null === $type) { | ||
// No type provided, blindly set the value | ||
$this->setValue($object, $attribute, $value); | ||
|
||
return; | ||
// No type provided, blindly return the value | ||
return $value; | ||
} | ||
|
||
if (null === $value && $type->isNullable()) { | ||
$this->setValue($object, $attribute, $value); | ||
|
||
return; | ||
return $value; | ||
} | ||
|
||
if ( | ||
$type->isCollection() && | ||
null !== ($collectionValueType = $type->getCollectionValueType()) && | ||
null !== $className = $collectionValueType->getClassName() | ||
) { | ||
$this->setValue( | ||
$object, | ||
$attribute, | ||
$this->denormalizeCollection($attribute, $propertyMetadata, $type, $className, $value, $format, $context) | ||
); | ||
|
||
return; | ||
return $this->denormalizeCollection($attribute, $propertyMetadata, $type, $className, $value, $format, $context); | ||
} | ||
|
||
if (null !== $className = $type->getClassName()) { | ||
$this->setValue( | ||
$object, | ||
$attribute, | ||
$this->denormalizeRelation($attribute, $propertyMetadata, $className, $value, $format, $this->createChildContext($context, $attribute)) | ||
); | ||
|
||
return; | ||
return $this->denormalizeRelation($attribute, $propertyMetadata, $className, $value, $format, $this->createChildContext($context, $attribute)); | ||
} | ||
|
||
$this->validateType($attribute, $type, $value, $format); | ||
$this->setValue($object, $attribute, $value); | ||
|
||
return $value; | ||
} | ||
|
||
/** | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please mark it as
@internal
because we'll remove it at some point.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done