-
-
Notifications
You must be signed in to change notification settings - Fork 9.7k
Description
Symfony version(s) affected: All versions until 5.1 (but I guess 5.2 is too)
Description
Serializer component AbstractNormalizer attemps to guess constructor parameters, and falls back using default values when possible, which is good. Yet, it misses one use case: when you specify nullable non-optional parameter, case in which null is a valid value, not the default one, yet still valid.
It's a matter a choice, either we consider that the incoming data should explicitly set null here, or we consider that it might come from a dynamic language such as JavaScript that may remove undefined values from sent JSON: case in which we might want to fix that and consider that undefined is null.
How to reproduce
Simply write a class with a constructor as such:
class Foo
{
public function __construct(string $foo, ?string $bar)
{
}
}
And attempt to denormalize using this incoming array:
$input = [
'foo' => 'any value',
];
In this specific case, null
is an allowed value for $bar
, and value is not set in $input
, why not simply give it null ? Actual behaviour is that the exception for missing constructor argument is raised and prevents the object from being denormalized.
Possible Solution
Easy two-line fix, in AbstractNormalizer::instantiateObject()
as such:
Replace:
} elseif (\array_key_exists($key, $this->defaultContext[self::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class] ?? [])) {
$params[] = $this->defaultContext[self::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.', $class, $constructorParameter->name));
}
Using:
} elseif (\array_key_exists($key, $this->defaultContext[self::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class] ?? [])) {
$params[] = $this->defaultContext[self::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key];
} elseif ($constructorParameter->isDefaultValueAvailable()) {
$params[] = $constructorParameter->getDefaultValue();
} elseif ($constructorParameter->hasType() && $constructorParameter->getType()->allowsNull()) {
$params[] = null;
} else {
throw new MissingConstructorArgumentsException(sprintf('Cannot create an instance of "%s" from serialized data because its constructor requires parameter "%s" to be present.', $class, $constructorParameter->name));
}
I just added those two lines:
} elseif ($constructorParameter->hasType() && $constructorParameter->getType()->allowsNull()) {
$params[] = null;
Compatible for all PHP version from 7.0 if I read correctly https://www.php.net/manual/en/reflectiontype.allowsnull.php