GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — master ( 796de0...1e3025 )
by Sergey
22s queued 10s
created

AnnotationLoader   F

Complexity

Total Complexity 82

Size/Duplication

Total Lines 470
Duplicated Lines 11.91 %

Coupling/Cohesion

Components 1
Dependencies 11

Test Coverage

Coverage 88.53%

Importance

Changes 0
Metric Value
wmc 82
lcom 1
cbo 11
dl 56
loc 470
ccs 193
cts 218
cp 0.8853
rs 2
c 0
b 0
f 0

15 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
C loadResourceMetadata() 0 42 12
B loadObjectMetadata() 26 40 8
B loadDocumentMetadata() 0 23 6
A loadClassMetadata() 0 18 5
A loadPropertyMetadata() 0 31 4
A loadVirtualMetadata() 0 23 2
A parseDataType() 8 20 5
A parseVirtualDataType() 8 18 4
B parseDataTypeString() 0 43 10
A isScalarDataType() 0 4 1
B loadDiscriminatorMetadata() 0 30 9
B getDataPath() 7 21 6
B getVirtualDataPath() 7 21 6
A parseLoaders() 0 17 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like AnnotationLoader 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 AnnotationLoader, and based on these observations, apply Extract Interface, too.

1
<?php
2
/*
3
 * This file is part of the reva2/jsonapi.
4
 *
5
 * (c) Sergey Revenko <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
12
namespace Reva2\JsonApi\Decoders\Mapping\Loader;
13
14
use Doctrine\Common\Annotations\Reader;
15
use Reva2\JsonApi\Annotations\Attribute;
16
use Reva2\JsonApi\Annotations\ApiDocument;
17
use Reva2\JsonApi\Annotations\Id;
18
use Reva2\JsonApi\Annotations\ApiResource;
19
use Reva2\JsonApi\Annotations\ApiObject;
20
use Reva2\JsonApi\Annotations\Content as ApiContent;
21
use Reva2\JsonApi\Annotations\Loader;
22
use Reva2\JsonApi\Annotations\Metadata;
23
use Reva2\JsonApi\Annotations\Property;
24
use Reva2\JsonApi\Annotations\Relationship;
25
use Reva2\JsonApi\Annotations\VirtualAttribute;
26
use Reva2\JsonApi\Annotations\VirtualProperty;
27
use Reva2\JsonApi\Annotations\VirtualRelationship;
28
use Reva2\JsonApi\Contracts\Decoders\Mapping\Loader\LoaderInterface;
29
use Reva2\JsonApi\Contracts\Decoders\Mapping\ObjectMetadataInterface;
30
use Reva2\JsonApi\Decoders\Mapping\ClassMetadata;
31
use Reva2\JsonApi\Decoders\Mapping\DocumentMetadata;
32
use Reva2\JsonApi\Decoders\Mapping\ObjectMetadata;
33
use Reva2\JsonApi\Decoders\Mapping\PropertyMetadata;
34
use Reva2\JsonApi\Decoders\Mapping\ResourceMetadata;
35
36
/**
37
 * Loads JSON API metadata using a Doctrine annotations
38
 *
39
 * @package Reva2\JsonApi\Decoders\Mapping\Loader
40
 * @author Sergey Revenko <[email protected]>
41
 */
