serializableProperties)) { $vars = array_filter($vars, function($key) { return in_array($key, $this->serializableProperties); }, ARRAY_FILTER_USE_KEY); } // Process each property for special types foreach ($vars as $key => $value) { // Skip internal control properties if (in_array($key, $this->nonSerializableProperties)) { unset($vars[$key]); continue; } // Handle DateTimeInterface (DateTime/DateTimeImmutable) if ($value instanceof DateTimeInterface) { $vars[$key] = $value->format($this->dateTimeFormat); } // Handle DateTimeZone elseif ($value instanceof DateTimeZone) { $vars[$key] = $value->getName(); } // Handle DateInterval elseif ($value instanceof DateInterval) { $vars[$key] = $this->fromDateInterval($value); } // Handle backed enums elseif ($value instanceof \BackedEnum) { $vars[$key] = $value->value; } // Handle JsonSerializable objects elseif ($value instanceof JsonSerializable) { $vars[$key] = $value->jsonSerialize(); } } return $vars; } public function jsonDeserialize(array|string $data): static { if (is_string($data)) { $data = json_decode($data, true); } foreach ($data as $key => $value) { if (property_exists($this, $key)) { // Skip internal control properties if (in_array($key, $this->nonSerializableProperties)) { continue; } // Check if property should be deserialized (if serializableProperties is set) 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 if ($type === 'object' && $this->$key instanceof JsonDeserializable) { $this->$key = $this->$key->jsonDeserialize($value); } // Handle DateTimeInterface (DateTime/DateTimeImmutable) elseif ($type === 'object' && $this->$key instanceof DateTimeInterface) { $this->$key = new \DateTimeImmutable($value); } // Handle DateTimeZone elseif ($type === 'object' && $this->$key instanceof DateTimeZone) { $this->$key = new DateTimeZone($value); } // Handle DateInterval elseif ($type === 'object' && $this->$key instanceof DateInterval) { $this->$key = $this->toDateInterval($value); } // Handle backed enums elseif ($type === 'object' && $this->$key instanceof \BackedEnum) { $enumClass = get_class($this->$key); $this->$key = $enumClass::from($value); } // Handle regular values, coercing by declared type when the // current value is null and carries no instance to key off else { $this->$key = $this->coerceValue($key, $value); } } } 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 = ''; // Handle negative intervals if ($interval->invert === 1) { $spec = '-'; } $spec .= 'P'; if ($interval->y > 0) $spec .= $interval->y . 'Y'; if ($interval->m > 0) $spec .= $interval->m . 'M'; if ($interval->d > 0) $spec .= $interval->d . 'D'; $timePart = ''; if ($interval->h > 0) $timePart .= $interval->h . 'H'; if ($interval->i > 0) $timePart .= $interval->i . 'M'; if ($interval->s > 0) $timePart .= $interval->s . 'S'; if (!empty($timePart)) { $spec .= 'T' . $timePart; } // Handle edge case of zero duration if ($spec === 'P' || $spec === '-P') { $spec = 'PT0S'; } return $spec; } protected function toDateInterval(string $value): DateInterval { $isNegative = false; // Check for negative interval if (str_starts_with($value, '-')) { $isNegative = true; $value = substr($value, 1); } // Create the interval $interval = new DateInterval($value); // Set invert property for negative intervals if ($isNegative) { $interval->invert = 1; } return $interval; } }