Passed
Pull Request — master (#1609)
by Ivan
03:14
created

ClassMetadata   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 411
Duplicated Lines 0 %

Test Coverage

Coverage 53.53%

Importance

Changes 2
Bugs 0 Features 1
Metric Value
eloc 202
dl 0
loc 411
rs 8.8
c 2
b 0
f 1
ccs 91
cts 170
cp 0.5353
wmc 45

13 Methods

Rating   Name   Duplication   Size   Complexity  
C merge() 0 62 12
A unserializeFromArray() 0 32 1
A setAccessorOrder() 0 15 4
A addPostDeserializeMethod() 0 3 1
A serializeToArray() 0 31 1
B handleDiscriminatorProperty() 0 48 9
A sortProperties() 0 14 4
A getReflection() 0 3 1
A addPostSerializeMethod() 0 3 1
A registerNamespace() 0 15 4
A addPropertyMetadata() 0 7 3
A addPreSerializeMethod() 0 3 1
A setDiscriminator() 0 18 3

How to fix   Complexity   

Complex Class

Complex classes like ClassMetadata often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ClassMetadata, and based on these observations, apply Extract Interface, too.

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 bool
96
     */
97
    public $discriminatorVirtual = true;
98
99
    /**
100
     * @var string
101
     */
102 39
    public $discriminatorBaseClass;
103
    /**
104 39
     * @var string
105
     */
106
    public $discriminatorDefaultClass = null;
107
    /**
108 39
     * @var string
109 34
     */
110 34
    public $discriminatorFieldName;
111
    /**
112
     * @var string
113
     */
114 39
    public $discriminatorValue;
115 39
116 39
    /**
117 39
     * @var string[]
118
     */
119 282
    public $discriminatorMap = [];
120
121 282
    /**
122 282
     * @var string[]
123 282
     */
124 26
    public $discriminatorGroups = [];
125
126 282
    /**
127
     * @var bool
128 2
     */
129
    public $xmlDiscriminatorAttribute = false;
130 2
131 2
    /**
132
     * @var bool
133 2
     */
134
    public $xmlDiscriminatorCData = true;
135 2
136 2
    /**
137
     * @var string
138 4
     */
139
    public $xmlDiscriminatorNamespace;
140 4
141 4
    /**
142
     * @var string|Expression
143 27
     */
144
    public $excludeIf;
145 27
146
    public function setDiscriminator(string $fieldName, array $map, array $groups = [], $defaultClass = null, $virtual = true): void
147
    {
148 27
        if (empty($fieldName)) {
149
            throw new InvalidMetadataException('The $fieldName cannot be empty.');
150 27
        }
151 27
152 27
        if (empty($map)) {
153 27
            throw new InvalidMetadataException('The discriminator map cannot be empty.');
154 27
        }
155 27
156
        $this->discriminatorBaseClass = $this->name;
157 27
        $this->discriminatorDefaultClass = $defaultClass;
158 2
        $this->discriminatorFieldName = $fieldName;
159 2
        $this->discriminatorMap = $map;
160
        $this->discriminatorGroups = $groups;
161
        $this->discriminatorVirtual = $virtual;
162 27
163
        $this->handleDiscriminatorProperty();
164
    }
165
166
    private function getReflection(): \ReflectionClass
167
    {
168
        return new \ReflectionClass($this->name);
169 27
    }
170 2
171 2
    /**
172
     * Sets the order of properties in the class.
173
     *
174 27
     * @param array $customOrder
175 27
     *
176
     * @throws InvalidMetadataException When the accessor order is not valid.
177
     * @throws InvalidMetadataException When the custom order is not valid.
178 27
     */
179 2
    public function setAccessorOrder(string $order, array $customOrder = []): void
180 2
    {
181 2
        if (!in_array($order, [self::ACCESSOR_ORDER_UNDEFINED, self::ACCESSOR_ORDER_ALPHABETICAL, self::ACCESSOR_ORDER_CUSTOM], true)) {
182
            throw new InvalidMetadataException(sprintf('The accessor order "%s" is invalid.', $order));
183
        }
184 27
185 15
        foreach ($customOrder as $name) {
186
            if (!\is_string($name)) {
187
                throw new InvalidMetadataException(sprintf('$customOrder is expected to be a list of strings, but got element of value %s.', json_encode($name)));
188
            }
189
        }
190
191
        $this->accessorOrder = $order;
192
        $this->customOrder = array_flip($customOrder);
193 15
        $this->sortProperties();
194
    }
195 15
196 15
    public function addPropertyMetadata(BasePropertyMetadata $metadata): void
197
    {
198
        parent::addPropertyMetadata($metadata);
199
200
        $this->sortProperties();
201
        if ($metadata instanceof PropertyMetadata && $metadata->excludeIf) {
202
            $this->usingExpression = true;
203
        }
204
    }
205
206 15
    public function addPreSerializeMethod(MethodMetadata $method): void
207 15
    {
208 15
        $this->preSerializeMethods[] = $method;
209 15
    }
210 15
211
    public function addPostSerializeMethod(MethodMetadata $method): void
212 15
    {
213 15
        $this->postSerializeMethods[] = $method;
214 15
    }
215 15
216 15
    public function addPostDeserializeMethod(MethodMetadata $method): void
217
    {
218
        $this->postDeserializeMethods[] = $method;
219 27
    }
220 27
221
    public function merge(MergeableInterface $object): void
222 31
    {
223
        if (!$object instanceof ClassMetadata) {
224 31
            throw new InvalidMetadataException('$object must be an instance of ClassMetadata.');
225
        }
226
227
        parent::merge($object);
228 31
229 31
        $this->preSerializeMethods = array_merge($this->preSerializeMethods, $object->preSerializeMethods);
230 31
        $this->postSerializeMethods = array_merge($this->postSerializeMethods, $object->postSerializeMethods);
231
        $this->postDeserializeMethods = array_merge($this->postDeserializeMethods, $object->postDeserializeMethods);
232
        $this->xmlRootName = $object->xmlRootName;
233 2
        $this->xmlRootPrefix = $object->xmlRootPrefix;
234
        $this->xmlRootNamespace = $object->xmlRootNamespace;
235
        if (null !== $object->excludeIf) {
236 31
            $this->excludeIf = $object->excludeIf;
237 31
        }
238
239
        $this->isMap = $object->isMap;
240
        $this->isList = $object->isList;
241
242
        $this->xmlNamespaces = array_merge($this->xmlNamespaces, $object->xmlNamespaces);
243
244
        if ($object->accessorOrder) {
245
            $this->accessorOrder = $object->accessorOrder;
246
            $this->customOrder = $object->customOrder;
247
        }
248
249
        if ($object->discriminatorFieldName && $this->discriminatorFieldName && $object->discriminatorFieldName !== $this->discriminatorFieldName) {
250
            throw new InvalidMetadataException(sprintf(
251
                '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.',
252
                $object->name,
253
                $this->discriminatorBaseClass,
254
                $this->discriminatorBaseClass,
255
            ));
256
        } elseif (!$this->discriminatorFieldName && $object->discriminatorFieldName) {
257
            $this->discriminatorFieldName = $object->discriminatorFieldName;
258
            $this->discriminatorMap = $object->discriminatorMap;
259
        }
260
261
        if (null !== $object->discriminatorDisabled) {
262
            $this->discriminatorDisabled = $object->discriminatorDisabled;
263
        }
264
265
        if (null !== $object->discriminatorDefaultClass) {
266
            $this->discriminatorDefaultClass = $object->discriminatorDefaultClass;
267
        }
268
269
        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...
270
            $this->discriminatorFieldName = $object->discriminatorFieldName;
271
            $this->discriminatorMap = $object->discriminatorMap;
272
            $this->discriminatorBaseClass = $object->discriminatorBaseClass;
273
            $this->discriminatorGroups = $object->discriminatorGroups;
274
275
            $this->xmlDiscriminatorCData = $object->xmlDiscriminatorCData;
276
            $this->xmlDiscriminatorAttribute = $object->xmlDiscriminatorAttribute;
277
            $this->xmlDiscriminatorNamespace = $object->xmlDiscriminatorNamespace;
278
        }
279
280
        $this->handleDiscriminatorProperty();
281
282
        $this->sortProperties();
283
    }
