Passed
Push — master ( f1b26f...3d38d1 )
by Julien
03:36
created

Serializer::deserialize()   C

Complexity

Conditions 17
Paths 12

Size

Total Lines 95
Code Lines 58

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 50
CRAP Score 17.0021

Importance

Changes 13
Bugs 1 Features 0
Metric Value
cc 17
eloc 58
c 13
b 1
f 0
nc 12
nop 2
dl 0
loc 95
ccs 50
cts 51
cp 0.9804
crap 17.0021
rs 5.2166

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Exception\InvalidArgumentException;
21
use Symfony\Component\PropertyAccess\PropertyAccess;
22
use Symfony\Component\PropertyAccess\PropertyAccessor;
23
24
/**
25
 * Class Serializer
26
 *
27
 * @author Julien Deniau <[email protected]>
28
 */
29
class Serializer
30
{
31
    /**
32
     * mapping
33
     *
34
     * @var Mapping
35
     */
36
    private $mapping;
37
38
    /**
39
     * @var SdkClient
40
     */
41
    private $sdk;
42
43
    /**
44
     * @var UnitOfWork
45
     */
46
    private $unitOfWork;
47
48
    /**
49
     * @var PropertyAccessor
50
     */
51
    private $propertyAccessor;
52
53
    public function __construct(Mapping $mapping, UnitOfWork $unitOfWork)
54
    {
55 1
        $this->mapping = $mapping;
56 1
        $this->unitOfWork = $unitOfWork;
57 1
        $this->propertyAccessor = PropertyAccess::createPropertyAccessor();
58 1
    }
59
60
    /**
61
     * @required
62
     */
63
    public function setSdk(SdkClient $sdk): self
64
    {
65 1
        $this->sdk = $sdk;
66
67 1
        return $this;
68
    }
69
70
    /**
71
     * serialize entity for POST and PUT
72
     */
73
    public function serialize(
74
        object $entity,
75
        string $modelName,
76
        array $context = []
77
    ): array {
78 1
        $out = $this->recursiveSerialize($entity, $modelName, 0, $context);
79
80 1
        if (is_string($out)) {
81
            throw new \RuntimeException(
82
                'recursiveSerialize should return an array for level 0 of serialization. This should not happen.'
83
            );
84
        }
85
86 1
        return $out;
87
    }
88
89
    public function deserialize(array $data, string $className): object
90
    {
91 1
        $className = $this->resolveRealClassName($data, $className);
92
93 1
        $classMetadata = $this->mapping->getClassMetadata($className);
94
95 1
        $attributeList = $classMetadata->getAttributeList();
96
97 1
        $instance = new $className();
98
99 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...
100 1
            foreach ($attributeList as $attribute) {
101 1
                $key = $attribute->getSerializedKey();
102
103 1
                if (!ArrayHelper::arrayHas($data, $key)) {
104 1
                    continue;
105
                }
106
107 1
                $value = ArrayHelper::arrayGet($data, $key);
108
109 1
                $attributeName = $attribute->getAttributeName();
110 1
                $this->throwIfAttributeIsNotWritable($instance, $attributeName);
111
112 1
                $relation = $classMetadata->getRelation($key);
113 1
                if ($relation) {
114 1
                    if (is_string($value)) {
115 1
                        $value = $this->sdk->createProxy($value);
116 1
                    } elseif (is_array($value)) {
117 1
                        $targetEntity = $relation->getTargetEntity();
118 1
                        $relationClassMetadata = $this->mapping->getClassMetadata(
119 1
                            $targetEntity
120
                        );
121
122 1
                        if ($relation->isManyToOne()) {
123 1
                            $value = $this->deserialize(
124 1
                                $value,
125 1
                                $relationClassMetadata->getModelName()
126
                            );
127
                        } else {
128
                            // One-To-Many association
129 1
                            $list = [];
130 1
                            foreach ($value as $item) {
131 1
                                if (is_string($item)) {
132
                                    $list[] = $this->sdk->createProxy($item);
133 1
                                } elseif (is_array($item)) {
134 1
                                    $list[] = $this->deserialize(
135 1
                                        $item,
136 1
                                        $relationClassMetadata->getModelName()
137
                                    );
138
                                }
139
                            }
140
141 1
                            $value = $list;
142
                        }
143
                    }
144
                }
145
146 1
                if (isset($value)) {
147 1
                    if ('datetime' === $attribute->getType()) {
148 1
                        $this->setDateTimeValue(
149 1
                            $instance,
150
                            $attributeName,
151
                            $value
152
                        );
153
                    } else {
154 1
                        $this->propertyAccessor->setValue(
155 1
                            $instance,
156
                            $attributeName,
157
                            $value
158
                        );
159
                    }
160
                }
161
            }
162
        }
163
164 1
        $classMetadata = $this->getClassMetadata($instance);
165 1
        if ($classMetadata->hasIdentifierAttribute()) {
166 1
            $idGetter = $classMetadata->getIdGetter();
167
168 1
            if ($idGetter) {
169 1
                $callable = [$instance, $idGetter];
170 1
                $identifier = is_callable($callable)
171 1
                    ? call_user_func($callable)
172 1
                    : null;
173
174 1
                if ($identifier) {
175 1
                    $this->unitOfWork->registerClean(
176 1
                        (string) $identifier,
177
                        $instance
178
                    );
179
                }
180
            }
181
        }
182
183 1
        return $instance;
184
    }
