Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.08% covered (success)
96.08%
98 / 102
66.67% covered (warning)
66.67%
4 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Serializer
96.08% covered (success)
96.08%
98 / 102
66.67% covered (warning)
66.67%
4 / 6
31
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
93.33% covered (success)
93.33%
28 / 30
0.00% covered (danger)
0.00%
0 / 1
15.07
 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            if (in_array(get_debug_type($object), ['resource', 'resource (closed)'])) {
118                throw new InvalidTypeException($object, 'primitive');
119            }
120            return $object;
121        }
122        $metadata = MetadataFactory::getResultMetadata(new ReflectionClass($object), $apieContext);
123        $returnValue = [];
124
125        foreach ($metadata->getHashmap()->filterOnContext($apieContext, getters: true) as $fieldName => $metadata) {
126            if ($metadata->isField() && $fieldFilter->isFiltered($fieldName)) {
127                $returnValue[$fieldName] = $serializerContext->normalizeChildElement(
128                    $fieldName,
129                    $metadata->getValue($object, $apieContext)
130                );
131            }
132        }
133        return new ItemHashmap($returnValue);
134    }
135
136    public function denormalizeOnMethodCall(string|int|float|bool|ItemList|ItemHashmap|array|null|UploadedFileInterface $input, ?object $object, ReflectionMethod $method, ApieContext $apieContext): mixed
137    {
138        $serializerContext = new ApieSerializerContext($this, $apieContext);
139        try {
140            $arguments = $serializerContext->denormalizeFromMethod($input, $method);
141        } catch (Exception $error) {
142            throw ValidationException::createFromArray(['' => $error]);
143        }
144        return $method->invokeArgs($object, $arguments);
145    }
146
147    public function denormalizeNewObject(string|int|float|bool|ItemList|ItemHashmap|array|null|UploadedFileInterface $object, string $desiredType, ApieContext $apieContext): mixed
148    {
149        if (is_array($object)) {
150            $isList = false;
151            if ($desiredType === 'mixed') {
152                $isList = true;
153                $count = 0;
154                foreach (array_keys($object) as $key) {
155                    if ($key === $count) {
156                        $count++;
157                    } else {
158                        $isList = false;
159                        break;
160                    }
161                }
162            }
163            $object = $isList ? new ItemList($object) : new ItemHashmap($object);
164        }
165        if ($desiredType === 'mixed') {
166            return $object;
167        }
168        $serializerContext = new ApieSerializerContext($this, $apieContext);
169        foreach ($this->normalizers->iterateOverDenormalizers() as $denormalizer) {
170            if ($denormalizer->supportsDenormalization($object, $desiredType, $serializerContext)) {
171                return $denormalizer->denormalize($object, $desiredType, $serializerContext);
172            }
173        }
174        $refl = ConverterUtils::toReflectionClass($desiredType);
175        if (!$refl || !$refl->isInstantiable()) {
176            throw new InvalidTypeException($desiredType, 'a instantiable object');
177        }
178        $metadata = MetadataFactory::getCreationMetadata(
179            $refl,
180            $apieContext
181        );
182        $group = new NormalizeChildGroup(
183            $serializerContext,
184            $metadata
185        );
186        $normalizedData = $group->buildNormalizedData($refl, Utils::toArray($object));
187        return $normalizedData->createNewObject();
188    }
189
190    public function denormalizeOnExistingObject(ItemHashmap $object, object $existingObject, ApieContext $apieContext): mixed
191    {
192        $refl = new ReflectionClass($existingObject);
193        $serializerContext = new ApieSerializerContext($this, $apieContext);
194        $metadata = MetadataFactory::getModificationMetadata(
195            $refl,
196            $apieContext
197        );
198        $group = new NormalizeChildGroup(
199            $serializerContext,
200            $metadata
201        );
202        $normalizedData = $group->buildNormalizedData($refl, Utils::toArray($object));
203        return $normalizedData->modifyExistingObject($existingObject);
204    }
205}