284
285
    public function registerNamespace(string $uri, ?string $prefix = null): void
286
    {
287
        if (!\is_string($uri)) {
0 ignored issues
show
introduced by
The condition is_string($uri) is always true.
Loading history...
288
            throw new InvalidMetadataException(sprintf('$uri is expected to be a strings, but got value %s.', json_encode($uri)));
289
        }
290
291
        if (null !== $prefix) {
292
            if (!\is_string($prefix)) {
0 ignored issues
show
introduced by
The condition is_string($prefix) is always true.
Loading history...
293
                throw new InvalidMetadataException(sprintf('$prefix is expected to be a strings, but got value %s.', json_encode($prefix)));
294
            }
295
        } else {
296
            $prefix = '';
297
        }
298
299
        $this->xmlNamespaces[$prefix] = $uri;
300
    }
301
302
    protected function serializeToArray(): array
303
    {
304
        $this->sortProperties();
305
306
        return [
307
            $this->preSerializeMethods,
308
            $this->postSerializeMethods,
309
            $this->postDeserializeMethods,
310
            $this->xmlRootName,
311
            $this->xmlRootNamespace,
312
            $this->xmlNamespaces,
313
            $this->accessorOrder,
314
            $this->customOrder,
315
            $this->discriminatorDisabled,
316
            $this->discriminatorBaseClass,
317
            $this->discriminatorFieldName,
318
            $this->discriminatorValue,
319 286
            $this->discriminatorMap,
320
            $this->discriminatorGroups,
321 286
            $this->excludeIf,
322 286
            $this->discriminatorGroups,
323
            $this->xmlDiscriminatorAttribute,
324
            $this->xmlDiscriminatorCData,
325
            $this->usingExpression,
326 286
            $this->xmlDiscriminatorNamespace,
327 7
            $this->xmlRootPrefix,
328 7
            $this->isList,
329
            $this->isMap,
330 284
            parent::serializeToArray(),
331 34
            $this->discriminatorDefaultClass,
332 34
            $this->discriminatorVirtual,
333
        ];
334 286
    }
