Passed
Push — master ( 0e744e...957281 )
by Julien
01:07 queued 11s
created

Serializer::deserialize()   C

Complexity

Conditions 17
Paths 12

Size

Total Lines 92
Code Lines 55

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 52
CRAP Score 17.0146

Importance

Changes 11
Bugs 0 Features 0
Metric Value
cc 17
eloc 55
c 11
b 0
f 0
nc 12
nop 2
dl 0
loc 92
ccs 52
cts 54
cp 0.963
crap 17.0146
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 libphonenumber\PhoneNumber;
8
use libphonenumber\PhoneNumberFormat;
9
use libphonenumber\PhoneNumberUtil;
10
use Mapado\RestClientSdk\Exception\MissingSetterException;
11
use Mapado\RestClientSdk\Exception\SdkException;
12
use Mapado\RestClientSdk\Helper\ArrayHelper;
13
use Mapado\RestClientSdk\Mapping;
14
use Mapado\RestClientSdk\Mapping\ClassMetadata;
15
use Mapado\RestClientSdk\SdkClient;
16
use Mapado\RestClientSdk\UnitOfWork;
17
use Symfony\Component\PropertyAccess\PropertyAccess;
18
use Symfony\Component\PropertyAccess\PropertyAccessor;
19
20
/**
21
 * Class Serializer
22
 *
23
 * @author Julien Deniau <[email protected]>
24
 */
25
class Serializer
26
{
27
    /**
28
     * mapping
29
     *
30
     * @var Mapping
31
     */
32
    private $mapping;
33
34
    /**
35
     * @var SdkClient
36
     */
37
    private $sdk;
38
39
    /**
40
     * @var UnitOfWork
41
     */
42
    private $unitOfWork;
43
44
    /**
45
     * @var PropertyAccessor
46
     */
47
    private $propertyAccessor;
48
49
    public function __construct(Mapping $mapping, UnitOfWork $unitOfWork)
50
    {
51 1
        $this->mapping = $mapping;
52 1
        $this->unitOfWork = $unitOfWork;
53 1
        $this->propertyAccessor = PropertyAccess::createPropertyAccessor();
54 1
    }
55
56
    /**
57
     * @required
58
     */
59
    public function setSdk(SdkClient $sdk): self
60
    {
61 1
        $this->sdk = $sdk;
62
63 1
        return $this;
64
    }
65
66
    /**
67
     * serialize entity for POST and PUT
68
     */
69
    public function serialize(
70
        object $entity,
71
        string $modelName,
72
        array $context = []
73
    ): array {
74 1
        $out = $this->recursiveSerialize($entity, $modelName, 0, $context);
75
76 1
        if (is_string($out)) {
77
            throw new \RuntimeException(
78
                'recursiveSerialize should return an array for level 0 of serialization. This should not happen.'
79
            );
80
        }
81
82 1
        return $out;
83
    }
84
85
    public function deserialize(array $data, string $className): object
86
    {
87 1
        $className = $this->resolveRealClassName($data, $className);
88
89 1
        $classMetadata = $this->mapping->getClassMetadata($className);
90
91 1
        $attributeList = $classMetadata->getAttributeList();
92
93 1
        $instance = new $className();
94
95 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...
96 1
            foreach ($attributeList as $attribute) {
97 1
                $key = $attribute->getSerializedKey();
98
99 1
                if (!ArrayHelper::arrayHas($data, $key)) {
100 1
                    continue;
101
                }
102
103 1
                $value = ArrayHelper::arrayGet($data, $key);
104
105 1
                $attributeName = $attribute->getAttributeName();
106 1
                $this->throwIfAttributeIsNotWritable($instance, $attributeName);
107
108 1
                $relation = $classMetadata->getRelation($key);
109 1
                if ($relation) {
110 1
                    if (is_string($value)) {
111 1
                        $value = $this->sdk->createProxy($value);
112 1
                    } elseif (is_array($value)) {
113 1
                        $targetEntity = $relation->getTargetEntity();
114 1
                        $relationClassMetadata = $this->mapping->getClassMetadata(
115 1
                            $targetEntity
116
                        );
117
118 1
                        if ($relation->isManyToOne()) {
119 1
                            $value = $this->deserialize(
120 1
                                $value,
121 1
                                $relationClassMetadata->getModelName()
122
                            );
123
                        } else {
124
                            // One-To-Many association
125 1
                            $list = [];
126 1
                            foreach ($value as $item) {
127 1
                                if (is_string($item)) {
128
                                    var_dump(__METHOD__, $item);
0 ignored issues
show
Security Debugging Code introduced by
var_dump(__METHOD__, $item) looks like debug code. Are you sure you do not want to remove it?
Loading history...
129
                                    $list[] = $this->sdk->createProxy($item);
130 1
                                } elseif (is_array($item)) {
131 1
                                    $list[] = $this->deserialize(
132 1
                                        $item,
133 1
                                        $relationClassMetadata->getModelName()
134
                                    );
135
                                }
136
                            }
137
138 1
                            $value = $list;
139
                        }
140
                    }
141
                }
142
143 1
                if (isset($value)) {
144 1
                    if ('datetime' === $attribute->getType()) {
145 1
                        $value = new \DateTime($value);
146
                    }
147
148 1
                    $this->propertyAccessor->setValue(
149 1
                        $instance,
150 1
                        $attributeName,
151 1
                        $value
152
                    );
153
                }
154
            }
155
        }
156
157 1
        $classMetadata = $this->getClassMetadata($instance);
158 1
        if ($classMetadata->hasIdentifierAttribute()) {
159 1
            $idGetter = $classMetadata->getIdGetter();
160
161 1
            if ($idGetter) {
162 1
                $callable = [$instance, $idGetter];
163 1
                $identifier = is_callable($callable)
164 1
                    ? call_user_func($callable)
165 1
                    : null;
166
167 1
                if ($identifier) {
168 1
                    $this->unitOfWork->registerClean(
169 1
                        (string) $identifier,
170 1
                        $instance
171
                    );
172
                }
173
            }
174
        }
175
176 1
        return $instance;
177
    }
