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