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

Serializer::deserialize()   D

Complexity

Conditions 19
Paths 17

Size

Total Lines 115
Code Lines 70

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 58
CRAP Score 19.0133

Importance

Changes 12
Bugs 0 Features 0
Metric Value
cc 19
eloc 70
c 12
b 0
f 0
nc 17
nop 2
dl 0
loc 115
ccs 58
cts 60
cp 0.9667
crap 19.0133
rs 4.5166

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\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