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

Serializer   D

Complexity

Total Complexity 59

Size/Duplication

Total Lines 334
Duplicated Lines 0 %

Test Coverage

Coverage 96.08%

Importance

Changes 16
Bugs 0 Features 1
Metric Value
eloc 167
c 16
b 0
f 1
dl 0
loc 334
ccs 147
cts 153
cp 0.9608
rs 4.08
wmc 59

9 Methods

Rating   Name   Duplication   Size   Complexity  
A getClassMetadata() 0 3 1
A getClassMetadataFromId() 0 5 1
A setSdk() 0 5 1
A resolveRealClassName() 0 16 3
A serialize() 0 14 2
D recursiveSerialize() 0 131 31
A __construct() 0 5 1
C deserialize() 0 92 17
A throwIfAttributeIsNotWritable() 0 10 2

How to fix   Complexity   

Complex Class

Complex classes like Serializer often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Serializer, and based on these observations, apply Extract Interface, too.

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