Passed
Push — master ( a73c0f...44755a )
by Julien
03:04
created

Serializer   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 314
Duplicated Lines 0 %

Test Coverage

Coverage 95.77%

Importance

Changes 15
Bugs 0 Features 1
Metric Value
eloc 156
c 15
b 0
f 1
dl 0
loc 314
ccs 136
cts 142
cp 0.9577
rs 4.5599
wmc 58

8 Methods

Rating   Name   Duplication   Size   Complexity  
A setSdk() 0 5 1
A serialize() 0 14 2
A __construct() 0 4 1
A getClassMetadata() 0 3 1
A getClassMetadataFromId() 0 5 1
A resolveRealClassName() 0 16 3
D recursiveSerialize() 0 131 31
D deserialize() 0 90 18

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