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