Passed
Push — master ( 13c3e7...28eebb )
by Julien
01:28 queued 15s
created

Serializer::resolveRealClassName()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 6
nc 3
nop 2
dl 0
loc 14
ccs 6
cts 6
cp 1
crap 3
rs 9.4285
c 0
b 0
f 0
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|null
31
     */
32
    private $sdk;
33
34
    /**
35
     * @var UnitOfWork
36
     */
37
    private $unitOfWork;
38
39
    /**
40
     * Constructor.
41
     *
42
     * @param Mapping $mapping
43
     */
44
    public function __construct(Mapping $mapping, UnitOfWork $unitOfWork)
45
    {
46 1
        $this->mapping = $mapping;
47 1
        $this->unitOfWork = $unitOfWork;
48 1
    }
49
50
    /**
51
     * setSdk
52
     *
53
     * @param SdkClient $sdk
54
     *
55
     * @return Serializer
56
     */
57
    public function setSdk(SdkClient $sdk)
58
    {
59 1
        $this->sdk = $sdk;
60
61 1
        return $this;
62
    }
63
64
    /**
65
     * serialize entity for POST and PUT
66
     *
67
     * @param object $entity
68
     * @param string $modelName
69
     * @param array  $context
70
     *
71
     * @return array
72
     */
73
    public function serialize($entity, $modelName, $context = [])
74
    {
75 1
        return $this->recursiveSerialize($entity, $modelName, 0, $context);
76
    }
77
78
    /**
79
     * deserialize
80
     *
81
     * @param array  $data
82
     * @param string $className
83
     *
84
     * @return object
85
     */
86
    public function deserialize(array $data, $className)
87
    {
88 1
        $className = $this->resolveRealClassName($data, $className);
89
90 1
        $classMetadata = $this->mapping->getClassMetadata($className);
91 1
        $identifierAttribute = $classMetadata->getIdentifierAttribute();
92 1
        $identifierAttrKey = $identifierAttribute
0 ignored issues
show
introduced by
$identifierAttribute is of type Mapado\RestClientSdk\Mapping\Attribute, thus it always evaluated to true.
Loading history...
Unused Code introduced by
The assignment to $identifierAttrKey is dead and can be removed.
Loading history...
93 1
            ? $identifierAttribute->getSerializedKey()
94 1
            : null;
95
96 1
        $attributeList = $classMetadata->getAttributeList();
97
98 1
        $instance = new $className();
99
100 1
        foreach ($attributeList as $attribute) {
101 1
            $key = $attribute->getSerializedKey();
102
103 1
            if (!ArrayHelper::arrayHas($data, $key)) {
104 1
                continue;
105
            }
106
107 1
            $value = ArrayHelper::arrayGet($data, $key);
108
109 1
            $setter = 'set' . ucfirst($attribute->getAttributeName());
110
111 1
            if (method_exists($instance, $setter)) {
112 1
                $relation = $classMetadata->getRelation($key);
113 1
                if ($relation) {
114 1
                    if (is_string($value)) {
115 1
                        $value = $this->sdk->createProxy($value);
0 ignored issues
show
Bug introduced by
The method createProxy() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

115
                        /** @scrutinizer ignore-call */ 
116
                        $value = $this->sdk->createProxy($value);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
116 1
                    } elseif (is_array($value)) {
117 1
                        $targetEntity = $relation->getTargetEntity();
118 1
                        $relationClassMetadata = $this->mapping->getClassMetadata(
119 1
                            $targetEntity
120
                        );
121
122 1
                        $relationIdentifierAttribute = $relationClassMetadata->getIdentifierAttribute();
123 1
                        $relationIdentifierAttrKey = $relationIdentifierAttribute
0 ignored issues
show
Unused Code introduced by
The assignment to $relationIdentifierAttrKey is dead and can be removed.
Loading history...
124 1
                            ? $relationIdentifierAttribute->getSerializedKey()
125 1
                            : null;
126
127 1
                        if ($relation->isManyToOne()) {
128 1
                            $value = $this->deserialize(
129 1
                                $value,
130 1
                                $relationClassMetadata->getModelName()
131
                            );
132
                        } else {
133
                            // One-To-Many association
134 1
                            $list = [];
135 1
                            foreach ($value as $item) {
136 1
                                if (is_string($item)) {
137
                                    $list[] = $this->sdk->createProxy($item);
138 1
                                } elseif (is_array($item)) {
139 1
                                    $list[] = $this->deserialize(
140 1
                                        $item,
141 1
                                        $relationClassMetadata->getModelName()
142
                                    );
143
                                }
144
                            }
145
146 1
                            $value = $list;
147
                        }
148
                    }
149
                }
150
151 1
                if (isset($value)) {
152 1
                    if ($attribute && 'datetime' === $attribute->getType()) {
153 1
                        $value = new \DateTime($value);
154
                    }
155
156 1
                    $instance->{$setter}($value);
157
                }
158
            }
159
        }
160
161 1
        $idGetter = $this->getClassMetadata($instance)->getIdGetter();
162 1
        $identifier = $idGetter ?? $instance->{$idGetter}();
163 1
        if ($identifier) {
164 1
            $this->unitOfWork->registerClean($identifier, $instance);
165
        }
166
167 1
        return $instance;
168
    }
