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