3 declare(strict_types=1);
5 namespace CuyZ\Valinor\Normalizer\Transformer;
9 use CuyZ\Valinor\Definition\Attributes;
10 use CuyZ\Valinor\Definition\Repository\ClassDefinitionRepository;
11 use CuyZ\Valinor\Normalizer\Exception\CircularReferenceFoundDuringNormalization;
12 use CuyZ\Valinor\Normalizer\Exception\TypeUnhandledByNormalizer;
13 use CuyZ\Valinor\Type\Types\NativeClassType;
14 use DateTimeInterface;
21 use function array_map;
22 use function array_reverse;
23 use function get_object_vars;
24 use function is_array;
25 use function is_iterable;
28 final class RecursiveTransformer
30 public function __construct(
31 private ClassDefinitionRepository $classDefinitionRepository,
32 private ValueTransformersHandler $valueTransformers,
33 private KeyTransformersHandler $keyTransformers,
34 /** @var list<callable> */
35 private array $transformers,
36 /** @var list<class-string> */
37 private array $transformerAttributes,
41 * @return array<mixed>|scalar|null
43 public function transform(mixed $value): mixed
45 return $this->doTransform($value, new WeakMap()); // @phpstan-ignore-line
49 * @param WeakMap<object, true> $references
50 * @param list<object> $attributes
51 * @return iterable<mixed>|scalar|null
53 private function doTransform(mixed $value, WeakMap $references, array $attributes = []): mixed
55 if (is_object($value)) {
56 if (isset($references[$value])) {
57 throw new CircularReferenceFoundDuringNormalization($value);
60 // @infection-ignore-all
61 $references[$value] = true;
64 if ($this->transformers === [] && $this->transformerAttributes === []) {
65 return $this->defaultTransformer($value, $references);
68 if ($this->transformerAttributes !== [] && is_object($value)) {
69 $classAttributes = $this->classDefinitionRepository->for(NativeClassType::for($value::class))->attributes();
71 $attributes = [...$attributes, ...$classAttributes];
74 return $this->valueTransformers->transform(
78 fn (mixed $value) => $this->defaultTransformer($value, $references),
83 * @param WeakMap<object, true> $references
84 * @return iterable<mixed>|scalar|null
86 private function defaultTransformer(mixed $value, WeakMap $references): mixed
88 if ($value === null) {
92 if (is_scalar($value)) {
96 if (is_object($value) && ! $value instanceof Closure && ! $value instanceof Generator) {
97 if ($value instanceof UnitEnum) {
98 return $value instanceof BackedEnum ? $value->value : $value->name;
101 if ($value instanceof DateTimeInterface) {
102 return $value->format('Y-m-d\\TH:i:s.uP'); // RFC 3339
105 if ($value::class === stdClass::class) {
107 fn (mixed $value) => $this->doTransform($value, $references),
112 $values = (fn () => get_object_vars($this))->call($value);
114 // @infection-ignore-all
115 if (PHP_VERSION_ID < 8_01_00) {
116 // In PHP 8.1, behavior changed for `get_object_vars` function:
117 // the sorting order was taking children properties first, now
118 // it takes parents properties first. This code is a temporary
119 // workaround to keep the same behavior in PHP 8.0 and later
123 $parents = array_reverse(class_parents($value));
124 $parents[] = $value::class;
126 foreach ($parents as $parent) {
127 foreach ((new ReflectionClass($parent))->getProperties() as $property) {
128 if (! isset($values[$property->name])) {
132 $sorted[$property->name] = $values[$property->name];
141 $class = $this->classDefinitionRepository->for(NativeClassType::for($value::class));
143 foreach ($values as $key => $subValue) {
144 $attributes = $this->filterAttributes($class->properties()->get($key)->attributes());
146 $key = $this->keyTransformers->transformKey($key, $attributes);
148 $transformed[$key] = $this->doTransform($subValue, $references, $attributes);
154 if (is_iterable($value)) {
155 if (is_array($value)) {
157 fn (mixed $value) => $this->doTransform($value, $references),
162 return (function () use ($value, $references) {
163 foreach ($value as $key => $item) {
164 yield $key => $this->doTransform($item, $references);
169 throw new TypeUnhandledByNormalizer($value);
173 * @return list<object>
175 private function filterAttributes(Attributes $attributes): array
177 $filteredAttributes = [];
179 foreach ($attributes as $attribute) {
180 foreach ($this->transformerAttributes as $transformerAttribute) {
181 if ($attribute instanceof $transformerAttribute) {
182 $filteredAttributes[] = $attribute;
188 return $filteredAttributes;