42
class AnnotationLoader implements LoaderInterface
43
{
44
    /**
45
     * @var Reader
46
     */
47
    protected $reader;
48
49
    /**
50
     * Constructor
51
     *
52
     * @param Reader $reader
53
     */
54 29
    public function __construct(Reader $reader)
55
    {
56 29
        $this->reader = $reader;
57 29
    }
58
59
    /**
60
     * @inheritdoc
61
     */
62 18
    public function loadClassMetadata(\ReflectionClass $class)
63
    {
64 18
        if (class_exists('Doctrine\ORM\Proxy\Proxy') && $class->implementsInterface('Doctrine\ORM\Proxy\Proxy')) {
65
            return $this->loadClassMetadata($class->getParentClass());
66
        }
67
68 18
        if (null !== ($resource = $this->reader->getClassAnnotation($class, ApiResource::class))) {
69
            /* @var $resource ApiResource */
70 10
            return $this->loadResourceMetadata($resource, $class);
71 11
        } elseif (null !== ($document = $this->reader->getClassAnnotation($class, ApiDocument::class))) {
72
            /* @var $document ApiDocument */
73 4
            return $this->loadDocumentMetadata($document, $class);
74
        } else {
75 9
            $object = $this->reader->getClassAnnotation($class, ApiObject::class);
76
77 9
            return $this->loadObjectMetadata($class, $object);
78
        }
79
    }
80
81
    /**
82
     * Parse JSON API resource metadata
83
     *
84
     * @param ApiResource $resource
85
     * @param \ReflectionClass $class
86
     * @return ResourceMetadata
87
     */
88 10
    private function loadResourceMetadata(ApiResource $resource, \ReflectionClass $class)
89
    {
90 10
        $metadata = new ResourceMetadata($class->name);
91 10
        $metadata->setName($resource->name);
92 10
        $metadata->setLoader($resource->loader);
93
94 10
        $properties = $class->getProperties();
95 10
        foreach ($properties as $property) {
96 10
            if ($property->getDeclaringClass()->name !== $class->name) {
97 5
                continue;
98
            }
99
100 10
            foreach ($this->reader->getPropertyAnnotations($property) as $annotation) {
101 10
                if ($annotation instanceof Attribute) {
102 10
                    $metadata->addAttribute($this->loadPropertyMetadata($annotation, $property));
103 10
                } elseif ($annotation instanceof Relationship) {
104 10
                    $metadata->addRelationship($this->loadPropertyMetadata($annotation, $property));
105 10
                } elseif ($annotation instanceof Id) {
106 10
                    $metadata->setIdMetadata($this->loadPropertyMetadata($annotation, $property));
107
                }
108
            }
109
        }
110
111 10
        $methods = $class->getMethods(\ReflectionMethod::IS_PUBLIC);
112 10
        foreach ($methods as $method) {
113 9
            if ($method->getDeclaringClass()->name !== $class->name) {
114 5
                continue;
115
            }
116
117 9
            foreach ($this->reader->getMethodAnnotations($method) as $annotation) {
118 9
                if ($annotation instanceof VirtualAttribute) {
119 9
                    $metadata->addAttribute($this->loadVirtualMetadata($annotation, $method));
120 9
                } else if ($annotation instanceof VirtualRelationship) {
121 9
                    $metadata->addRelationship($this->loadVirtualMetadata($annotation, $method));
122
                }
123
            }
124
        }
125
126 10
        $this->loadDiscriminatorMetadata($resource, $metadata);
127
128 10
        return $metadata;
129
    }
130
131
    /**
132
     * @param \ReflectionClass $class
133
     * @param ApiObject|null $object
134
     * @return ObjectMetadata
135
     */
136 9
    private function loadObjectMetadata(\ReflectionClass $class, ApiObject $object = null)
137
    {
138 9
        $metadata = new ObjectMetadata($class->name);
139
140 9
        $properties = $class->getProperties();
141 9 View Code Duplication
        foreach ($properties as $property) {
142 9
            if ($property->getDeclaringClass()->name !== $class->name) {
143 5
                continue;
144
            }
145
146 9
            $annotation = $this->reader->getPropertyAnnotation($property, Property::class);
147
            /* @var $annotation Property */
148 9
            if (null === $annotation) {
149
                continue;
150
            }
151
152 9
            $metadata->addProperty($this->loadPropertyMetadata($annotation, $property));
153
        }
154
155 7
        $methods = $class->getMethods(\ReflectionMethod::IS_PUBLIC);
156 7 View Code Duplication
        foreach ($methods as $method) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
157 6
            if ($method->getDeclaringClass()->name !== $class->name) {
158 5
                continue;
159
            }
160
161 6
            $annotation = $this->reader->getMethodAnnotation($method, VirtualProperty::class);
162
            /* @var $annotation VirtualProperty */
163 6
            if ($annotation === null) {
164 6
                continue;
165
            }
166
167 2
            $metadata->addProperty($this->loadVirtualMetadata($annotation, $method));
168
        }
169
170 7
        if (null !== $object) {
171 3
            $this->loadDiscriminatorMetadata($object, $metadata);
172
        }
173
174 7
        return $metadata;
175
    }
