Completed
Push — master ( 7e12ab...eee1f0 )
by Eric
03:22
created

AbstractClassMetadataLoader::loadClassMetadata()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 16
ccs 9
cts 9
cp 1
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 8
nc 4
nop 1
crap 3
1
<?php
2
3
/*
4
 * This file is part of the Ivory Serializer package.
5
 *
6
 * (c) Eric GELOEN <[email protected]>
7
 *
8
 * For the full copyright and license information, please read the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Ivory\Serializer\Mapping\Loader;
13
14
use Ivory\Serializer\Exclusion\ExclusionPolicy;
15
use Ivory\Serializer\Mapping\ClassMetadataInterface;
16
use Ivory\Serializer\Mapping\PropertyMetadata;
17
use Ivory\Serializer\Mapping\PropertyMetadataInterface;
18
use Ivory\Serializer\Type\Parser\TypeParser;
19
use Ivory\Serializer\Type\Parser\TypeParserInterface;
20
21
/**
22
 * @author GeLo <[email protected]>
23
 */
24
abstract class AbstractClassMetadataLoader implements ClassMetadataLoaderInterface
25
{
26
    /**
27
     * @var TypeParserInterface
28
     */
29
    private $typeParser;
30
31
    /**
32
     * @var mixed[][]
33
     */
34
    private $data = [];
35
36
    /**
37
     * @param TypeParserInterface|null $typeParser
38
     */
39 1011
    public function __construct(TypeParserInterface $typeParser = null)
40
    {
41 1011
        $this->typeParser = $typeParser ?: new TypeParser();
42 1011
    }
43
44
    /**
45
     * {@inheritdoc}
46
     */
47 789
    public function loadClassMetadata(ClassMetadataInterface $classMetadata)
48
    {
49 789
        $class = $classMetadata->getName();
50
51 789
        if (!array_key_exists($class, $this->data)) {
52 789
            $this->data[$class] = $this->loadData($class);
53 510
        }
54
55 765
        if (!is_array($data = $this->data[$class])) {
56 18
            return false;
57
        }
58
59 747
        $this->doLoadClassMetadata($classMetadata, $data);
60
61 639
        return true;
62
    }
63
64
    /**
65
     * @param string $class
66
     *
67
     * @return mixed[]|null
68
     */
69
    abstract protected function loadData($class);
70
71
    /**
72
     * @param ClassMetadataInterface $classMetadata
73
     * @param mixed[]                $data
74
     */
75 747
    private function doLoadClassMetadata(ClassMetadataInterface $classMetadata, array $data)
76
    {
77 747
        if (!isset($data['properties']) || empty($data['properties'])) {
78 9
            throw new \InvalidArgumentException(sprintf(
79 9
                'No mapping properties found for "%s".',
80 9
                $classMetadata->getName()
81 6
            ));
82
        }
83
84 738
        $properties = $classMetadata->getProperties();
85 738
        $policy = $this->getExclusionPolicy($data);
86
87 729
        foreach ($data['properties'] as $property => $value) {
88 729
            $propertyMetadata = $classMetadata->getProperty($property) ?: new PropertyMetadata($property);
89 729
            $this->loadPropertyMetadata($propertyMetadata, $value);
90
91 666
            if ($this->isPropertyMetadataExposed($value, $policy)) {
92 666
                $properties[$property] = $propertyMetadata;
93 444
            }
94 444
        }
95
96 666
        if (($order = $this->getOrder($data, $properties)) !== null) {
97 90
            $properties = $this->sortProperties($properties, $order);
0 ignored issues
show
Bug introduced by
It seems like $order defined by $this->getOrder($data, $properties) on line 96 can also be of type array; however, Ivory\Serializer\Mapping...oader::sortProperties() does only seem to accept string|array<integer,string>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
98 60
        }
99
100 639
        $classMetadata->setProperties($properties);
101 639
    }
102
103
    /**
104
     * @param PropertyMetadataInterface $propertyMetadata
105
     * @param mixed                     $data
106
     */
107 729
    private function loadPropertyMetadata(PropertyMetadataInterface $propertyMetadata, $data)
108
    {
109 729
        if (!is_array($data)) {
110 30
            return;
111
        }
112
113 711
        if (array_key_exists('exclude', $data)) {
114 36
            $this->validatePropertyMetadataExclude($data['exclude']);
115 20
        }
116
117 705
        if (array_key_exists('expose', $data)) {
118 36
            $this->validatePropertyMetadataExpose($data['expose']);
119 20
        }
120
121 699
        if (array_key_exists('alias', $data)) {
122 93
            $this->loadPropertyMetadataAlias($propertyMetadata, $data['alias']);
123 56
        }
124
125 690
        if (array_key_exists('type', $data)) {
126 267
            $this->loadPropertyMetadataType($propertyMetadata, $data['type']);
127 172
        }
128
129 681
        if (array_key_exists('accessor', $data)) {
130 30
            $this->loadPropertyMetadataAccessor($propertyMetadata, $data['accessor']);
131 20
        }
132
133 681
        if (array_key_exists('mutator', $data)) {
134 30
            $this->loadPropertyMetadataMutator($propertyMetadata, $data['mutator']);
135 20
        }
136
137 681
        if (array_key_exists('since', $data)) {
138 75
            $this->loadPropertyMetadataSinceVersion($propertyMetadata, $data['since']);
139 44
        }
140
141 672
        if (array_key_exists('until', $data)) {
142 75
            $this->loadPropertyMetadataUntilVersion($propertyMetadata, $data['until']);
143 44
        }
144
145 663
        if (array_key_exists('max_depth', $data)) {
146 54
            $this->loadPropertyMetadataMaxDepth($propertyMetadata, $data['max_depth']);
147 32
        }
148
149 657
        if (array_key_exists('groups', $data)) {
150 84
            $this->loadPropertyMetadataGroups($propertyMetadata, $data['groups']);
151 50
        }
152 648
    }
153
154
    /**
155
     * @param PropertyMetadataInterface $propertyMetadata
156
     * @param string                    $alias
157
     */
158 93 View Code Duplication
    private function loadPropertyMetadataAlias(PropertyMetadataInterface $propertyMetadata, $alias)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
159
    {
160 93
        if (!is_string($alias)) {
161 6
            throw new \InvalidArgumentException(sprintf(
162 6
                'The mapping property alias must be a non empty string, got "%s".',
163 6
                is_object($alias) ? get_class($alias) : gettype($alias)
164 4
            ));
165
        }
166
167 87
        $alias = trim($alias);
168
169 87
        if (empty($alias)) {
170 3
            throw new \InvalidArgumentException('The mapping property alias must be a non empty string.');
171
        }
172
173 84
        $propertyMetadata->setAlias($alias);
174 84
    }
175
176
    /**
177
     * @param PropertyMetadataInterface $propertyMetadata
178
     * @param string                    $type
179
     */
180 267
    private function loadPropertyMetadataType(PropertyMetadataInterface $propertyMetadata, $type)
181
    {
182 267
        if (!is_string($type)) {
183 6
            throw new \InvalidArgumentException(sprintf(
184 6
                'The mapping property type must be a non empty string, got "%s".',
185 6
                is_object($type) ? get_class($type) : gettype($type)
186 4
            ));
187
        }
188
189 261
        $propertyMetadata->setType($this->typeParser->parse($type));
190 258
    }
191
192
    /**
193
     * @param PropertyMetadataInterface $propertyMetadata
194
     * @param string                    $accessor
195
     */
196 30 View Code Duplication
    private function loadPropertyMetadataAccessor(PropertyMetadataInterface $propertyMetadata, $accessor)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
197
    {
198 30
        if (!is_string($accessor)) {
199
            throw new \InvalidArgumentException(sprintf(
200
                'The mapping property accessor must be a non empty string, got "%s".',
201
                is_object($accessor) ? get_class($accessor) : gettype($accessor)
202
            ));
203
        }
204
205 30
        $accessor = trim($accessor);
206
207 30
        if (empty($accessor)) {
208
            throw new \InvalidArgumentException('The mapping property accessor must be a non empty string.');
209
        }
210
211 30
        $propertyMetadata->setAccessor($accessor);
212 30
    }
213
214
    /**
215
     * @param PropertyMetadataInterface $propertyMetadata
216
     * @param string                    $mutator
217
     */
218 30 View Code Duplication
    private function loadPropertyMetadataMutator(PropertyMetadataInterface $propertyMetadata, $mutator)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
219
    {
220 30
        if (!is_string($mutator)) {
221
            throw new \InvalidArgumentException(sprintf(
222
                'The mapping property mutator must be a non empty string, got "%s".',
223
                is_object($mutator) ? get_class($mutator) : gettype($mutator)
224
            ));
225
        }
226
227 30
        $mutator = trim($mutator);
228
229 30
        if (empty($mutator)) {
230
            throw new \InvalidArgumentException('The mapping property mutator must be a non empty string.');
231
        }
232
233 30
        $propertyMetadata->setMutator($mutator);
234 30
    }
235
236
    /**
237
     * @param PropertyMetadataInterface $propertyMetadata
238
     * @param string                    $version
239
     */
240 75 View Code Duplication
    private function loadPropertyMetadataSinceVersion(PropertyMetadataInterface $propertyMetadata, $version)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
241
    {
242 75
        if (!is_string($version)) {
243 6
            throw new \InvalidArgumentException(sprintf(
244 6
                'The mapping property since version must be a non empty string, got "%s".',
245 6
                is_object($version) ? get_class($version) : gettype($version)
246 4
            ));
247
        }
248
249 69
        $version = trim($version);
250
251 69
        if (empty($version)) {
252 3
            throw new \InvalidArgumentException('The mapping property since version must be a non empty string.');
253
        }
254
255 66
        $propertyMetadata->setSinceVersion($version);
256 66
    }
257
258
    /**
259
     * @param PropertyMetadataInterface $propertyMetadata
260
     * @param string                    $version
261
     */
262 75 View Code Duplication
    private function loadPropertyMetadataUntilVersion(PropertyMetadataInterface $propertyMetadata, $version)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
263
    {
264 75
        if (!is_string($version)) {
265 6
            throw new \InvalidArgumentException(sprintf(
266 6
                'The mapping property until version must be a non empty string, got "%s".',
267 6
                is_object($version) ? get_class($version) : gettype($version)
268 4
            ));
269
        }
270
271 69
        $version = trim($version);
272
273 69
        if (empty($version)) {
274 3
            throw new \InvalidArgumentException('The mapping property until version must be a non empty string.');
275
        }
276
277 66
        $propertyMetadata->setUntilVersion($version);
278 66
    }
279
280
    /**
281
     * @param PropertyMetadataInterface $propertyMetadata
282
     * @param string|int                $maxDepth
283
     */
284 54
    private function loadPropertyMetadataMaxDepth(PropertyMetadataInterface $propertyMetadata, $maxDepth)
285
    {
286 54
        if (!is_int($maxDepth) && !is_string($maxDepth) && !ctype_digit($maxDepth)) {
287 3
            throw new \InvalidArgumentException(sprintf(
288 3
                'The mapping property max depth must be a positive integer, got "%s".',
289 3
                is_object($maxDepth) ? get_class($maxDepth) : gettype($maxDepth)
290 2
            ));
291
        }
292
293 51
        $maxDepth = (int) $maxDepth;
294
295 51
        if ($maxDepth <= 0) {
296 3
            throw new \InvalidArgumentException(sprintf(
297 3
                'The mapping property max depth must be a positive integer, got "%d".',
298
                $maxDepth
299 2
            ));
300
        }
301
302 48
        $propertyMetadata->setMaxDepth($maxDepth);
303 48
    }
304
305
    /**
306
     * @param PropertyMetadataInterface $propertyMetadata
307
     * @param string[]                  $groups
308
     */
309 84
    private function loadPropertyMetadataGroups(PropertyMetadataInterface $propertyMetadata, $groups)
310
    {
311 84
        if (!is_array($groups)) {
312 3
            throw new \InvalidArgumentException(sprintf(
313 3
                'The mapping property groups must be an array of non empty strings, got "%s".',
314 3
                is_object($groups) ? get_class($groups) : gettype($groups)
315 2
            ));
316
        }
317
318 81
        foreach ($groups as $group) {
319 81
            if (!is_string($group)) {
320 3
                throw new \InvalidArgumentException(sprintf(
321 3
                    'The mapping property groups must be an array of non empty strings, got "%s".',
322 3
                    is_object($group) ? get_class($group) : gettype($group)
323 2
                ));
324
            }
325
326 78
            $group = trim($group);
327
328 78
            if (empty($group)) {
329 3
                throw new \InvalidArgumentException(
330 1
                    'The mapping property groups must be an array of non empty strings.'
331 2
                );
332
            }
333
334 75
            $propertyMetadata->addGroup($group);
335 50
        }
336 75
    }
337
338
    /**
339
     * @param mixed[] $data
340
     *
341
     * @return string|null
342
     */
343 738
    private function getExclusionPolicy(array $data)
344
    {
345 738
        if (!isset($data['exclusion_policy'])) {
346 669
            return ExclusionPolicy::NONE;
347
        }
348
349 69
        $policy = $data['exclusion_policy'];
350
351 69
        if (!is_string($policy)) {
352 3
            throw new \InvalidArgumentException(sprintf(
353 3
                'The mapping exclusion policy must be "%s" or "%s", got "%s".',
354 3
                ExclusionPolicy::ALL,
355 3
                ExclusionPolicy::NONE,
356 3
                is_object($policy) ? get_class($policy) : gettype($policy)
357 2
            ));
358
        }
359
360 66
        $policy = strtolower(trim($policy));
361
362 66
        if ($policy !== ExclusionPolicy::ALL && $policy !== ExclusionPolicy::NONE) {
363 6
            throw new \InvalidArgumentException(sprintf(
364 6
                'The mapping exclusion policy must be "%s" or "%s", got "%s".',
365 6
                ExclusionPolicy::ALL,
366 6
                ExclusionPolicy::NONE,
367
                $policy
368 4
            ));
369
        }
370
371 60
        return $policy;
372
    }
373
374
    /**
375
     * @param mixed[]                     $data
376
     * @param PropertyMetadataInterface[] $properties
377
     *
378
     * @return string|string[]|null
379
     */
380 666
    private function getOrder(array $data, array $properties)
381
    {
382 666
        if (!isset($data['order'])) {
383 549
            return;
384
        }
385
386 117
        $order = $data['order'];
387
388 117
        if (is_string($order)) {
389 72
            $order = trim($order);
390
391 72
            if (empty($order)) {
392 3
                throw new \InvalidArgumentException(
393 1
                    'The mapping order must be an non empty strings or an array of non empty strings.'
394 2
                );
395
            }
396
397 69
            if (strcasecmp($order, 'ASC') === 0 || strcasecmp($order, 'DESC') === 0) {
398 60
                return strtoupper($order);
399
            }
400
401 9
            $order = explode(',', $order);
402 51
        } elseif (!is_array($order)) {
403 3
            throw new \InvalidArgumentException(
404 1
                'The mapping order must be an non empty strings or an array of non empty strings.'
405 2
            );
406
        }
407
408 51
        if (empty($order)) {
409 6
            throw new \InvalidArgumentException(
410 2
                'The mapping order must be an non empty strings or an array of non empty strings.'
411 4
            );
412
        }
413
414 45
        foreach ($order as &$property) {
415 45
            if (!is_string($property)) {
416 3
                throw new \InvalidArgumentException(sprintf(
417 3
                    'The mapping order must be an non empty strings or an array of non empty strings, got "%s".',
418 3
                    is_object($property) ? get_class($property) : gettype($property)
419 2
                ));
420
            }
421
422 42
            $property = trim($property);
423
424 42
            if (empty($property)) {
425 6
                throw new \InvalidArgumentException(
426 2
                    'The mapping order must be an non empty strings or an array of non empty strings.'
427 4
                );
428
            }
429
430 36
            if (!isset($properties[$property])) {
431 6
                throw new \InvalidArgumentException(sprintf(
432 16
                    'The property "%s" defined in the mapping order does not exist.',
433
                    $property
434 4
                ));
435
            }
436 20
        }
437
438 30
        return $order;
439
    }
440
441
    /**
442
     * @param PropertyMetadataInterface[] $properties
443
     * @param string|string[]             $order
444
     *
445
     * @return PropertyMetadataInterface[]
446
     */
447 90
    private function sortProperties(array $properties, $order)
448
    {
449 90
        if (is_string($order)) {
450 60
            if ($order === 'ASC') {
451 30
                ksort($properties);
452 20
            } else {
453 40
                krsort($properties);
454
            }
455 70
        } elseif (is_array($order)) {
456 30
            $properties = array_merge(array_flip($order), $properties);
457 20
        }
458
459 90
        return $properties;
460
    }
461
462
    /**
463
     * @param bool $exclude
464
     */
465 36 View Code Duplication
    private function validatePropertyMetadataExclude($exclude)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
466
    {
467 36
        if (!is_bool($exclude)) {
468 6
            throw new \InvalidArgumentException(sprintf(
469 6
                'The mapping property exclude must be a boolean, got "%s".',
470 6
                is_object($exclude) ? get_class($exclude) : gettype($exclude)
471 4
            ));
472
        }
473 30
    }
474
475
    /**
476
     * @param bool $expose
477
     */
478 36 View Code Duplication
    private function validatePropertyMetadataExpose($expose)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
479
    {
480 36
        if (!is_bool($expose)) {
481 6
            throw new \InvalidArgumentException(sprintf(
482 6
                'The mapping property expose must be a boolean, got "%s".',
483 6
                is_object($expose) ? get_class($expose) : gettype($expose)
484 4
            ));
485
        }
486 30
    }
487
488
    /**
489
     * @param mixed[] $property
490
     * @param string  $policy
491
     *
492
     * @return bool
493
     */
494 666
    private function isPropertyMetadataExposed($property, $policy)
495
    {
496 666
        $expose = isset($property['expose']) && $property['expose'];
497 666
        $exclude = isset($property['exclude']) && $property['exclude'];
498
499 666
        return ($policy === ExclusionPolicy::ALL && $expose) || ($policy === ExclusionPolicy::NONE && !$exclude);
500
    }
501
}
502