Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.41% covered (success)
97.41%
113 / 116
66.67% covered (warning)
66.67%
4 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Serializer
97.41% covered (success)
97.41%
113 / 116
66.67% covered (warning)
66.67%
4 / 6
32
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
 create
100.00% covered (success)
100.00%
37 / 37
100.00% covered (success)
100.00%
1 / 1
1
 normalize
96.77% covered (success)
96.77%
30 / 31
0.00% covered (danger)
0.00%
0 / 1
16
 denormalizeOnMethodCall
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 denormalizeNewObject
93.33% covered (success)
93.33%
28 / 30
0.00% covered (danger)
0.00%
0 / 1
11.04
 denormalizeOnExistingObject
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2namespace Apie\Serializer;
3
4use Apie\Core\Context\ApieContext;
5use Apie\Core\Exceptions\InvalidTypeException;
6use Apie\Core\Lists\ItemHashmap;
7use Apie\Core\Lists\ItemList;
8use Apie\Core\Metadata\Concerns\UseContextKey;
9use Apie\Core\Metadata\MetadataFactory;
10use Apie\Core\Utils\ConverterUtils;
11use Apie\Core\ValueObjects\Utils;
12use Apie\Serializer\Context\ApieSerializerContext;
13use Apie\Serializer\Context\NormalizeChildGroup;
14use Apie\Serializer\Exceptions\ValidationException;
15use Apie\Serializer\FieldFilters\FieldFilterInterface;
16use Apie\Serializer\FieldFilters\NoFiltering;
17use Apie\Serializer\Interfaces\DenormalizerInterface;
18use Apie\Serializer\Interfaces\NormalizerInterface;
19use Apie\Serializer\Lists\NormalizerList;
20use Apie\Serializer\Normalizers\AliasDenormalizer;
21use Apie\Serializer\Normalizers\BcMathNormalizer;
22use Apie\Serializer\Normalizers\BooleanNormalizer;
23use Apie\Serializer\Normalizers\DateTimeNormalizer;
24use Apie\Serializer\Normalizers\DateTimeZoneNormalizer;
25use Apie\Serializer\Normalizers\DomNormalizer;
26use Apie\Serializer\Normalizers\DoNotChangeFileNormalizer;
27use Apie\Serializer\Normalizers\DurationNormalizer;
28use Apie\Serializer\Normalizers\EnumNormalizer;
29use Apie\Serializer\Normalizers\FfiCdataNormalizer;
30use Apie\Serializer\Normalizers\FfiCtypeNormalizer;
31use Apie\Serializer\Normalizers\FloatNormalizer;
32use Apie\Serializer\Normalizers\GmpNormalizer;
33use Apie\Serializer\Normalizers\IdentifierNormalizer;
34use Apie\Serializer\Normalizers\IntegerNormalizer;
35use Apie\Serializer\Normalizers\ItemListNormalizer;
36use Apie\Serializer\Normalizers\PaginatedResultNormalizer;
37use Apie\Serializer\Normalizers\PermissionListNormalizer;
38use Apie\Serializer\Normalizers\PolymorphicObjectNormalizer;
39use Apie\Serializer\Normalizers\ReflectionClassNormalizer;
40use Apie\Serializer\Normalizers\ReflectionMethodNormalizer;
41use Apie\Serializer\Normalizers\ReflectionTypeNormalizer;
42use Apie\Serializer\Normalizers\RelationNormalizer;
43use Apie\Serializer\Normalizers\ResourceNormalizer;
44use Apie\Serializer\Normalizers\SelfNormalizer;
45use Apie\Serializer\Normalizers\SimpleXMLElementNormalizer;
46use Apie\Serializer\Normalizers\StreamBucketNormalizer;
47use Apie\Serializer\Normalizers\StringableCompositeValueObjectNormalizer;
48use Apie\Serializer\Normalizers\StringNormalizer;
49use Apie\Serializer\Normalizers\TranslationNormalizer;
50use Apie\Serializer\Normalizers\UnionDenormalizer;
51use Apie\Serializer\Normalizers\UploadedFileNormalizer;
52use Apie\Serializer\Normalizers\UriNormalizer;
53use Apie\Serializer\Normalizers\ValueObjectNormalizer;
54use Apie\Serializer\Relations\EmbedRelationInterface;
55use Apie\Serializer\Relations\NoRelationEmbedded;
56use Exception;
57use Psr\Http\Message\UploadedFileInterface;
58use ReflectionClass;
59use ReflectionMethod;
60
61class Serializer
62{
63    use UseContextKey;
64
65    public function __construct(private NormalizerList $normalizers)
66    {
67    }
68
69    /**
70     * @param iterable<int, NormalizerInterface|DenormalizerInterface> $additionalNormalizers
71     */
72    public static function create(iterable $additionalNormalizers = []): self
73    {
74        return new self(new NormalizerList([
75            ...$additionalNormalizers,
76
77            new AliasDenormalizer(),
78            new PaginatedResultNormalizer(),
79            new DoNotChangeFileNormalizer(),
80            new DomNormalizer(),
81            new SelfNormalizer(),
82
83            new TranslationNormalizer(),
84            new PermissionListNormalizer(),
85            new RelationNormalizer(),
86            new UploadedFileNormalizer(),
87            new IdentifierNormalizer(),
88            new StringableCompositeValueObjectNormalizer(),
89            new PolymorphicObjectNormalizer(),
90            new DateTimeNormalizer(),
91            new DurationNormalizer(),
92            new UriNormalizer(),
93            new FfiCdataNormalizer(),
94            new FfiCtypeNormalizer(),
95            new SimpleXMLElementNormalizer(),
96            new GmpNormalizer(),
97            new BcMathNormalizer(),
98            new DateTimeZoneNormalizer(),
99            new StreamBucketNormalizer(),
100            new ResourceNormalizer(),
101            new EnumNormalizer(),
102            new ValueObjectNormalizer(),
103            new StringNormalizer(),
104            new IntegerNormalizer(),
105            new FloatNormalizer(),
106            new BooleanNormalizer(),
107            new ItemListNormalizer(),
108            new ReflectionClassNormalizer(),
109            new ReflectionTypeNormalizer(),
110            new ReflectionMethodNormalizer(),
111            new UnionDenormalizer(),
112        ]));
113    }
114
115    public function normalize(mixed $object, ApieContext $apieContext, bool $forceDefaultNormalization = false): string|int|float|bool|ItemList|ItemHashmap|null
116    {
117        $serializerContext = new ApieSerializerContext($this, $apieContext);
118        if (!$forceDefaultNormalization) {
119            foreach ($this->normalizers->iterateOverNormalizers() as $normalizer) {
120                if ($normalizer->supportsNormalization($object, $serializerContext)) {
121                    return $normalizer->normalize($object, $serializerContext);
122                }
123            }
124        }
125
126        $fieldFilter = $apieContext->getContext(FieldFilterInterface::class, false) ? : new NoFiltering();
127        $relationEmbedder = $apieContext->getContext(EmbedRelationInterface::class, false) ? : new NoRelationEmbedded();
128
129        if (is_array($object)) {
130            $count = 0;
131            $returnValue = [];
132            $isList = true;
133            // TODO: should a field filter have effect on arrays?
134            foreach ($object as $key => $value) {
135                if ($key === $count) {
136                    $count++;
137                } else {
138                    $isList = false;
139                }
140                $returnValue[$key] = $serializerContext->normalizeChildElement($key, $value);
141            }
142            return $isList ? new ItemList($returnValue) : new ItemHashmap($returnValue);
143        }
144        if (!is_object($object)) {
145            $type = get_debug_type($object);
146            if ($type === 'resource' || str_starts_with($type, 'resource ')) {
147                throw new InvalidTypeException($object, 'primitive');
148            }
149            return $object;
150        }
151        $metadata = MetadataFactory::getResultMetadata(new ReflectionClass($object), $apieContext);
152        $returnValue = [];
153        foreach ($metadata->getHashmap()->filterOnContext($apieContext, getters: true) as $fieldName => $metadata) {
154            if ($metadata->isField() && $fieldFilter->isFiltered($fieldName)) {
155                $returnValue[$fieldName] = $serializerContext->normalizeChildElement(
156                    $fieldName,
157                    $metadata->getValue($object, $apieContext)
158                );
159            }
160        }
161        return new ItemHashmap($returnValue);
162    }
163
164    public function denormalizeOnMethodCall(string|int|float|bool|ItemList|ItemHashmap|array|null|UploadedFileInterface $input, ?object $object, ReflectionMethod $method, ApieContext $apieContext): mixed
165    {
166        $serializerContext = new ApieSerializerContext($this, $apieContext);
167        try {
168            $arguments = $serializerContext->denormalizeFromMethod($input, $method);
169        } catch (Exception $error) {
170            throw ValidationException::createFromArray(['' => $error]);
171        }
172        return $method->invokeArgs($object, $arguments);
173    }
174
175    public function denormalizeNewObject(string|int|float|bool|ItemList|ItemHashmap|array|null|UploadedFileInterface $object, string $desiredType, ApieContext $apieContext): mixed
176    {
177        if (is_array($object)) {
178            $isList = false;
179            if ($desiredType === 'mixed') {
180                $isList = true;
181                $count = 0;
182                foreach (array_keys($object) as $key) {
183                    if ($key === $count) {
184                        $count++;
185                    } else {
186                        $isList = false;
187                        break;
188                    }
189                }
190            }
191            $object = $isList ? new ItemList($object) : new ItemHashmap($object);
192        }
193        if ($desiredType === 'mixed') {
194            return $object;
195        }
196        $serializerContext = new ApieSerializerContext($this, $apieContext);
197        foreach ($this->normalizers->iterateOverDenormalizers() as $denormalizer) {
198            if ($denormalizer->supportsDenormalization($object, $desiredType, $serializerContext)) {
199                return $denormalizer->denormalize($object, $desiredType, $serializerContext);
200            }
201        }
202        $refl = ConverterUtils::toReflectionClass($desiredType);
203        if (!$refl || !$refl->isInstantiable()) {
204            throw new InvalidTypeException($desiredType, 'a instantiable object');
205        }
206        $metadata = MetadataFactory::getCreationMetadata(
207            $refl,
208            $apieContext
209        );
210        $group = new NormalizeChildGroup(
211            $serializerContext,
212            $metadata
213        );
214        $normalizedData = $group->buildNormalizedData($refl, Utils::toArray($object));
215        return $normalizedData->createNewObject();
216    }
217
218    public function denormalizeOnExistingObject(ItemHashmap $object, object $existingObject, ApieContext $apieContext): mixed
219    {
220        $refl = new ReflectionClass($existingObject);
221        $serializerContext = new ApieSerializerContext($this, $apieContext);
222        $metadata = MetadataFactory::getModificationMetadata(
223            $refl,
224            $apieContext
225        );
226        $group = new NormalizeChildGroup(
227            $serializerContext,
228            $metadata
229        );
230        $normalizedData = $group->buildNormalizedData($refl, Utils::toArray($object));
231        return $normalizedData->modifyExistingObject($existingObject);
232    }
233}