Completed
Pull Request — master (#91)
by Julien
03:40
created

Serializer::deserialize()   C

Complexity

Conditions 17
Paths 12

Size

Total Lines 88
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 49
CRAP Score 17.0023

Importance

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