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

Serializer::throwIfInstanceDoesNotHasSetter()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 6
c 1
b 0
f 0
nc 2
nop 2
dl 0
loc 10
ccs 6
cts 6
cp 1
crap 2
rs 10
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