176
177
    /**
178
     * Parse JSON API document metadata
179
     *
180
     * @param ApiDocument $document
181
     * @param \ReflectionClass $class
182
     * @return DocumentMetadata
183
     */
184 4
    private function loadDocumentMetadata(ApiDocument $document, \ReflectionClass $class)
185
    {
186 4
        $metadata = new DocumentMetadata($class->name);
187 4
        $metadata->setAllowEmpty($document->allowEmpty);
188
189 4
        $properties = $class->getProperties();
190 4
        foreach ($properties as $property) {
191 4
            if ($property->getDeclaringClass()->name !== $class->name) {
192
                continue;
193
            }
194
195
196 4
            foreach ($this->reader->getPropertyAnnotations($property) as $annotation) {
197 4
                if ($annotation instanceof ApiContent) {
198 4
                    $metadata->setContentMetadata($this->loadPropertyMetadata($annotation, $property));
199 3
                } elseif ($annotation instanceof Metadata) {
200 3
                    $metadata->setMetadata($this->loadPropertyMetadata($annotation, $property));
201
                }
202
            }
203
        }
204
205 4
        return $metadata;
206
    }
207
208
    /**
209
     * Parse property metadata
210
     *
211
     * @param Property $annotation
212
     * @param \ReflectionProperty $property
213
     * @return PropertyMetadata
214
     */
215 18
    private function loadPropertyMetadata(Property $annotation, \ReflectionProperty $property)
216
    {
217 18
        $metadata = new PropertyMetadata($property->name, $property->class);
218
219 18
        list($dataType, $dataTypeParams) = $this->parseDataType($annotation, $property);
220
221
        $metadata
222 17
            ->setDataType($dataType)
223 17
            ->setDataTypeParams($dataTypeParams)
224 17
            ->setDataPath($this->getDataPath($annotation, $property))
225 17
            ->setConverter($annotation->converter)
226 17
            ->setGroups($annotation->groups)
227 17
            ->setLoaders($this->parseLoaders($annotation->loaders));
228
229 17
        if ($annotation->setter) {
230
            $metadata->setSetter($annotation->setter);
231 17
        } elseif (false === $property->isPublic()) {
232 9
            $setter = 'set' . ucfirst($property->name);
233 9
            if (false === $property->getDeclaringClass()->hasMethod($setter)) {
234 1
                throw new \RuntimeException(sprintf(
235 1
                    "Couldn't find setter for non public property: %s:%s",
236 1
                    $property->class,
237 1
                    $property->name
238
                ));
239
            }
240
241 8
            $metadata->setSetter($setter);
242
        }
243
244 16
        return $metadata;
245
    }
246
247
    /**
248
     * Parse virtual property metadata
249
     *
250
     * @param VirtualProperty $annotation
251
     * @param \ReflectionMethod $method
252
     * @return PropertyMetadata
253
     */
254 11
    private function loadVirtualMetadata(VirtualProperty $annotation, \ReflectionMethod $method)
255
    {
256 11
        if (empty($annotation->name)) {
257
            throw new \InvalidArgumentException(sprintf(
258
                "Virtual property name not specified: %s:%s()",
259
                $method->class,
260
                $method->name
261
            ));
262
        }
263
264 11
        list($dataType, $dataTypeParams) = $this->parseVirtualDataType($annotation, $method);
265
266 11
        $metadata = new PropertyMetadata($annotation->name, $method->class);
267
        $metadata
268 11
            ->setDataType($dataType)
269 11
            ->setDataTypeParams($dataTypeParams)
270 11
            ->setDataPath($this->getVirtualDataPath($annotation, $method))
271 11
            ->setConverter($annotation->converter)
272 11
            ->setGroups($annotation->groups)
273 11
            ->setSetter($method->name);
274
275 11
        return $metadata;
276
    }
277
278
    /**
279
     * Parse property data type
280
     *
281
     * @param Property $annotation
282
     * @param \ReflectionProperty $property
283
     * @return array
284
     */
