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