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