fix(shared): deserialize into null-initialized typed properties
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -77,7 +77,14 @@ abstract class JsonSerializableObject implements JsonSerializable, JsonDeseriali
|
||||
if (!empty($this->serializableProperties) && !in_array($key, $this->serializableProperties)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Null means absent: leave the property at its default rather
|
||||
// than passing null into an object's jsonDeserialize/enum
|
||||
// coercion or overwriting an initialized collection.
|
||||
if ($value === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$type = gettype($this->$key);
|
||||
|
||||
// Handle JsonDeserializable objects
|
||||
@@ -101,9 +108,10 @@ abstract class JsonSerializableObject implements JsonSerializable, JsonDeseriali
|
||||
$enumClass = get_class($this->$key);
|
||||
$this->$key = $enumClass::from($value);
|
||||
}
|
||||
// Handle regular values
|
||||
// Handle regular values, coercing by declared type when the
|
||||
// current value is null and carries no instance to key off
|
||||
else {
|
||||
$this->$key = $value;
|
||||
$this->$key = $this->coerceValue($key, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,6 +119,48 @@ abstract class JsonSerializableObject implements JsonSerializable, JsonDeseriali
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Coerces a decoded JSON value to the property's declared type. Needed for
|
||||
* properties whose current value is null: the instanceof-based branches
|
||||
* above cannot dispatch without an instance, and assigning the raw string
|
||||
* to a typed property (DateTime, enum, ...) would raise a TypeError.
|
||||
*/
|
||||
private function coerceValue(string $key, mixed $value): mixed {
|
||||
$type = (new \ReflectionProperty($this, $key))->getType();
|
||||
if ($type instanceof \ReflectionNamedType) {
|
||||
$names = [$type->getName()];
|
||||
} elseif ($type instanceof \ReflectionUnionType) {
|
||||
$names = array_map(static fn($member) => $member->getName(), $type->getTypes());
|
||||
} else {
|
||||
return $value;
|
||||
}
|
||||
|
||||
foreach ($names as $name) {
|
||||
if (!class_exists($name) && !interface_exists($name) && !enum_exists($name)) {
|
||||
continue;
|
||||
}
|
||||
if (is_string($value) && is_a($name, DateTimeInterface::class, true)) {
|
||||
return $name === \DateTime::class ? new \DateTime($value) : new \DateTimeImmutable($value);
|
||||
}
|
||||
if (is_string($value) && is_a($name, DateTimeZone::class, true)) {
|
||||
return new DateTimeZone($value);
|
||||
}
|
||||
if (is_string($value) && is_a($name, DateInterval::class, true)) {
|
||||
return $this->toDateInterval($value);
|
||||
}
|
||||
if ((is_string($value) || is_int($value)) && is_subclass_of($name, \BackedEnum::class)) {
|
||||
return $name::from($value);
|
||||
}
|
||||
if ((is_array($value) || is_string($value)) && is_subclass_of($name, JsonDeserializable::class)) {
|
||||
/** @var JsonDeserializable $instance */
|
||||
$instance = new $name();
|
||||
return $instance->jsonDeserialize($value);
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
protected function fromDateInterval(DateInterval $interval): string {
|
||||
$spec = '';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user