Completed
Pull Request — master (#1303)
by Ayrton
04:48
created

ClassMetadata   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 431
Duplicated Lines 0 %

Test Coverage

Coverage 53.53%

Importance

Changes 4
Bugs 1 Features 0
Metric Value
eloc 197
c 4
b 1
f 0
dl 0
loc 431
ccs 91
cts 170
cp 0.5353
rs 7.92
wmc 51

13 Methods

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