335
336
    protected function unserializeFromArray(array $data): void
337
    {
338
        [
339
            $this->preSerializeMethods,
340
            $this->postSerializeMethods,
341
            $this->postDeserializeMethods,
342
            $this->xmlRootName,
343
            $this->xmlRootNamespace,
344
            $this->xmlNamespaces,
345
            $this->accessorOrder,
346
            $this->customOrder,
347
            $this->discriminatorDisabled,
348
            $this->discriminatorBaseClass,
349
            $this->discriminatorFieldName,
350
            $this->discriminatorValue,
351
            $this->discriminatorMap,
352
            $this->discriminatorGroups,
353
            $this->excludeIf,
354
            $this->discriminatorGroups,
355
            $this->xmlDiscriminatorAttribute,
356
            $this->xmlDiscriminatorCData,
357
            $this->usingExpression,
358
            $this->xmlDiscriminatorNamespace,
359
            $this->xmlRootPrefix,
360
            $this->isList,
361
            $this->isMap,
362
            $parentData,
363
            $this->discriminatorDefaultClass,
364
            $this->discriminatorVirtual,
365
        ] = $data;
366
367
        parent::unserializeFromArray($parentData);
368
    }
369
370
    private function handleDiscriminatorProperty(): void
371
    {
372
        if (
373
            $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...
374
            && !$this->getReflection()->isAbstract()
375
            && !$this->getReflection()->isInterface()
376
        ) {
377
            if (false === $typeValue = array_search($this->name, $this->discriminatorMap, true)) {
378
                if (! empty($this->discriminatorDefaultClass)) {
379
                    $typeValue = $this->discriminatorDefaultClass;
380
                } else {
381
                    throw new InvalidMetadataException(sprintf(
382
                        'The sub-class "%s" is not listed in the discriminator of the base class "%s".',
383
                        $this->name,
384
                        $this->discriminatorBaseClass,
385
                    ));
386
                }
387
            }
388
389
            if (!$this->discriminatorVirtual) {
390
                return;
391
            }
392
393
            $this->discriminatorValue = $typeValue;
394
395
            if (
396
                isset($this->propertyMetadata[$this->discriminatorFieldName])
397
                && !$this->propertyMetadata[$this->discriminatorFieldName] instanceof StaticPropertyMetadata
398
            ) {
399
                throw new InvalidMetadataException(sprintf(
400
                    'The discriminator field name "%s" of the base-class "%s" conflicts with a regular property of the sub-class "%s".',
401
                    $this->discriminatorFieldName,
402
                    $this->discriminatorBaseClass,
403
                    $this->name,
404
                ));
405
            }
406
407
            $discriminatorProperty = new StaticPropertyMetadata(
408
                $this->name,
409
                $this->discriminatorFieldName,
410
                $typeValue,
411
                $this->discriminatorGroups,
412
            );
413
            $discriminatorProperty->serializedName = $this->discriminatorFieldName;
414
            $discriminatorProperty->xmlAttribute = $this->xmlDiscriminatorAttribute;
415
            $discriminatorProperty->xmlElementCData = $this->xmlDiscriminatorCData;
416
            $discriminatorProperty->xmlNamespace = $this->xmlDiscriminatorNamespace;
417
            $this->propertyMetadata[$this->discriminatorFieldName] = $discriminatorProperty;
418
        }
419
    }
420
421
    private function sortProperties(): void
422
    {
423
        switch ($this->accessorOrder) {
424
            case self::ACCESSOR_ORDER_UNDEFINED:
425
                $this->propertyMetadata = (new IdenticalPropertyOrderingStrategy())->order($this->propertyMetadata);
426
                break;
427
428
            case self::ACCESSOR_ORDER_ALPHABETICAL:
429
                $this->propertyMetadata = (new AlphabeticalPropertyOrderingStrategy())->order($this->propertyMetadata);
430
                break;
431
432
            case self::ACCESSOR_ORDER_CUSTOM:
433
                $this->propertyMetadata = (new CustomPropertyOrderingStrategy($this->customOrder))->order($this->propertyMetadata);
434
                break;
435
        }
436
    }
437
}
438