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) { |
|
|
|
|
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
|
|
|
|
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.