object)) { $this->object = (object) $this->object; } } /** * @throws \InvalidArgumentException * @throws \RuntimeException * @throws \ReflectionException **/ public function castTo(string $classDest, array $constructParams = [], array $mappings = []): object { $this->mappings ??= $mappings; if (! class_exists($classDest)) { throw new \InvalidArgumentException(sprintf('Class %s does not exist.', $classDest)); } $sourceObj = $this->object; $classRef = new \ReflectionClass($classDest); $properties = $classRef->getProperties(); if ( ! empty($reflectedConstructParams = $classRef->getConstructor()?->getParameters() ?? []) && empty($constructParams) ) { foreach ($reflectedConstructParams as $param) { if (isset($sourceObj->{$param->getName()})) { $constructParams[] = $sourceObj->{$param->getName()}; continue; } if ($param->isOptional()) { $constructParams[] = $param->getDefaultValue(); continue; } throw new \InvalidArgumentException(sprintf('Missing construct parameter %s.', $param->getName())); } } $returnObj = build(new $classDest(...$constructParams)); foreach ($properties as $property) { $name = $property->getName(); if (! isset($sourceObj->$name)) { if ($property->hasDefaultValue()) { $returnObj->set($name, $property->getDefaultValue()); continue; } throw new \RuntimeException(sprintf('Property %s does not exist in source object.', $name)); } try { $type = collect($mappings)->firstOrFail(fn ($mapping, $key) => in_array($key, [$name, '*'])); } catch (\Illuminate\Support\ItemNotFoundException) { $type = ($reflectionType = $property->getType()) instanceof \ReflectionNamedType ? $reflectionType->getName() : null; } $returnObj->set($name, match (true) { enum_exists($type) => self::castEnum($sourceObj->$name, $type), $type !== 'stdClass' && class_exists($type) => (new self($sourceObj->$name))->castTo( classDest: $type, mappings: $this->getMatchingMappings($mappings, $name), ), in_array($type, ['array', 'object', 'stdClass']) => $this->handleIterator( $sourceObj->$name, $this->getMatchingMappings($mappings, $name) ), is_null($type) || $type == 'mixed' => $sourceObj->$name, default => self::castSimple($sourceObj->$name, $type), }); } return $returnObj->get(); } /** * @throws \RuntimeException * @throws \InvalidArgumentException **/ public static function castSimple(mixed $value, string $simpleType): mixed { if ( is_null($castedValue = match ($simpleType) { 'int', 'integer' => filter_var($value, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE), 'float' => filter_var($value, FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE), 'string', 'str' => is_array($value) || (is_object($value) && ! method_exists($value, '__toString')) ? null : (string) $value, 'bool', 'boolean' => filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE), 'object', 'stdClass' => is_array($value) ? (object) $value : null, 'array' => is_object($value) || is_array($value) ? (array) $value : null, default => throw new \InvalidArgumentException(sprintf('%s is not a simple type.', $simpleType)), }) ) { throw new \RuntimeException(sprintf('Could not cast value to type %s.', $simpleType)); } return $castedValue; } /** * Cast to backed enum * **/ public static function castEnum(mixed $value, string $enumClass): mixed { // For non-backed enums, iterate and match by name. if (is_string($value)) { foreach ($enumClass::cases() as $case) { if ($case->name === $value) { return $case; } } } // For backed enums, try to get the case by value if ( is_subclass_of($enumClass, \BackedEnum::class) && ($enum = $enumClass::tryFrom($value) ?? false) ) { return $enum; } throw new \InvalidArgumentException(sprintf('Value cannot be casted to %s.', $enumClass)); } /** * Casts a string value into a datetime object. * * @param string $value The value to be casted into a datetime object. * @return \Carbon\CarbonImmutable The datetime object. * * @throws \InvalidArgumentException If the value is not a valid datetime string. **/ public static function castDateTime(string $value) { if (is_string($value)) { return dtHelper()->parseDbDateTime($value); } } protected function handleIterator(iterable $iterator, array $mappings = []): array|object { $result = is_object($iterator) ? new \stdClass : []; foreach ($iterator as $key => $value) { if (is_numeric($key)) { $type = $mappings['*'] ?? false; } else { if ($type = preg_match('/\<[a-zA-Z0-9\\\\]+\>/', $key)) { $key = preg_replace('/\<[a-zA-Z0-9\\\\]+\>/', '', $key); } else { $type = $mappings[$key] ?? $mappings['*'] ?? false; } } $value = match (true) { $type && enum_exists($type) => self::castEnum($value, $type), $type && class_exists($type) => (new self($value))->castTo($type), $type && in_array($type, ['string', 'str', 'int', 'integer', 'float', 'bool', 'boolean']) => self::castSimple($value, $type), $type && in_array($type, ['array', 'object', 'stdClass']), is_array($value), is_object($value) => $this->{__FUNCTION__}($value, $this->getMatchingMappings($mappings, (string) $key)), default => $value, }; if (is_object($result)) { $result->$key = $value; } else { $result[$key] = $value; } } return $result; } protected function getMatchingMappings(array $mappings, string $propName): array { return collect($mappings) ->filter(fn ($mapping, $key) => Str::startsWith($key, "$propName.") || Str::startsWith($key, '*.')) ->mapWithKeys(fn ($mapping, $key) => [Str::after($key, '.') => $mapping]) ->all(); } }