285 18
    private function parseDataType(Property $annotation, \ReflectionProperty $property)
286
    {
287 18
        if (!empty($annotation->parser)) {
288 6 View Code Duplication
            if (!$property->getDeclaringClass()->hasMethod($annotation->parser)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
289 1
                throw new \InvalidArgumentException(sprintf(
290 1
                    "Custom parser function %s:%s() for property '%s' does not exist",
291 1
                    $property->class,
292 1
                    $annotation->parser,
293 1
                    $property->name
294
                ));
295
            }
296 5
            return ['custom', $annotation->parser];
297 17
        } elseif (!empty($annotation->type)) {
298 15
            return $this->parseDataTypeString($annotation->type);
299 15
        } elseif (preg_match('~@var\s(.*?)\s~si', $property->getDocComment(), $matches)) {
300 15
            return $this->parseDataTypeString($matches[1]);
301
        } else {
302 2
            return ['raw', null];
303
        }
304
    }
305
306
    /**
307
     * Parse virtual property data type
308
     *
309
     * @param VirtualProperty $annotation
310
     * @param \ReflectionMethod $method
311
     * @return array
312
     */
313 11
    private function parseVirtualDataType(VirtualProperty $annotation, \ReflectionMethod $method)
314
    {
315 11
        if (!empty($annotation->parser)) {
316 View Code Duplication
            if (!$method->getDeclaringClass()->hasMethod($annotation->parser)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
317
                throw new \InvalidArgumentException(sprintf(
318
                    "Custom parser function %s:%s() for virtual property '%s' does not exist",
319
                    $method->class,
320
                    $annotation->parser,
321
                    $annotation->name
322
                ));
323
            }
324
            return ['custom', $annotation->parser];
325 11
        } elseif (!empty($annotation->type)) {
326 11
            return $this->parseDataTypeString($annotation->type);
327
        } else {
328
            return ['raw', null];
329
        }
330
    }
331
332
    /**
333
     * Parse data type string
334
     *
335
     * @param string $type
336
     * @return array
337
     */
338 17
    private function parseDataTypeString($type)
339
    {
340 17
        $params = null;
341
342 17
        if ('raw' === $type) {
343 2
            $dataType = 'raw';
344 2
            $params = null;
345 17
        } elseif ($this->isScalarDataType($type)) {
346 16
            $dataType = 'scalar';
347 16
            $params = $type;
348 13
        } elseif (preg_match('~^DateTime(<(.*?)>)?$~', $type, $matches)) {
349 2
            $dataType = 'datetime';
350 2
            if (3 === count($matches)) {
351 2
                $params = $matches[2];
352
            }
353
        } elseif (
354 13
            (preg_match('~Array(<(.*?)>)?$~si', $type, $matches)) ||
355 13
            (preg_match('~^(.*?)\[\]$~si', $type, $matches))
356
        ) {
357 13
            $dataType = 'array';
358 13
            if (3 === count($matches)) {
359 5
                $params = $this->parseDataTypeString($matches[2]);
360 12
            } elseif (2 === count($matches)) {
361 12
                $params = $this->parseDataTypeString($matches[1]);
362
            } else {
363 13
                $params = ['raw', null];
364
            }
365
        } else {
366 13
            $type = ltrim($type, '\\');
367
368 13
            if (!class_exists($type)) {
369
                throw new \InvalidArgumentException(sprintf(
370
                    "Unknown object type '%s' specified",
371
                    $type
372
                ));
373
            }
374
375 13
            $dataType = 'object';
376 13
            $params = $type;
377
        }
378
379 17
        return [$dataType, $params];
380
    }
381
382
    /**
383
     * Returns true if specified type scalar. False otherwise.
384
     *
385
     * @param string $type
386
     * @return bool
387
     */
388 17
    private function isScalarDataType($type)
389
    {
390 17
        return in_array($type, ['string', 'bool', 'boolean', 'int', 'integer', 'float', 'double']);
391
    }
392
393
    /**
394
     * Load discriminator metadata
395
     *
396
     * @param ApiObject $object
397
     * @param ClassMetadata $metadata
398
     */