185
186
    /**
187
     * If provided class name is abstract (a base class), the real class name (child class)
188
     * may be available in some data fields.
189
     */
190
    private function resolveRealClassName(
191
        array $data,
192
        string $className
193
    ): string {
194 1
        if (!empty($data['@id'])) {
195 1
            $classMetadata = $this->mapping->tryGetClassMetadataById(
196 1
                $data['@id']
197
            );
198
199 1
            if ($classMetadata) {
200 1
                return $classMetadata->getModelName();
201
            }
202
        }
203
204
        // Real class name could also be retrieved from @type property.
205 1
        return $className;
206
    }
207
208
    /**
209
     * @return array|string
210
     */
211
    private function recursiveSerialize(
212
        object $entity,
213
        string $modelName,
214
        int $level = 0,
215
        array $context = []
216
    ) {
217 1
        $classMetadata = $this->mapping->getClassMetadata($modelName);
218
219 1
        if ($level > 0 && empty($context['serializeRelation'])) {
220 1
            if ($classMetadata->hasIdentifierAttribute()) {
221 1
                $tmpId = $entity->{$classMetadata->getIdGetter()}();
222 1
                if ($tmpId) {
223 1
                    return $tmpId;
224
                }
225
            }
226
        }
227
228 1
        $attributeList = $classMetadata->getAttributeList();
229
230 1
        $out = [];
231 1
        if (!empty($attributeList)) {
232 1
            foreach ($attributeList as $attribute) {
233 1
                $method = 'get' . ucfirst($attribute->getAttributeName());
234
235 1
                if ($attribute->isIdentifier() && !$entity->{$method}()) {
236 1
                    continue;
237
                }
238 1
                $relation = $classMetadata->getRelation(
239 1
                    $attribute->getSerializedKey()
240
                );
241
242 1
                $data = $entity->{$method}();
243
244
                if (
245 1
                    null === $data &&
246 1
                    $relation &&
247 1
                    $relation->isManyToOne() &&
248 1
                    $level > 0
249
                ) {
250
                    /*
251
                        We only serialize the root many-to-one relations to prevent, hopefully,
252
                        unlinked and/or duplicated content. For instance, a cart with cartItemList containing
253
                        null values for the cart [{ cart => null, ... }] may lead the creation of
254
                        CartItem entities explicitly bound to a null Cart instead of the created/updated Cart.
255
                     */
256 1
                    continue;
257 1
                } elseif ($data instanceof DateTimeInterface) {
258 1
                    $data = $data->format('c');
259 1
                } elseif (is_object($data) && $data instanceof PhoneNumber) {
260 1
                    $phoneNumberUtil = PhoneNumberUtil::getInstance();
261 1
                    $data = $phoneNumberUtil->format(
262 1
                        $data,
263 1
                        PhoneNumberFormat::INTERNATIONAL
264
                    );
265
                } elseif (
266 1
                    is_object($data) &&
267 1
                    $relation &&
268 1
                    $this->mapping->hasClassMetadata(
269 1
                        $relation->getTargetEntity()
270
                    )
271
                ) {
272 1
                    $relationClassMetadata = $this->mapping->getClassMetadata(
273 1
                        $relation->getTargetEntity()
274
                    );
275
276 1
                    if (!$relationClassMetadata->hasIdentifierAttribute()) {
277 1
                        $data = $this->recursiveSerialize(
278 1
                            $data,
279 1
                            $relation->getTargetEntity(),
280 1
                            $level + 1,
281 1
                            $context
282
                        );
283
                    } else {
284 1
                        $idAttribute = $relationClassMetadata->getIdentifierAttribute();
285
                        $idGetter =
286 1
                            'get' . ucfirst($idAttribute->getAttributeName());
287
288
                        if (
289 1
                            method_exists($data, $idGetter) &&
290 1
                            $data->{$idGetter}()
291
                        ) {
292 1
                            $data = $data->{$idGetter}();
293 1
                        } elseif ($relation->isManyToOne()) {
294 1
                            if ($level > 0) {
295 1
                                continue;
296
                            } else {
297 1
                                throw new SdkException(
298 1
                                    'Case not allowed for now'
299
                                );
300
                            }
301
                        }
302
                    }
303 1
                } elseif (is_array($data)) {
304 1
                    $newData = [];
305 1
                    foreach ($data as $key => $item) {
306 1
                        if ($item instanceof DateTimeInterface) {
307 1
                            $newData[$key] = $item->format('c');
308
                        } elseif (
309 1
                            is_object($item) &&
310 1
                            $relation &&
311 1
                            $this->mapping->hasClassMetadata(
312 1
                                $relation->getTargetEntity()
313
                            )
314
                        ) {
315
                            $serializeRelation =
316 1
                                !empty($context['serializeRelations']) &&
317 1
                                in_array(
318 1
                                    $relation->getSerializedKey(),
319 1
                                    $context['serializeRelations']
320
                                );
321
322 1
                            $newData[$key] = $this->recursiveSerialize(
323 1
                                $item,
324 1
                                $relation->getTargetEntity(),
325 1
                                $level + 1,
326 1
                                ['serializeRelation' => $serializeRelation]
327
                            );
328
                        } else {
329 1
                            $newData[$key] = $item;
330
                        }
331
                    }
332 1
                    $data = $newData;
333
                }
334
335 1
                $key = $attribute->getSerializedKey();
336
337 1
                $out[$key] = $data;
338
            }
339
        }
340
341 1
        return $out;
342
    }
