Passed
Push — master ( 355206...f1b26f )
by Julien
03:04
created

Serializer::setSdk()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 5
ccs 2
cts 2
cp 1
crap 1
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Mapado\RestClientSdk\Model;
6
7
use DateTime;
8
use DateTimeImmutable;
9
use DateTimeInterface;
10
use libphonenumber\PhoneNumber;
11
use libphonenumber\PhoneNumberFormat;
12
use libphonenumber\PhoneNumberUtil;
13
use Mapado\RestClientSdk\Exception\MissingSetterException;
14
use Mapado\RestClientSdk\Exception\SdkException;
15
use Mapado\RestClientSdk\Helper\ArrayHelper;
16
use Mapado\RestClientSdk\Mapping;
17
use Mapado\RestClientSdk\Mapping\ClassMetadata;
18
use Mapado\RestClientSdk\SdkClient;
19
use Mapado\RestClientSdk\UnitOfWork;
20
use Symfony\Component\PropertyAccess\PropertyAccess;
21
use Symfony\Component\PropertyAccess\PropertyAccessor;
22
23
/**
24
 * Class Serializer
25
 *
26
 * @author Julien Deniau <[email protected]>
27
 */
28
class Serializer
29
{
30
    /**
31
     * mapping
32
     *
33
     * @var Mapping
34
     */
35
    private $mapping;
36
37
    /**
38
     * @var SdkClient
39
     */
40
    private $sdk;
41
42
    /**
43
     * @var UnitOfWork
44
     */
45
    private $unitOfWork;
46
47
    /**
48
     * @var PropertyAccessor
49
     */
50
    private $propertyAccessor;
51
52
    public function __construct(Mapping $mapping, UnitOfWork $unitOfWork)
53
    {
54 1
        $this->mapping = $mapping;
55 1
        $this->unitOfWork = $unitOfWork;
56 1
        $this->propertyAccessor = PropertyAccess::createPropertyAccessor();
57 1
    }
58
59
    /**
60
     * @required
61
     */
62
    public function setSdk(SdkClient $sdk): self
63
    {
64 1
        $this->sdk = $sdk;
65
66 1
        return $this;
67
    }
68
69
    /**
70
     * serialize entity for POST and PUT
71
     */
72
    public function serialize(
73
        object $entity,
74
        string $modelName,
75
        array $context = []
76
    ): array {
77 1
        $out = $this->recursiveSerialize($entity, $modelName, 0, $context);
78
79 1
        if (is_string($out)) {
80
            throw new \RuntimeException(
81
                'recursiveSerialize should return an array for level 0 of serialization. This should not happen.'
82
            );
83
        }
84
85 1
        return $out;
86
    }
87
88
    public function deserialize(array $data, string $className): object
89
    {
90 1
        $className = $this->resolveRealClassName($data, $className);
91
92 1
        $classMetadata = $this->mapping->getClassMetadata($className);
93
94 1
        $attributeList = $classMetadata->getAttributeList();
95
96 1
        $instance = new $className();
97
98 1
        if ($attributeList) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $attributeList of type Mapado\RestClientSdk\Mapping\Attribute[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
99 1
            foreach ($attributeList as $attribute) {
100 1
                $key = $attribute->getSerializedKey();
101
102 1
                if (!ArrayHelper::arrayHas($data, $key)) {
103 1
                    continue;
104
                }
105
106 1
                $value = ArrayHelper::arrayGet($data, $key);
107
108 1
                $attributeName = $attribute->getAttributeName();
109 1
                $this->throwIfAttributeIsNotWritable($instance, $attributeName);
110
111 1
                $relation = $classMetadata->getRelation($key);
112 1
                if ($relation) {
113 1
                    if (is_string($value)) {
114 1
                        $value = $this->sdk->createProxy($value);
115 1
                    } elseif (is_array($value)) {
116 1
                        $targetEntity = $relation->getTargetEntity();
117 1
                        $relationClassMetadata = $this->mapping->getClassMetadata(
118 1
                            $targetEntity
119
                        );
120
121 1
                        if ($relation->isManyToOne()) {
122 1
                            $value = $this->deserialize(
123 1
                                $value,
124 1
                                $relationClassMetadata->getModelName()
125
                            );
126
                        } else {
127
                            // One-To-Many association
128 1
                            $list = [];
129 1
                            foreach ($value as $item) {
130 1
                                if (is_string($item)) {
131
                                    $list[] = $this->sdk->createProxy($item);
132 1
                                } elseif (is_array($item)) {
133 1
                                    $list[] = $this->deserialize(
134 1
                                        $item,
135 1
                                        $relationClassMetadata->getModelName()
136
                                    );
137
                                }
138
                            }
139
140 1
                            $value = $list;
141
                        }
142
                    }
143
                }
144
145 1
                if (isset($value)) {
146 1
                    if ('datetime' === $attribute->getType()) {
147
                        try {
148 1
                            $this->propertyAccessor->setValue(
149 1
                                $instance,
150
                                $attributeName,
151 1
                                new DateTime($value)
152
                            );
153 1
                        } catch (\Exception $e) {
154
                            if (
155
                                false ===
156 1
                                mb_strpos(
157 1
                                    $e->getMessage(),
158 1
                                    'Expected argument of type "DateTimeImmutable", "DateTime" given'
159
                                )
160
                            ) {
161
                                // not an issue with DateTimeImmutable, then rethrow exception
162
                                throw $e;
163
                            }
164
165
                            // The excepted value is a DateTimeImmutable, so let's do that
166 1
                            $this->propertyAccessor->setValue(
167 1
                                $instance,
168
                                $attributeName,
169 1
                                new DateTimeImmutable($value)
170
                            );
171
                        }
172
                    } else {
173 1
                        $this->propertyAccessor->setValue(
174 1
                            $instance,
175
                            $attributeName,
176
                            $value
177
                        );
178
                    }
179
                }
180
            }
181
        }
182
183 1
        $classMetadata = $this->getClassMetadata($instance);
184 1
        if ($classMetadata->hasIdentifierAttribute()) {
185 1
            $idGetter = $classMetadata->getIdGetter();
186
187 1
            if ($idGetter) {
188 1
                $callable = [$instance, $idGetter];
189 1
                $identifier = is_callable($callable)
190 1
                    ? call_user_func($callable)
191 1
                    : null;
192
193 1
                if ($identifier) {
194 1
                    $this->unitOfWork->registerClean(
195 1
                        (string) $identifier,
196
                        $instance
197
                    );
198
                }
199
            }
200
        }
201
202 1
        return $instance;
203
    }
204
205
    /**
206
     * If provided class name is abstract (a base class), the real class name (child class)
207
     * may be available in some data fields.
208
     */
209
    private function resolveRealClassName(
210
        array $data,
211
        string $className
212
    ): string {
213 1
        if (!empty($data['@id'])) {
214 1
            $classMetadata = $this->mapping->tryGetClassMetadataById(
215 1
                $data['@id']
216
            );
217
218 1
            if ($classMetadata) {
219 1
                return $classMetadata->getModelName();
220
            }
221
        }
222
223
        // Real class name could also be retrieved from @type property.
224 1
        return $className;
225
    }
226
227
    /**
228
     * @return array|string
229
     */
230
    private function recursiveSerialize(
231
        object $entity,
232
        string $modelName,
233
        int $level = 0,
234
        array $context = []
235
    ) {
236 1
        $classMetadata = $this->mapping->getClassMetadata($modelName);
237
238 1
        if ($level > 0 && empty($context['serializeRelation'])) {
239 1
            if ($classMetadata->hasIdentifierAttribute()) {
240 1
                $tmpId = $entity->{$classMetadata->getIdGetter()}();
241 1
                if ($tmpId) {
242 1
                    return $tmpId;
243
                }
244
            }
245
        }
246
247 1
        $attributeList = $classMetadata->getAttributeList();
248
249 1
        $out = [];
250 1
        if (!empty($attributeList)) {
251 1
            foreach ($attributeList as $attribute) {
252 1
                $method = 'get' . ucfirst($attribute->getAttributeName());
253
254 1
                if ($attribute->isIdentifier() && !$entity->{$method}()) {
255 1
                    continue;
256
                }
257 1
                $relation = $classMetadata->getRelation(
258 1
                    $attribute->getSerializedKey()
259
                );
260
261 1
                $data = $entity->{$method}();
262
263
                if (
264 1
                    null === $data &&
265 1
                    $relation &&
266 1
                    $relation->isManyToOne() &&
267 1
                    $level > 0
268
                ) {
269
                    /*
270
                        We only serialize the root many-to-one relations to prevent, hopefully,
271
                        unlinked and/or duplicated content. For instance, a cart with cartItemList containing
272
                        null values for the cart [{ cart => null, ... }] may lead the creation of
273
                        CartItem entities explicitly bound to a null Cart instead of the created/updated Cart.
274
                     */
275 1
                    continue;
276 1
                } elseif ($data instanceof DateTimeInterface) {
277 1
                    $data = $data->format('c');
278 1
                } elseif (is_object($data) && $data instanceof PhoneNumber) {
279 1
                    $phoneNumberUtil = PhoneNumberUtil::getInstance();
280 1
                    $data = $phoneNumberUtil->format(
281 1
                        $data,
282 1
                        PhoneNumberFormat::INTERNATIONAL
283
                    );
284
                } elseif (
285 1
                    is_object($data) &&
286 1
                    $relation &&
287 1
                    $this->mapping->hasClassMetadata(
288 1
                        $relation->getTargetEntity()
289
                    )
290
                ) {
291 1
                    $relationClassMetadata = $this->mapping->getClassMetadata(
292 1
                        $relation->getTargetEntity()
293
                    );
294
295 1
                    if (!$relationClassMetadata->hasIdentifierAttribute()) {
296 1
                        $data = $this->recursiveSerialize(
297 1
                            $data,
298 1
                            $relation->getTargetEntity(),
299 1
                            $level + 1,
300 1
                            $context
301
                        );
302
                    } else {
303 1
                        $idAttribute = $relationClassMetadata->getIdentifierAttribute();
304
                        $idGetter =
305 1
                            'get' . ucfirst($idAttribute->getAttributeName());
306
307
                        if (
308 1
                            method_exists($data, $idGetter) &&
309 1
                            $data->{$idGetter}()
310
                        ) {
311 1
                            $data = $data->{$idGetter}();
312 1
                        } elseif ($relation->isManyToOne()) {
313 1
                            if ($level > 0) {
314 1
                                continue;
315
                            } else {
316 1
                                throw new SdkException(
317 1
                                    'Case not allowed for now'
318
                                );
319
                            }
320
                        }
321
                    }
322 1
                } elseif (is_array($data)) {
323 1
                    $newData = [];
324 1
                    foreach ($data as $key => $item) {
325 1
                        if ($item instanceof DateTimeInterface) {
326 1
                            $newData[$key] = $item->format('c');
327
                        } elseif (
328 1
                            is_object($item) &&
329 1
                            $relation &&
330 1
                            $this->mapping->hasClassMetadata(
331 1
                                $relation->getTargetEntity()
332
                            )
333
                        ) {
334
                            $serializeRelation =
335 1
                                !empty($context['serializeRelations']) &&
336 1
                                in_array(
337 1
                                    $relation->getSerializedKey(),
338 1
                                    $context['serializeRelations']
339
                                );
340
341 1
                            $newData[$key] = $this->recursiveSerialize(
342 1
                                $item,
343 1
                                $relation->getTargetEntity(),
344 1
                                $level + 1,
345 1
                                ['serializeRelation' => $serializeRelation]
346
                            );
347
                        } else {
348 1
                            $newData[$key] = $item;
349
                        }
350
                    }
351 1
                    $data = $newData;
352
                }
353
354 1
                $key = $attribute->getSerializedKey();
355
356 1
                $out[$key] = $data;
357
            }
358
        }
359
360 1
        return $out;
361
    }
362
363
    private function getClassMetadataFromId(string $id): ?ClassMetadata
364
    {
365
        $key = $this->mapping->getKeyFromId($id);
366
367
        return $this->mapping->getClassMetadataByKey($key);
368
    }
369
370
    private function getClassMetadata(object $entity): ClassMetadata
371
    {
372 1
        return $this->mapping->getClassMetadata(get_class($entity));
373
    }
374
375
    private function throwIfAttributeIsNotWritable(
376
        object $instance,
377
        string $attribute
378
    ): void {
379 1
        if (!$this->propertyAccessor->isWritable($instance, $attribute)) {
380 1
            throw new MissingSetterException(
381 1
                sprintf(
382 1
                    'Property %s is not writable for class %s. Please make it writable. You can check the property-access documentation here : https://symfony.com/doc/current/components/property_access.html#writing-to-objects',
383
                    $attribute,
384 1
                    get_class($instance)
385
                )
386
            );
387
        }
388 1
    }
389
}
390