399 12
    private function loadDiscriminatorMetadata(ApiObject $object, ClassMetadata $metadata)
400
    {
401 12
        if (!$object->discField) {
402 6
            return;
403
        }
404
405 11
        $fieldMeta = null;
406 11
        $field = $object->discField;
407 11
        if ($metadata instanceof ObjectMetadataInterface) {
408 2
            $properties = $metadata->getProperties();
409 2
            if (array_key_exists($field, $properties)) {
410 2
                $fieldMeta = $properties[$field];
411
            }
412 9
        } elseif ($metadata instanceof ResourceMetadata) {
413 9
            $attributes = $metadata->getAttributes();
414 9
            if (array_key_exists($field, $attributes)) {
415 9
                $fieldMeta = $attributes[$field];
416
            }
417
        }
418
419 11
        if (null === $fieldMeta) {
420
            throw new \InvalidArgumentException("Specified discriminator field not found in object properties");
421 11
        } elseif (('scalar' !== $fieldMeta->getDataType()) || ('string' !== $fieldMeta->getDataTypeParams())) {
422
            throw new \InvalidArgumentException("Discriminator field must point to property that contain string value");
423
        }
424
425 11
        $metadata->setDiscriminatorField($fieldMeta);
426 11
        $metadata->setDiscriminatorMap($object->discMap);
427 11
        $metadata->setDiscriminatorError($object->discError);
428 11
    }
429
430
    /**
431
     * Returns data path
432
     *
433
     * @param Property $annotation
434
     * @param \ReflectionProperty $property
435
     * @return string
436
     */
437 17
    private function getDataPath(Property $annotation, \ReflectionProperty $property)
438
    {
439 17
        $prefix = '';
440 17
        $suffix = '';
441 17
        if ($annotation instanceof Attribute) {
442 10
            $prefix = 'attributes.';
443 17
        } elseif ($annotation instanceof Relationship) {
444 10
            $prefix = 'relationships.';
445 10
            $suffix = '.data';
446
        }
447
448 17 View Code Duplication
        if (!empty($prefix) || !empty($suffix)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
449 10
            if (null !== $annotation->path) {
450
                return $prefix . $annotation->path . $suffix;
451
            }
452
453 10
            return $prefix . $property->name . $suffix;
454
        }
455
456 17
        return $annotation->path;
457
    }
458
459
    /**
460
     * Returns data path for virtual property
461
     *
462
     * @param VirtualProperty $annotation
463
     * @param \ReflectionMethod $method
464
     * @return string
465
     */
466 11
    private function getVirtualDataPath(VirtualProperty $annotation, \ReflectionMethod $method)
0 ignored issues
show
Unused Code introduced by
The parameter $method is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
467
    {
468 11
        $prefix = '';
469 11
        $suffix = '';
470 11
        if ($annotation instanceof VirtualAttribute) {
471 9
            $prefix = 'attributes.';
472 11
        } elseif ($annotation instanceof VirtualRelationship) {
473 9
            $prefix = 'relationships.';
474 9
            $suffix = '.data';
475
        }
476
477 11 View Code Duplication
        if (!empty($prefix) || !empty($suffix)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
478 9
            if (null !== $annotation->path) {
479
                return $prefix . $annotation->path . $suffix;
480
            }
481
482 9
            return $prefix . $annotation->name . $suffix;
483
        }
484
485 2
        return $annotation->path;
486
    }
487
488
    /**
489
     * Parse property loaders
490
     *
491
     * @param array|Loader[] $loaders
492
     * @return array
493
     */
494 17
    private function parseLoaders(array $loaders)
495
    {
496 17
        $propLoaders = [];
497
498 17
        foreach ($loaders as $loader) {
499 1
            if (array_key_exists($loader->group, $propLoaders)) {
500
                throw new \InvalidArgumentException(sprintf(
501
                    "Only one loader for serialization group '%s' can be specified",
502
                    $loader->group
503
                ));
504
            }
505
506 1
            $propLoaders[$loader->group] = $loader->loader;
507
        }
508
509 17
        return $propLoaders;
510
    }
511
}
512