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)) {
|
if (!empty($this->serializableProperties) && !in_array($key, $this->serializableProperties)) {
|
||||||
continue;
|
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);
|
$type = gettype($this->$key);
|
||||||
|
|
||||||
// Handle JsonDeserializable objects
|
// Handle JsonDeserializable objects
|
||||||
@@ -101,9 +108,10 @@ abstract class JsonSerializableObject implements JsonSerializable, JsonDeseriali
|
|||||||
$enumClass = get_class($this->$key);
|
$enumClass = get_class($this->$key);
|
||||||
$this->$key = $enumClass::from($value);
|
$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 {
|
else {
|
||||||
$this->$key = $value;
|
$this->$key = $this->coerceValue($key, $value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,6 +119,48 @@ abstract class JsonSerializableObject implements JsonSerializable, JsonDeseriali
|
|||||||
return $this;
|
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 {
|
protected function fromDateInterval(DateInterval $interval): string {
|
||||||
$spec = '';
|
$spec = '';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user