Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.03% covered (success)
91.03%
71 / 78
72.73% covered (warning)
72.73%
8 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApieSerializerContext
91.03% covered (success)
91.03%
71 / 78
72.73% covered (warning)
72.73%
8 / 11
42.22
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 denormalizeFromTypehint
87.50% covered (warning)
87.50%
14 / 16
0.00% covered (danger)
0.00%
0 / 1
13.33
 denormalizeFromParameter
80.00% covered (warning)
80.00%
16 / 20
0.00% covered (danger)
0.00%
0 / 1
9.65
 denormalizeFromMethod
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
8
 denormalizeChildElement
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 normalizeAgain
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 normalizeChildElement
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 createChildContext
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 visit
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getParentState
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getContext
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2namespace Apie\Serializer\Context;
3
4use Apie\Core\Attributes\Context;
5use Apie\Core\Context\ApieContext;
6use Apie\Core\ContextConstants;
7use Apie\Core\Exceptions\IndexNotFoundException;
8use Apie\Core\Exceptions\InvalidTypeException;
9use Apie\Core\Lists\ItemHashmap;
10use Apie\Core\Lists\ItemList;
11use Apie\Core\Metadata\Concerns\UseContextKey;
12use Apie\Core\TypeUtils;
13use Apie\Core\Utils\ConverterUtils;
14use Apie\Serializer\Exceptions\ValidationException;
15use Apie\Serializer\FieldFilters\FieldFilterInterface;
16use Apie\Serializer\FieldFilters\NoFiltering;
17use Apie\Serializer\Relations\EmbedRelationInterface;
18use Apie\Serializer\Relations\NoRelationEmbedded;
19use Apie\Serializer\Serializer;
20use Apie\TypeConverter\Exceptions\CanNotConvertObjectToUnionException;
21use Exception;
22use ReflectionIntersectionType;
23use ReflectionMethod;
24use ReflectionNamedType;
25use ReflectionParameter;
26use ReflectionType;
27use ReflectionUnionType;
28
29final class ApieSerializerContext
30{
31    use UseContextKey;
32
33    private ?ApieSerializerContext $parentState = null;
34
35    public function __construct(private Serializer $serializer, private ApieContext $apieContext)
36    {
37    }
38
39    public function denormalizeFromTypehint(mixed $input, ReflectionType|null $typehint): mixed
40    {
41        if ($input === null && (!$typehint || $typehint->allowsNull())) {
42            return null;
43        }
44        if ($typehint instanceof ReflectionIntersectionType) {
45            throw new InvalidTypeException($typehint, 'ReflectionNamedType|ReflectionUnionType|null');
46        }
47        if ($typehint instanceof ReflectionUnionType) {
48            $exceptions = [];
49            foreach ($typehint->getTypes() as $type) {
50                try {
51                    return $this->serializer->denormalizeNewObject($input, $type->getName(), $this->apieContext);
52                } catch (Exception $exception) {
53                    $exceptions[$type->getName()] = $exception;
54                }
55            }
56            throw new CanNotConvertObjectToUnionException($input, $exceptions, $typehint);
57        }
58        if ($typehint instanceof ReflectionNamedType) {
59            // edge case, should probably work differently then this
60            if ($input === '' && $typehint->allowsNull() && !TypeUtils::allowEmptyString($typehint) && $this->apieContext->hasContext(ContextConstants::CMS)) {
61                return null;
62            }
63            return $this->serializer->denormalizeNewObject($input, $typehint->getName(), $this->apieContext);
64        }
65        return $this->serializer->denormalizeNewObject($input, 'mixed', $this->apieContext);
66    }
67
68    public function denormalizeFromParameter(ItemHashmap $input, ReflectionParameter $parameter): mixed
69    {
70        $key = $parameter->getName();
71        $type = $parameter->getType();
72        if ($parameter->getAttributes(Context::class)) {
73            $contextKey = $this->getContextKey($this->apieContext, $parameter, false);
74            $contextValue = $this->apieContext->getContext(
75                $contextKey,
76                !$parameter->isDefaultValueAvailable()
77            ) ?? $parameter->getDefaultValue();
78            if ($type) {
79                return ConverterUtils::dynamicCast($contextValue, $type);
80            }
81            return $contextValue;
82        }
83        if (!$parameter->isOptional() && !isset($input[$key])) {
84            throw new IndexNotFoundException($key);
85        }
86        $defaultValue = null;
87        if ($parameter->isDefaultValueAvailable()) {
88            $defaultValue = $parameter->getDefaultValue();
89        }
90        if ($type === null || ((string) $type) === 'mixed' || !isset($input[$key])) {
91            return $input[$key] ?? $defaultValue;
92        }
93        $newContext = $this->visit($key);
94        return $newContext->denormalizeFromTypehint($input[$key], $type);
95    }
96
97    public function denormalizeFromMethod(mixed $input, ReflectionMethod $method): array
98    {
99        if (! ($input instanceof ItemHashmap)) {
100            $input = $this->serializer->denormalizeNewObject($input, ItemHashmap::class, $this->apieContext);
101        }
102        $result = [];
103        $validationErrors = [];
104        // this construction is for performance reasons as it maintains only one try catch context.
105        $todo = $method->getParameters();
106        while (!empty($todo)) {
107            try {
108                while (!empty($todo)) {
109                    $parameter = array_shift($todo);
110                    if ($parameter->isVariadic()) {
111                        foreach (($input[$parameter->name] ?? []) as $variadicValue) {
112                            $copy = $input;
113                            $copy[$parameter->name] = $variadicValue;
114                            $result[] = $this->denormalizeFromParameter($copy, $parameter);
115                        }
116                    } else {
117                        $result[] = $this->denormalizeFromParameter($input, $parameter);
118                    }
119                }
120            } catch (Exception $error) {
121                assert(isset($parameter));
122                $validationErrors[$parameter->name] = $error;
123            }
124        }
125        if (!empty($validationErrors)) {
126            throw ValidationException::createFromArray($validationErrors);
127        }
128        return $result;
129    }
130
131    public function denormalizeChildElement(string $key, mixed $input, string $desiredType): mixed
132    {
133        $newContext = $this->createChildContext($key);
134        return $this->serializer->denormalizeNewObject($input, $desiredType, $newContext);
135    }
136
137    public function normalizeAgain(mixed $object, bool $forceDefaultNormalization = false): string|int|float|bool|ItemList|ItemHashmap|null
138    {
139        return $this->serializer->normalize($object, $this->apieContext, $forceDefaultNormalization);
140    }
141
142    public function normalizeChildElement(string $key, mixed $object): string|int|float|bool|ItemList|ItemHashmap|null
143    {
144        $newContext = $this->createChildContext($key);
145        return $this->serializer->normalize($object, $newContext);
146    }
147
148    private function createChildContext(string $key): ApieContext
149    {
150        $context = $this->apieContext;
151        $hierarchy = [];
152        if ($context->hasContext('hierarchy')) {
153            $hierarchy = $context->getContext('hierarchy');
154        }
155        $hierarchy[] = $key;
156        $fieldFilter = $context->getContext(FieldFilterInterface::class, false) ? : new NoFiltering();
157        $context = $context->withContext(FieldFilterInterface::class, $fieldFilter->followField($key));
158
159        $relationFilter = $context->getContext(EmbedRelationInterface::class, false) ? : new NoRelationEmbedded();
160        $context = $context->withContext(EmbedRelationInterface::class, $relationFilter->followField($key));
161
162        return $context->withContext('hierarchy', $hierarchy);
163    }
164
165    public function visit(string $key): self
166    {
167        $childContext = $this->createChildContext($key);
168        $res = new ApieSerializerContext($this->serializer, $childContext);
169        $res->parentState = $this;
170        return $res;
171    }
172
173    public function getParentState(): ?self
174    {
175        return $this->parentState;
176    }
177
178    public function getContext(): ApieContext
179    {
180        return $this->apieContext;
181    }
182}