Passed
Pull Request — master (#1606)
by Ivan
07:56
created

ClassMetadata::sortProperties()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
cc 4
eloc 10
nc 4
nop 0
dl 0
loc 14
ccs 0
cts 0
cp 0
crap 20
rs 9.9332
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace JMS\Serializer\Metadata;
6
7
use JMS\Serializer\Exception\InvalidMetadataException;
8
use JMS\Serializer\Expression\Expression;
9
use JMS\Serializer\Ordering\AlphabeticalPropertyOrderingStrategy;
10
use JMS\Serializer\Ordering\CustomPropertyOrderingStrategy;
11
use JMS\Serializer\Ordering\IdenticalPropertyOrderingStrategy;
12
use Metadata\MergeableClassMetadata;
13
use Metadata\MergeableInterface;
14
use Metadata\MethodMetadata;
15
use Metadata\PropertyMetadata as BasePropertyMetadata;
16
17
/**
18
 * Class Metadata used to customize the serialization process.
19
 *
20
 * @author Johannes M. Schmitt <[email protected]>
21
 *
22
 * @property PropertyMetadata[] $propertyMetadata
23
 */
24
class ClassMetadata extends MergeableClassMetadata
25
{
26
    public const ACCESSOR_ORDER_UNDEFINED = 'undefined';
27
    public const ACCESSOR_ORDER_ALPHABETICAL = 'alphabetical';
28
    public const ACCESSOR_ORDER_CUSTOM = 'custom';
29
30
    /** @var \ReflectionMethod[] */
31
    public $preSerializeMethods = [];
32
33
    /** @var \ReflectionMethod[] */
34
    public $postSerializeMethods = [];
35
36
    /** @var \ReflectionMethod[] */
37
    public $postDeserializeMethods = [];
38
39
    /**
40
     * @var string
41
     */
42
    public $xmlRootName;
43
44
    /**
45
     * @var string
46
     */
47
    public $xmlRootNamespace;
48
49
    /**
50
     * @var string
51
     */
52
    public $xmlRootPrefix;
53
    /**
54
     * @var string[]
55
     */
56
    public $xmlNamespaces = [];
57
58
    /**
59
     * @var string
60
     */
61
    public $accessorOrder;
62
63
    /**
64
     * @var string[]
65
     */
66
    public $customOrder;
67
68
    /**
69
     * @internal
70
     *
71
     * @var bool
72 32
     */
73
    public $usingExpression = false;
74 32
75
    /**
76
     * @internal
77
     *
78 32
     * @var bool
79
     */
80
    public $isList = false;
81
82 32
    /**
83 32
     * @internal
84 32
     *
85 32
     * @var bool
86 32
     */
87
    public $isMap = false;
88 15
89
    /**
90 15
     * @var bool
91
     */
92
    public $discriminatorDisabled = false;
93
94
    /**
95
     * @var string
96
     */
97
    public $discriminatorBaseClass;
98
    /**
99
     * @var string
100
     */
101
    public $discriminatorDefaultClass = null;
102 39
    /**
103
     * @var string
104 39
     */
105
    public $discriminatorFieldName;
106
    /**
107
     * @var string
108 39
     */
109 34
    public $discriminatorValue;
110 34
111
    /**
112
     * @var string[]
113
     */
114 39
    public $discriminatorMap = [];
115 39
116 39
    /**
117 39
     * @var string[]
118
     */
119 282
    public $discriminatorGroups = [];
120
121 282
    /**
122 282
     * @var bool
123 282
     */
124 26
    public $xmlDiscriminatorAttribute = false;
125
126 282
    /**
127
     * @var bool
128 2
     */
129
    public $xmlDiscriminatorCData = true;
130 2
131 2
    /**
132
     * @var string
133 2
     */
134
    public $xmlDiscriminatorNamespace;
135 2
136 2
    /**
137
     * @var string|Expression
138 4
     */
139
    public $excludeIf;
140 4
141 4
    public function setDiscriminator(string $fieldName, array $map, array $groups = [], $defaultClass = null): void
142
    {
143 27
        if (empty($fieldName)) {
144
            throw new InvalidMetadataException('The $fieldName cannot be empty.');
145 27
        }
146
147
        if (empty($map)) {
148 27
            throw new InvalidMetadataException('The discriminator map cannot be empty.');
149
        }
150 27
151 27
        $this->discriminatorBaseClass = $this->name;
152 27
        $this->discriminatorDefaultClass = $defaultClass;
153 27
        $this->discriminatorFieldName = $fieldName;
154 27
        $this->discriminatorMap = $map;
155 27
        $this->discriminatorGroups = $groups;
156
157 27
        $this->handleDiscriminatorProperty();
158 2
    }
159 2
160
    private function getReflection(): \ReflectionClass
161
    {
162 27
        return new \ReflectionClass($this->name);
163
    }
164
165
    /**
166
     * Sets the order of properties in the class.
167
     *
168
     * @param array $customOrder
169 27
     *
170 2
     * @throws InvalidMetadataException When the accessor order is not valid.
171 2
     * @throws InvalidMetadataException When the custom order is not valid.
172
     */
173
    public function setAccessorOrder(string $order, array $customOrder = []): void
174 27
    {
175 27
        if (!in_array($order, [self::ACCESSOR_ORDER_UNDEFINED, self::ACCESSOR_ORDER_ALPHABETICAL, self::ACCESSOR_ORDER_CUSTOM], true)) {
176
            throw new InvalidMetadataException(sprintf('The accessor order "%s" is invalid.', $order));
177
        }
178 27
179 2
        foreach ($customOrder as $name) {
180 2
            if (!\is_string($name)) {
181 2
                throw new InvalidMetadataException(sprintf('$customOrder is expected to be a list of strings, but got element of value %s.', json_encode($name)));
182
            }
183
        }
184 27
185 15
        $this->accessorOrder = $order;
186
        $this->customOrder = array_flip($customOrder);
187
        $this->sortProperties();
188
    }
189
190
    public function addPropertyMetadata(BasePropertyMetadata $metadata): void
191
    {
192
        parent::addPropertyMetadata($metadata);
193 15
194
        $this->sortProperties();
195 15
        if ($metadata instanceof PropertyMetadata && $metadata->excludeIf) {
196 15
            $this->usingExpression = true;
197
        }
198
    }
199
200
    public function addPreSerializeMethod(MethodMetadata $method): void
201
    {
202
        $this->preSerializeMethods[] = $method;
203
    }
204
205
    public function addPostSerializeMethod(MethodMetadata $method): void
206 15
    {
207 15
        $this->postSerializeMethods[] = $method;
208 15
    }
209 15
210 15
    public function addPostDeserializeMethod(MethodMetadata $method): void
211
    {
212 15
        $this->postDeserializeMethods[] = $method;
213 15
    }
214 15
215 15
    public function merge(MergeableInterface $object): void
216 15
    {
217
        if (!$object instanceof ClassMetadata) {
218
            throw new InvalidMetadataException('$object must be an instance of ClassMetadata.');
219 27
        }
220 27
221
        parent::merge($object);
222 31
223
        $this->preSerializeMethods = array_merge($this->preSerializeMethods, $object->preSerializeMethods);
224 31
        $this->postSerializeMethods = array_merge($this->postSerializeMethods, $object->postSerializeMethods);
225
        $this->postDeserializeMethods = array_merge($this->postDeserializeMethods, $object->postDeserializeMethods);
226
        $this->xmlRootName = $object->xmlRootName;
227
        $this->xmlRootPrefix = $object->xmlRootPrefix;
228 31
        $this->xmlRootNamespace = $object->xmlRootNamespace;
229 31
        if (null !== $object->excludeIf) {
230 31
            $this->excludeIf = $object->excludeIf;
231
        }
232
233 2
        $this->isMap = $object->isMap;
234
        $this->isList = $object->isList;
235
236 31
        $this->xmlNamespaces = array_merge($this->xmlNamespaces, $object->xmlNamespaces);
237 31
238
        if ($object->accessorOrder) {
239
            $this->accessorOrder = $object->accessorOrder;
240
            $this->customOrder = $object->customOrder;
241
        }
242
243
        if ($object->discriminatorFieldName && $this->discriminatorFieldName && $object->discriminatorFieldName !== $this->discriminatorFieldName) {
244
            throw new InvalidMetadataException(sprintf(
245
                'The discriminator of class "%s" would overwrite the discriminator of the parent class "%s". Please define all possible sub-classes in the discriminator of %s.',
246
                $object->name,
247
                $this->discriminatorBaseClass,
248
                $this->discriminatorBaseClass,
249
            ));
250
        } elseif (!$this->discriminatorFieldName && $object->discriminatorFieldName) {
251
            $this->discriminatorFieldName = $object->discriminatorFieldName;
252
            $this->discriminatorMap = $object->discriminatorMap;
253
        }
254
255
        if (null !== $object->discriminatorDisabled) {
256
            $this->discriminatorDisabled = $object->discriminatorDisabled;
257
        }
258
259
        if (null !== $object->discriminatorDefaultClass) {
260
            $this->discriminatorDefaultClass = $object->discriminatorDefaultClass;
261
        }
262
263
        if ($object->discriminatorMap) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $object->discriminatorMap of type string[] 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...
264
            $this->discriminatorFieldName = $object->discriminatorFieldName;
265
            $this->discriminatorMap = $object->discriminatorMap;
266
            $this->discriminatorBaseClass = $object->discriminatorBaseClass;
267
            $this->discriminatorGroups = $object->discriminatorGroups;
268
269
            $this->xmlDiscriminatorCData = $object->xmlDiscriminatorCData;
270
            $this->xmlDiscriminatorAttribute = $object->xmlDiscriminatorAttribute;
271
            $this->xmlDiscriminatorNamespace = $object->xmlDiscriminatorNamespace;
272
        }
273
274
        $this->handleDiscriminatorProperty();
275
276
        $this->sortProperties();
277
    }
278
279
    public function registerNamespace(string $uri, ?string $prefix = null): void
280
    {
281
        if (!\is_string($uri)) {
0 ignored issues
show
introduced by
The condition is_string($uri) is always true.
Loading history...
282
            throw new InvalidMetadataException(sprintf('$uri is expected to be a strings, but got value %s.', json_encode($uri)));
283
        }
284
285
        if (null !== $prefix) {
286
            if (!\is_string($prefix)) {
0 ignored issues
show
introduced by
The condition is_string($prefix) is always true.
Loading history...
287
                throw new InvalidMetadataException(sprintf('$prefix is expected to be a strings, but got value %s.', json_encode($prefix)));
288
            }
289
        } else {
290
            $prefix = '';
291
        }
292
293
        $this->xmlNamespaces[$prefix] = $uri;
294
    }
295
296
    protected function serializeToArray(): array
297
    {
298
        $this->sortProperties();
299
300
        return [
301
            $this->preSerializeMethods,
302
            $this->postSerializeMethods,
303
            $this->postDeserializeMethods,
304
            $this->xmlRootName,
305
            $this->xmlRootNamespace,
306
            $this->xmlNamespaces,
307
            $this->accessorOrder,
308
            $this->customOrder,
309
            $this->discriminatorDisabled,
310
            $this->discriminatorBaseClass,
311
            $this->discriminatorFieldName,
312
            $this->discriminatorValue,
313
            $this->discriminatorMap,
314
            $this->discriminatorGroups,
315
            $this->excludeIf,
316
            $this->discriminatorGroups,
317
            $this->xmlDiscriminatorAttribute,
318
            $this->xmlDiscriminatorCData,
319 286
            $this->usingExpression,
320
            $this->xmlDiscriminatorNamespace,
321 286
            $this->xmlRootPrefix,
322 286
            $this->isList,
323
            $this->isMap,
324
            parent::serializeToArray(),
325
            $this->discriminatorDefaultClass,
326 286
        ];
327 7
    }
328 7
329
    protected function unserializeFromArray(array $data): void
330 284
    {
331 34
        [
332 34
            $this->preSerializeMethods,
333
            $this->postSerializeMethods,
334 286
            $this->postDeserializeMethods,
335
            $this->xmlRootName,
336
            $this->xmlRootNamespace,
337
            $this->xmlNamespaces,
338
            $this->accessorOrder,
339
            $this->customOrder,
340
            $this->discriminatorDisabled,
341
            $this->discriminatorBaseClass,
342
            $this->discriminatorFieldName,
343
            $this->discriminatorValue,
344
            $this->discriminatorMap,
345
            $this->discriminatorGroups,
346
            $this->excludeIf,
347
            $this->discriminatorGroups,
348
            $this->xmlDiscriminatorAttribute,
349
            $this->xmlDiscriminatorCData,
350
            $this->usingExpression,
351
            $this->xmlDiscriminatorNamespace,
352
            $this->xmlRootPrefix,
353
            $this->isList,
354
            $this->isMap,
355
            $parentData,
356
            $this->discriminatorDefaultClass,
357
        ] = $data;
358
359
        parent::unserializeFromArray($parentData);
360
    }
361
362
    private function handleDiscriminatorProperty(): void
363
    {
364
        if (
365
            $this->discriminatorMap
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->discriminatorMap of type string[] 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...
366
            && !$this->getReflection()->isAbstract()
367
            && !$this->getReflection()->isInterface()
368
        ) {
369
            if (false === $typeValue = array_search($this->name, $this->discriminatorMap, true)) {
370
                if (! empty($this->discriminatorDefaultClass)) {
371
                    $typeValue = $this->discriminatorDefaultClass;
372
                } else {
373
                    throw new InvalidMetadataException(sprintf(
374
                        'The sub-class "%s" is not listed in the discriminator of the base class "%s".',
375
                        $this->name,
376
                        $this->discriminatorBaseClass,
377
                    ));
378
                }
379
            }
380
381
            $this->discriminatorValue = $typeValue;
382
383
            if (
384
                isset($this->propertyMetadata[$this->discriminatorFieldName])
385
                && !$this->propertyMetadata[$this->discriminatorFieldName] instanceof StaticPropertyMetadata
386
            ) {
387
                throw new InvalidMetadataException(sprintf(
388
                    'The discriminator field name "%s" of the base-class "%s" conflicts with a regular property of the sub-class "%s".',
389
                    $this->discriminatorFieldName,
390
                    $this->discriminatorBaseClass,
391
                    $this->name,
392
                ));
393
            }
394
395
            $discriminatorProperty = new StaticPropertyMetadata(
396
                $this->name,
397
                $this->discriminatorFieldName,
398
                $typeValue,
399
                $this->discriminatorGroups,
400
            );
401
            $discriminatorProperty->serializedName = $this->discriminatorFieldName;
402
            $discriminatorProperty->xmlAttribute = $this->xmlDiscriminatorAttribute;
403
            $discriminatorProperty->xmlElementCData = $this->xmlDiscriminatorCData;
404
            $discriminatorProperty->xmlNamespace = $this->xmlDiscriminatorNamespace;
405
            $this->propertyMetadata[$this->discriminatorFieldName] = $discriminatorProperty;
406
        }
407
    }
408
409
    private function sortProperties(): void
410
    {
411
        switch ($this->accessorOrder) {
412
            case self::ACCESSOR_ORDER_UNDEFINED:
413
                $this->propertyMetadata = (new IdenticalPropertyOrderingStrategy())->order($this->propertyMetadata);
414
                break;
415
416
            case self::ACCESSOR_ORDER_ALPHABETICAL:
417
                $this->propertyMetadata = (new AlphabeticalPropertyOrderingStrategy())->order($this->propertyMetadata);
418
                break;
419
420
            case self::ACCESSOR_ORDER_CUSTOM:
421
                $this->propertyMetadata = (new CustomPropertyOrderingStrategy($this->customOrder))->order($this->propertyMetadata);
422
                break;
423
        }
424
    }
425
}
426