AbstractTransfer::toArray()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 2
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Copyright © 2016-present Spryker Systems GmbH. All rights reserved.
5
 * Use of this software requires acceptance of the Evaluation License Agreement. See LICENSE file.
6
 */
7
8
namespace Spryker\Shared\Kernel\Transfer;
9
10
use ArrayAccess;
11
use ArrayObject;
12
use Countable;
13
use Exception;
14
use InvalidArgumentException;
15
use Serializable;
16
use Spryker\Service\UtilEncoding\Model\Json;
17
use Spryker\Shared\Kernel\Transfer\Exception\ArrayAccessReadyOnlyException;
18
use Spryker\Shared\Kernel\Transfer\Exception\InvalidStrictTypeException;
19
use Spryker\Shared\Kernel\Transfer\Exception\NullValueException;
20
use Spryker\Shared\Kernel\Transfer\Exception\RequiredTransferPropertyException;
21
use Spryker\Shared\Kernel\Transfer\Exception\TransferUnserializationException;
22
23
/**
24
 * @implements \ArrayAccess<int|string, mixed>
25
 */
26
abstract class AbstractTransfer implements TransferInterface, Serializable, ArrayAccess
27
{
28
    /**
29
     * @var array<string, bool>
30
     */
31
    protected $modifiedProperties = [];
32
33
    /**
34
     * @var array<string, array<string, mixed>>
35
     */
36
    protected $transferMetadata = [];
37
38
    /**
39
     * @var array<string, string>
40
     */
41
    protected $transferPropertyNameMap = [];
42
43
    public function __construct()
44
    {
45
        $this->initCollectionProperties();
46
    }
47
48
    /**
49
     * @param bool $isRecursive
50
     * @param bool $camelCasedKeys Set to true for camelCased keys, defaults to under_scored keys.
51
     *
52
     * @return array
53
     */
54
    public function toArray($isRecursive = true, $camelCasedKeys = false)
55
    {
56
        return $this->propertiesToArray($this->getPropertyNames(), $isRecursive, 'toArray', $camelCasedKeys);
57
    }
58
59
    /**
60
     * @param bool $isRecursive
61
     * @param bool $camelCasedKeys
62
     *
63
     * @return array
64
     */
65
    public function modifiedToArray($isRecursive = true, $camelCasedKeys = false)
66
    {
67
        return $this->propertiesToArray(array_keys($this->modifiedProperties), $isRecursive, 'modifiedToArray', $camelCasedKeys);
68
    }
69
70
    /**
71
     * @param string $propertyName
72
     *
73
     * @return bool
74
     */
75
    public function isPropertyModified($propertyName)
76
    {
77
        return isset($this->modifiedProperties[$propertyName]);
78
    }
79
80
    /**
81
     * @param string $propertyName
82
     *
83
     * @return bool
84
     */
85
    public function isPropertyStrict(string $propertyName): bool
86
    {
87
        if (!isset($this->transferMetadata[$propertyName]['is_strict'])) {
88
            return false;
89
        }
90
91
        return $this->transferMetadata[$propertyName]['is_strict'];
92
    }
93
94
    /**
95
     * @return void
96
     */
97
    protected function initCollectionProperties()
98
    {
99
        foreach ($this->transferMetadata as $property => $metaData) {
100
            if ($metaData['is_collection'] && $this->$property === null) {
101
                $this->$property = new ArrayObject();
102
            }
103
        }
104
    }
105
106
    /**
107
     * @param array<string> $properties
108
     * @param bool $isRecursive
109
     * @param string $childConvertMethodName
110
     * @param bool $camelCasedKeys
111
     *
112
     * @return array<string, mixed>
113
     */
114
    private function propertiesToArray(array $properties, $isRecursive, $childConvertMethodName, $camelCasedKeys = false)
115
    {
116
        $values = [];
117
118
        foreach ($properties as $property) {
119
            $value = $this->$property;
120
121
            $arrayKey = $this->getArrayKey($property, $camelCasedKeys);
122
123
            if (is_object($value) && $isRecursive) {
124
                if ($value instanceof TransferInterface) {
125
                    $values[$arrayKey] = $value->$childConvertMethodName($isRecursive, $camelCasedKeys);
126
127
                    continue;
128
                }
129
130
                if ($this->transferMetadata[$property]['is_collection'] && ($value instanceof Countable) && count($value) >= 1) {
131
                    $values = $this->addValuesToCollection($value, $values, $arrayKey, $isRecursive, $childConvertMethodName, $camelCasedKeys);
132
133
                    continue;
134
                }
135
            }
136
137
            $values[$arrayKey] = $value;
138
        }
139
140
        return $values;
141
    }
142
143
    /**
144
     * @param string $propertyName
145
     * @param bool $camelCasedKeys
146
     *
147
     * @return string
148
     */
149
    protected function getArrayKey(string $propertyName, bool $camelCasedKeys): string
150
    {
151
        if ($camelCasedKeys) {
152
            return $propertyName;
153
        }
154
155
        return $this->transferMetadata[$propertyName]['name_underscore'];
156
    }
157
158
    /**
159
     * @return array
160
     */
161
    protected function getPropertyNames(): array
162
    {
163
        return array_keys($this->transferMetadata);
164
    }
165
166
    /**
167
     * @param array<string, mixed> $data
168
     * @param bool $ignoreMissingProperty
169
     *
170
     * @return $this
171
     */
172
    public function fromArray(array $data, $ignoreMissingProperty = false)
173
    {
174
        foreach ($data as $property => $value) {
175
            if ($this->hasProperty($property, $ignoreMissingProperty) === false) {
176
                continue;
177
            }
178
179
            $property = $this->transferPropertyNameMap[$property];
180
181
            if ($this->transferMetadata[$property]['is_collection']) {
182
                $elementType = $this->transferMetadata[$property]['type'];
183
                $value = $this->processArrayObject($elementType, $value, $ignoreMissingProperty);
184
            } elseif ($this->transferMetadata[$property]['is_transfer']) {
185
                $value = $this->initializeNestedTransferObject($property, $value, $ignoreMissingProperty);
186
187
                if ($value !== null && $this->isPropertyStrict($property)) {
188
                    $this->assertInstanceOfTransfer($property, $value);
189
                }
190
            }
191
192
            $this->$property = $value;
193
            $this->modifiedProperties[$property] = true;
194
        }
195
196
        return $this;
197
    }
198
199
    /**
200
     * @param string $property
201
     * @param mixed $value
202
     *
203
     * @throws \Spryker\Shared\Kernel\Transfer\Exception\InvalidStrictTypeException
204
     *
205
     * @return void
206
     */
207
    protected function assertInstanceOfTransfer(string $property, $value): void
208
    {
209
        if (!($value instanceof TransferInterface)) {
210
            throw new InvalidStrictTypeException(sprintf(
211
                'The value for the property "%s::$%s" must be an instance of "%s" but "%s" given.',
212
                static::class,
213
                $property,
214
                TransferInterface::class,
215
                gettype($value),
216
            ));
217
        }
218
    }
219
220
    /**
221
     * @param string $propertyName
222
     * @param mixed|null $value
223
     *
224
     * @return void
225
     */
226
    protected function assignValueObject(string $propertyName, $value): void
227
    {
228
        $propertySetterMethod = $this->getSetterMethod($propertyName);
229
        $this->$propertySetterMethod($value);
230
    }
231
232
    /**
233
     * @param string $elementType
234
     * @param \ArrayObject<int|string, mixed>|array $arrayObject
235
     * @param bool $ignoreMissingProperty
236
     *
237
     * @return \ArrayObject<int, \Spryker\Shared\Kernel\Transfer\TransferInterface>
238
     */
239
    protected function processArrayObject($elementType, $arrayObject, $ignoreMissingProperty = false): ArrayObject
240
    {
241
        $result = new ArrayObject();
242
        foreach ($arrayObject as $key => $arrayElement) {
243
            if (!is_array($arrayElement)) {
244
                $result->offsetSet($key, new $elementType());
245
246
                continue;
247
            }
248
249
            if ($arrayElement) {
250
                /** @var \Spryker\Shared\Kernel\Transfer\TransferInterface $transferObject */
251
                $transferObject = new $elementType();
252
                $transferObject->fromArray($arrayElement, $ignoreMissingProperty);
253
                $result->offsetSet($key, $transferObject);
254
            }
255
        }
256
257
        return $result;
258
    }
259
260
    /**
261
     * @param string $property
262
     *
263
     * @throws \Spryker\Shared\Kernel\Transfer\Exception\RequiredTransferPropertyException
264
     *
265
     * @return void
266
     */
267
    protected function assertPropertyIsSet($property): void
268
    {
269
        if ($this->$property === null || (is_array($this->$property) && $this->transferMetadata[$property]['is_strict'] && $this->$property === [])) {
270
            throw new RequiredTransferPropertyException(sprintf(
271
                'Missing required property "%s" for transfer %s.',
272
                $property,
273
                static::class,
274
            ));
275
        }
276
    }
277
278
    /**
279
     * @param string $property
280
     *
281
     * @throws \Spryker\Shared\Kernel\Transfer\Exception\RequiredTransferPropertyException
282
     *
283
     * @return void
284
     */
285
    protected function assertCollectionPropertyIsSet($property): void
286
    {
287
        /** @var \ArrayObject<int, mixed> $collection */
288
        $collection = $this->$property;
289
        if ($collection->count() === 0) {
290
            throw new RequiredTransferPropertyException(sprintf(
291
                'Empty required collection property "%s" for transfer %s.',
292
                $property,
293
                static::class,
294
            ));
295
        }
296
    }
297
298
    /**
299
     * @param string $property
300
     * @param mixed $value
301
     * @param bool $ignoreMissingProperty
302
     *
303
     * @return \Spryker\Shared\Kernel\Transfer\TransferInterface
304
     */
305
    protected function initializeNestedTransferObject($property, $value, $ignoreMissingProperty = false)
306
    {
307
        $type = $this->transferMetadata[$property]['type'];
308
309
        /** @var \Spryker\Shared\Kernel\Transfer\TransferInterface $transferObject */
310
        $transferObject = new $type();
311
312
        if (is_array($value)) {
313
            $transferObject->fromArray($value, $ignoreMissingProperty);
314
            $value = $transferObject;
315
        }
316
317
        return $value;
318
    }
319
320
    /**
321
     * @param string $propertyName
322
     *
323
     * @return string
324
     */
325
    protected function getSetterMethod(string $propertyName): string
326
    {
327
        return 'set' . ucfirst($propertyName);
328
    }
329
330
    /**
331
     * @param string $property
332
     * @param bool $ignoreMissingProperty
333
     *
334
     * @throws \InvalidArgumentException
335
     *
336
     * @return bool
337
     */
338
    protected function hasProperty($property, $ignoreMissingProperty)
339
    {
340
        if (isset($this->transferPropertyNameMap[$property])) {
341
            return true;
342
        }
343
344
        if ($ignoreMissingProperty) {
345
            return false;
346
        }
347
348
        throw new InvalidArgumentException(
349
            sprintf('Missing property "%s" in "%s"', $property, static::class),
350
        );
351
    }
352
353
    /**
354
     * @param \ArrayObject<string, mixed>|array<string, mixed> $value
355
     * @param array<string, mixed> $values
356
     * @param string $arrayKey
357
     * @param bool $isRecursive
358
     * @param string $childConvertMethodName
359
     * @param bool $camelCasedKeys
360
     *
361
     * @return array<string, mixed>
362
     */
363
    private function addValuesToCollection($value, $values, $arrayKey, $isRecursive, $childConvertMethodName, $camelCasedKeys = false)
364
    {
365
        foreach ($value as $elementKey => $arrayElement) {
366
            if (is_array($arrayElement) || is_scalar($arrayElement)) {
367
                $values[$arrayKey][$elementKey] = $arrayElement;
368
369
                continue;
370
            }
371
            $values[$arrayKey][$elementKey] = $arrayElement->$childConvertMethodName($isRecursive, $camelCasedKeys);
372
        }
373
374
        return $values;
375
    }
376
377
    /**
378
     * @return string
379
     */
380
    public function serialize()
381
    {
382
        $jsonUtil = new Json();
383
384
        /** @var string $jsonEncodedString */
385
        $jsonEncodedString = $jsonUtil->encode($this->modifiedToArray());
386
387
        return $jsonEncodedString;
388
    }
389
390
    /**
391
     * @param string $serialized
392
     *
393
     * @throws \Spryker\Shared\Kernel\Transfer\Exception\TransferUnserializationException
394
     *
395
     * @return void
396
     */
397
    public function unserialize($serialized)
398
    {
399
        try {
400
            $jsonUtil = new Json();
401
            $this->fromArray($jsonUtil->decode($serialized, true), true);
0 ignored issues
show
Bug introduced by
It seems like $jsonUtil->decode($serialized, true) can also be of type null; however, parameter $data of Spryker\Shared\Kernel\Tr...ctTransfer::fromArray() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

401
            $this->fromArray(/** @scrutinizer ignore-type */ $jsonUtil->decode($serialized, true), true);
Loading history...
402
            $this->initCollectionProperties();
403
        } catch (Exception $exception) {
404
            throw new TransferUnserializationException(
405
                sprintf(
406
                    'Failed to unserialize %s. Updating or clearing your data source may solve this problem: %s',
407
                    static::class,
408
                    $exception->getMessage(),
409
                ),
410
                $exception->getCode(),
411
                $exception,
412
            );
413
        }
414
    }
415
416
    /**
417
     * @param string $offset
418
     *
419
     * @return bool
420
     */
421
    public function offsetExists($offset): bool
422
    {
423
        return isset($this->transferMetadata[$offset]);
424
    }
425
426
    /**
427
     * @param string $offset
428
     *
429
     * @return mixed
430
     */
431
    #[\ReturnTypeWillChange]
432
    public function offsetGet($offset)
433
    {
434
        return $this->$offset;
435
    }
436
437
    /**
438
     * @param mixed $offset
439
     * @param mixed $value
440
     *
441
     * @return void
442
     */
443
    public function offsetSet($offset, $value): void
444
    {
445
        $this->$offset = $value;
446
    }
447
448
    /**
449
     * @param mixed $offset
450
     *
451
     * @throws \Spryker\Shared\Kernel\Transfer\Exception\ArrayAccessReadyOnlyException
452
     *
453
     * @return void
454
     */
455
    public function offsetUnset($offset): void
456
    {
457
        throw new ArrayAccessReadyOnlyException('Transfer object as an array is available only for read');
458
    }
459
460
    /**
461
     * @param string $propertyName
462
     *
463
     * @throws \Spryker\Shared\Kernel\Transfer\Exception\NullValueException
464
     *
465
     * @return void
466
     */
467
    protected function throwNullValueException(string $propertyName): void
468
    {
469
        throw new NullValueException(
470
            sprintf('Property "%s" of transfer `%s` is null.', $propertyName, static::class),
471
        );
472
    }
473
474
    /**
475
     * @return array<string, mixed>
476
     */
477
    public function __serialize(): array
478
    {
479
        return $this->modifiedToArray();
480
    }
481
482
    /**
483
     * @param array<string, mixed> $data
484
     *
485
     * @return void
486
     */
487
    public function __unserialize(array $data): void
488
    {
489
        $this->fromArray($data, true);
490
        $this->initCollectionProperties();
491
    }
492
}
493