Completed
Pull Request — master (#88)
by Julien
02:57
created

Serializer   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 311
Duplicated Lines 0 %

Test Coverage

Coverage 95.74%

Importance

Changes 0
Metric Value
eloc 154
dl 0
loc 311
ccs 135
cts 141
cp 0.9574
rs 4.5599
c 0
b 0
f 0
wmc 58

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