178
179
    /**
180
     * If provided class name is abstract (a base class), the real class name (child class)
181
     * may be available in some data fields.
182
     */
183
    private function resolveRealClassName(
184
        array $data,
185
        string $className
186
    ): string {
187 1
        if (!empty($data['@id'])) {
188 1
            $classMetadata = $this->mapping->tryGetClassMetadataById(
189 1
                $data['@id']
190
            );
191
192 1
            if ($classMetadata) {
193 1
                return $classMetadata->getModelName();
194
            }
195
        }
196
197
        // Real class name could also be retrieved from @type property.
198 1
        return $className;
199
    }
200
201
    /**
202
     * @return array|string
203
     */
204
    private function recursiveSerialize(
205
        object $entity,
206
        string $modelName,
207
        int $level = 0,
208
        array $context = []
209
    ) {
210 1
        $classMetadata = $this->mapping->getClassMetadata($modelName);
211
212 1
        if ($level > 0 && empty($context['serializeRelation'])) {
213 1
            if ($classMetadata->hasIdentifierAttribute()) {
214 1
                $tmpId = $entity->{$classMetadata->getIdGetter()}();
215 1
                if ($tmpId) {
216 1
                    return $tmpId;
217
                }
218
            }
219
        }
220
221 1
        $attributeList = $classMetadata->getAttributeList();
222
223 1
        $out = [];
224 1
        if (!empty($attributeList)) {
225 1
            foreach ($attributeList as $attribute) {
226 1
                $method = 'get' . ucfirst($attribute->getAttributeName());
227
228 1
                if ($attribute->isIdentifier() && !$entity->{$method}()) {
229 1
                    continue;
230
                }
231 1
                $relation = $classMetadata->getRelation(
232 1
                    $attribute->getSerializedKey()
233
                );
234
235 1
                $data = $entity->{$method}();
236
237
                if (
238 1
                    null === $data &&
239 1
                    $relation &&
240 1
                    $relation->isManyToOne() &&
241 1
                    $level > 0
242
                ) {
243
                    /*
244
                        We only serialize the root many-to-one relations to prevent, hopefully,
245
                        unlinked and/or duplicated content. For instance, a cart with cartItemList containing
246
                        null values for the cart [{ cart => null, ... }] may lead the creation of
247
                        CartItem entities explicitly bound to a null Cart instead of the created/updated Cart.
248
                     */
249 1
                    continue;
250 1
                } elseif ($data instanceof \DateTime) {
251 1
                    $data = $data->format('c');
252 1
                } elseif (is_object($data) && $data instanceof PhoneNumber) {
253 1
                    $phoneNumberUtil = PhoneNumberUtil::getInstance();
254 1
                    $data = $phoneNumberUtil->format(
255 1
                        $data,
256 1
                        PhoneNumberFormat::INTERNATIONAL
257
                    );
258
                } elseif (
259 1
                    is_object($data) &&
260 1
                    $relation &&
261 1
                    $this->mapping->hasClassMetadata(
262 1
                        $relation->getTargetEntity()
263
                    )
264
                ) {
265 1
                    $relationClassMetadata = $this->mapping->getClassMetadata(
266 1
                        $relation->getTargetEntity()
267
                    );
268
269 1
                    if (!$relationClassMetadata->hasIdentifierAttribute()) {
270 1
                        $data = $this->recursiveSerialize(
271 1
                            $data,
272 1
                            $relation->getTargetEntity(),
273 1
                            $level + 1,
274 1
                            $context
275
                        );
276
                    } else {
277 1
                        $idAttribute = $relationClassMetadata->getIdentifierAttribute();
278
                        $idGetter =
279 1
                            'get' . ucfirst($idAttribute->getAttributeName());
280
281
                        if (
282 1
                            method_exists($data, $idGetter) &&
283 1
                            $data->{$idGetter}()
284
                        ) {
285 1
                            $data = $data->{$idGetter}();
286 1
                        } elseif ($relation->isManyToOne()) {
287 1
                            if ($level > 0) {
288 1
                                continue;
289
                            } else {
290 1
                                throw new SdkException(
291 1
                                    'Case not allowed for now'
292
                                );
293
                            }
294
                        }
295
                    }
296 1
                } elseif (is_array($data)) {
297 1
                    $newData = [];
298 1
                    foreach ($data as $key => $item) {
299 1
                        if ($item instanceof \DateTime) {
300 1
                            $newData[$key] = $item->format('c');
301
                        } elseif (
302 1
                            is_object($item) &&
303 1
                            $relation &&
304 1
                            $this->mapping->hasClassMetadata(
305 1
                                $relation->getTargetEntity()
306
                            )
307
                        ) {
308
                            $serializeRelation =
309 1
                                !empty($context['serializeRelations']) &&
310 1
                                in_array(
311 1
                                    $relation->getSerializedKey(),
312 1
                                    $context['serializeRelations']
313
                                );
314
315 1
                            $newData[$key] = $this->recursiveSerialize(
316 1
                                $item,
317 1
                                $relation->getTargetEntity(),
318 1
                                $level + 1,
319 1
                                ['serializeRelation' => $serializeRelation]
320
                            );
321
                        } else {
322 1
                            $newData[$key] = $item;
323
                        }
324
                    }
325 1
                    $data = $newData;
326
                }
327
328 1
                $key = $attribute->getSerializedKey();
329
330 1
                $out[$key] = $data;
331
            }
332
        }
333
334 1
        return $out;
335
    }
336
337
    private function getClassMetadataFromId(string $id): ?ClassMetadata
338
    {
339
        $key = $this->mapping->getKeyFromId($id);
340
341
        return $this->mapping->getClassMetadataByKey($key);
342
    }
343
344
    private function getClassMetadata(object $entity): ClassMetadata
345
    {
346 1
        return $this->mapping->getClassMetadata(get_class($entity));
347
    }
348
349
    private function throwIfAttributeIsNotWritable(
350
        object $instance,
351
        string $attribute
352
    ): void {
353 1
        if (!$this->propertyAccessor->isWritable($instance, $attribute)) {
354 1
            throw new MissingSetterException(
355 1
                sprintf(
356 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',
357 1
                    $attribute,
358 1
                    get_class($instance)
359
                )
360
            );
361
        }
362 1
    }
363
}
364