343
344
    private function getClassMetadataFromId(string $id): ?ClassMetadata
345
    {
346
        $key = $this->mapping->getKeyFromId($id);
347
348
        return $this->mapping->getClassMetadataByKey($key);
349
    }
350
351
    private function getClassMetadata(object $entity): ClassMetadata
352
    {
353 1
        return $this->mapping->getClassMetadata(get_class($entity));
354
    }
355
356
    private function throwIfAttributeIsNotWritable(
357
        object $instance,
358
        string $attribute
359
    ): void {
360 1
        if (!$this->propertyAccessor->isWritable($instance, $attribute)) {
361 1
            throw new MissingSetterException(
362 1
                sprintf(
363 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',
364
                    $attribute,
365 1
                    get_class($instance)
366
                )
367
            );
368
        }
369 1
    }
370
371
    private function setDateTimeValue(
372
        object $instance,
373
        string $attributeName,
374
        string $value
375
    ): void {
376
        try {
377 1
            $this->propertyAccessor->setValue(
378 1
                $instance,
379
                $attributeName,
380 1
                new DateTime($value)
381
            );
382 1
        } catch (InvalidArgumentException $e) {
383
            if (
384
                false ===
385 1
                mb_strpos(
386 1
                    $e->getMessage(),
387 1
                    'Expected argument of type "DateTimeImmutable", "DateTime" given'
388
                )
389
            ) {
390
                // not an issue with DateTimeImmutable, then rethrow exception
391
                throw $e;
392
            }
393
394
            // The excepted value is a DateTimeImmutable, so let's do that
395 1
            $this->propertyAccessor->setValue(
396 1
                $instance,
397
                $attributeName,
398 1
                new DateTimeImmutable($value)
399
            );
400
        } catch (\TypeError $e) {
401
            // this `catch` block can be dropped when minimum support of symfony/property-access is 3.4
402
            if (
403
                false ===
404
                mb_strpos(
405
                    $e->getMessage(),
406
                    'must be an instance of DateTimeImmutable, instance of DateTime given'
407
                )
408
            ) {
409
                // not an issue with DateTimeImmutable, then rethrow exception
410
                throw $e;
411
            }
412
413
            // The excepted value is a DateTimeImmutable, so let's do that
414
            $this->propertyAccessor->setValue(
415
                $instance,
416
                $attributeName,
417
                new DateTimeImmutable($value)
418
            );
419
        }
420 1
    }
421
}
422