169
170
    /**
171
     * If provided class name is abstract (a base class), the real class name (child class)
172
     * may be available in some data fields.
173
     *
174
     * @param array  $data
175
     * @param string $className
176
     *
177
     * @return string
178
     */
179
    private function resolveRealClassName(array $data, $className)
180
    {
181 1
        if (!empty($data['@id'])) {
182 1
            $classMetadata = $this->mapping->tryGetClassMetadataById(
183 1
                $data['@id']
184
            );
185
186 1
            if ($classMetadata) {
187 1
                return $classMetadata->getModelName();
188
            }
189
        }
190
191
        // Real class name could also be retrieved from @type property.
192 1
        return $className;
193
    }
194
195
    /**
196
     * recursiveSerialize
197
     *
198
     * @param object $entity
199
     * @param string $modelName
200
     * @param int    $level
201
     * @param array  $context
202
     *
203
     * @return array|mixed
204
     */
205
    private function recursiveSerialize(
206
        $entity,
207
        $modelName,
208
        $level = 0,
209
        $context = []
210
    ) {
211 1
        $classMetadata = $this->mapping->getClassMetadata($modelName);
212
213 1
        if ($level > 0 && empty($context['serializeRelation'])) {
214 1
            $idAttribute = $classMetadata->getIdentifierAttribute();
215 1
            if ($idAttribute) {
0 ignored issues
show
introduced by
$idAttribute is of type Mapado\RestClientSdk\Mapping\Attribute, thus it always evaluated to true.
Loading history...
216 1
                $getter = 'get' . ucfirst($idAttribute->getAttributeName());
217 1
                $tmpId = $entity->{$getter}();
218 1
                if ($tmpId) {
219 1
                    return $tmpId;
220
                }
221
            }
222
        }
223
224 1
        $attributeList = $classMetadata->getAttributeList();
225
226 1
        $out = [];
227 1
        if (!empty($attributeList)) {
228 1
            foreach ($attributeList as $attribute) {
229 1
                $method = 'get' . ucfirst($attribute->getAttributeName());
230
231 1
                if ($attribute->isIdentifier() && !$entity->{$method}()) {
232 1
                    continue;
233
                }
234 1
                $relation = $classMetadata->getRelation(
235 1
                    $attribute->getSerializedKey()
236
                );
237
238 1
                $data = $entity->{$method}();
239
240
                if (
241 1
                    null === $data &&
242 1
                    $relation &&
243 1
                    $relation->isManyToOne() &&
244 1
                    $level > 0
245
                ) {
246
                    /*
247
                        We only serialize the root many-to-one relations to prevent, hopefully,
248
                        unlinked and/or duplicated content. For instance, a cart with cartItemList containing
249
                        null values for the cart [{ cart => null, ... }] may lead the creation of
250
                        CartItem entities explicitly bound to a null Cart instead of the created/updated Cart.
251
                     */
252 1
                    continue;
253 1
                } elseif ($data instanceof \DateTime) {
254 1
                    $data = $data->format('c');
255 1
                } elseif (is_object($data) && $data instanceof PhoneNumber) {
256 1
                    $phoneNumberUtil = PhoneNumberUtil::getInstance();
257 1
                    $data = $phoneNumberUtil->format(
258 1
                        $data,
259 1
                        PhoneNumberFormat::INTERNATIONAL
260
                    );
261
                } elseif (
262 1
                    is_object($data) &&
263 1
                    $relation &&
264 1
                    $this->mapping->hasClassMetadata(
265 1
                        $relation->getTargetEntity()
266
                    )
267
                ) {
268 1
                    $idAttribute = $this->mapping->getClassMetadata(
269 1
                        $relation->getTargetEntity()
270 1
                    )->getIdentifierAttribute();
271
272 1
                    if (!$idAttribute) {
273 1
                        $data = $this->recursiveSerialize(
274 1
                            $data,
275 1
                            $relation->getTargetEntity(),
276 1
                            $level + 1,
277 1
                            $context
278
                        );
279
                    } else {
280
                        $idGetter =
281 1
                            'get' . ucfirst($idAttribute->getAttributeName());
282
283
                        if (
284 1
                            method_exists($data, $idGetter) &&
285 1
                            $data->{$idGetter}()
286
                        ) {
287 1
                            $data = $data->{$idGetter}();
288 1
                        } elseif ($relation->isManyToOne()) {
289 1
                            if ($level > 0) {
290 1
                                continue;
291
                            } else {
292 1
                                throw new SdkException(
293 1
                                    'Case not allowed for now'
294
                                );
295
                            }
296
                        }
297
                    }
298 1
                } elseif (is_array($data)) {
299 1
                    $newData = [];
300 1
                    foreach ($data as $key => $item) {
301 1
                        if ($item instanceof \DateTime) {
302 1
                            $newData[$key] = $item->format('c');
303
                        } elseif (
304 1
                            is_object($item) &&
305 1
                            $relation &&
306 1
                            $this->mapping->hasClassMetadata(
307 1
                                $relation->getTargetEntity()
308
                            )
309
                        ) {
310
                            $serializeRelation =
311 1
                                !empty($context['serializeRelations']) &&
312 1
                                in_array(
313 1
                                    $relation->getSerializedKey(),
314 1
                                    $context['serializeRelations']
315
                                );
316
317 1
                            $newData[$key] = $this->recursiveSerialize(
318 1
                                $item,
319 1
                                $relation->getTargetEntity(),
320 1
                                $level + 1,
321 1
                                ['serializeRelation' => $serializeRelation]
322
                            );
323
                        } else {
324 1
                            $newData[$key] = $item;
325
                        }
326
                    }
327 1
                    $data = $newData;
328
                }
329
330 1
                $key = $attribute->getSerializedKey();
331
332 1
                $out[$key] = $data;
333
            }
334
        }
335
336 1
        return $out;
337
    }
338
339
    /**
340
     * getClassMetadataFromId
341
     *
342
     * @param string $id
343
     *
344
     * @return ClassMetadata|null
345
     */
346
    private function getClassMetadataFromId($id)
347
    {
348
        $key = $this->mapping->getKeyFromId($id);
349
        $classMetadata = $this->mapping->getClassMetadataByKey($key);
350
351
        return $classMetadata;
352
    }
353
354
    private function getClassMetadata($entity)
355
    {
356 1
        return $this->mapping->getClassMetadata(get_class($entity));
357
    }
358
}
359