Passed
Push — master ( f63313...36a266 )
by Michael
05:08
created

AnnotationDefinitionProvider::getDefinition()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 10
ccs 5
cts 5
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 1
crap 2
1
<?php
2
declare(strict_types = 1);
3
4
namespace Mikemirten\Component\JsonApi\Mapper\Definition;
5
6
use Doctrine\Common\Annotations\AnnotationRegistry;
7
use Doctrine\Common\Annotations\Reader;
8
use Mikemirten\Component\JsonApi\Mapper\Definition\Annotation\ResourceIdentifier as ResourceIdentifierAnnotation;
9
use Mikemirten\Component\JsonApi\Mapper\Definition\Annotation\Relationship as RelationshipAnnotation;
10
use Mikemirten\Component\JsonApi\Mapper\Definition\Annotation\Attribute as AttributeAnnotation;
11
use Mikemirten\Component\JsonApi\Mapper\Definition\Annotation\Link as LinkAnnotation;
12
13
/**
14
 * Annotation Definition Provider based on the Doctrine-Annotation library
15
 *
16
 * @package Mikemirten\Component\JsonApi\Mapper\Definition
17
 */
18
class AnnotationDefinitionProvider implements DefinitionProviderInterface
19
{
20
    /**
21
     * Pattern of "resource" parameter of link annotation
22
     */
23
    const RESOURCE_PATTERN = '~^(?<repository>[a-z_][a-z0-9_]*)\.(?<link>[a-z_][a-z0-9_]*)$~i';
24
25
    /**
26
     * Pattern of "type" parameter of attribute annotation
27
     */
28
    const DATATYPE_PATTERN = '~^(?<type>[a-z_][a-z0-9_]*)\s*(?:\((?<params>[^\)]*)\))?$~i';
29
30
    /**
31
     * Annotation classes ha been registered.
32
     *
33
     * @var bool
34
     */
35
    static private $annotationsRegistered = false;
36
37
    /**
38
     * Cache of created definitions
39
     *
40
     * @var array
41
     */
42
    private $definitionCache = [];
43
44
    /**
45
     * Register annotation classes.
46
     * Supports a medieval-aged way of "autoloading" for the Doctrine Annotation library.
47
     */
48 18
    static protected function registerAnnotations()
0 ignored issues
show
Coding Style introduced by
As per PSR2, the static declaration should come after the visibility declaration.
Loading history...
49
    {
50 18
        if (self::$annotationsRegistered === false) {
51 1
            AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Relationship.php');
52 1
            AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Attribute.php');
53 1
            AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Link.php');
54
55 1
            self::$annotationsRegistered = true;
56
        }
57 18
    }
58
59
    /**
60
     * Doctrine annotation reader
61
     *
62
     * @var Reader
63
     */
64
    protected $reader;
65
66
    /**
67
     * AnnotationDefinitionProvider constructor.
68
     *
69
     * @param Reader $reader
70
     */
71 18
    public function __construct(Reader $reader)
72
    {
73 18
        self::registerAnnotations();
74
75 18
        $this->reader = $reader;
76 18
    }
77
78
    /**
79
     * {@inheritdoc}
80
     */
81 18
    public function getDefinition(string $class): Definition
82
    {
83 18
        if (! isset($this->definitionCache[$class])) {
84 18
            $reflection = new \ReflectionClass($class);
85
86 18
            $this->definitionCache[$class] = $this->createDefinition($reflection);
87
        }
88
89 18
        return $this->definitionCache[$class];
90
    }
91
92
    /**
93
     * Create definition for given class
94
     *
95
     * @param  \ReflectionClass $reflection
96
     * @return Definition
97
     */
98 18
    protected function createDefinition(\ReflectionClass $reflection): Definition
99
    {
100 18
        $definition = new Definition($reflection->getName());
0 ignored issues
show
Bug introduced by
Consider using $reflection->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
101
102 18
        $this->processClassAnnotations($reflection, $definition);
103 18
        $this->processProperties($reflection, $definition);
104 18
        $this->processMethods($reflection, $definition);
105
106 18
        $parent = $reflection->getParentClass();
107
108 18
        if ($parent !== false) {
109 1
            $definition->merge($this->createDefinition($parent));
0 ignored issues
show
Documentation introduced by
$this->createDefinition($parent) is of type object<Mikemirten\Compon...\Definition\Definition>, but the function expects a object<self>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
110
        }
111
112 18
        foreach ($reflection->getTraits() as $trait)
113
        {
114 1
            $definition->merge($this->createDefinition($trait));
0 ignored issues
show
Documentation introduced by
$this->createDefinition($trait) is of type object<Mikemirten\Compon...\Definition\Definition>, but the function expects a object<self>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
115
        }
116
117 18
        return $definition;
118
    }
119
120
    /**
121
     * Process properties of class
122
     *
123
     * @param \ReflectionClass $reflection
124
     * @param Definition       $definition
125
     */
126 18
    protected function processProperties(\ReflectionClass $reflection, Definition $definition)
127
    {
128 18
        foreach ($reflection->getProperties() as $property)
129
        {
130 16
            $this->processProperty($property, $definition);
131
        }
132 18
    }
133
134
    /**
135
     * Process property of class
136
     *
137
     * @param \ReflectionProperty $property
138
     * @param Definition          $definition
139
     */
140 16
    protected function processProperty(\ReflectionProperty $property, Definition $definition)
141
    {
142 16
        $annotations = $this->reader->getPropertyAnnotations($property);
143
144 16
        foreach ($annotations as $annotation)
145
        {
146 11
            if ($annotation instanceof AttributeAnnotation) {
147 2
                $attribute = $this->createAttributeByProperty($annotation, $property);
148
149 2
                $definition->addAttribute($attribute);
150 2
                continue;
151
            }
152
153 9
            if ($annotation instanceof RelationshipAnnotation) {
154 9
                $relationship = $this->createRelationship($annotation, $property);
155
156 9
                $definition->addRelationship($relationship);
157
            }
158
        }
159 16
    }
160
161
    /**
162
     * Process methods of class
163
     *
164
     * @param \ReflectionClass $reflection
165
     * @param Definition       $definition
166
     */
167 18
    protected function processMethods(\ReflectionClass $reflection, Definition $definition)
168
    {
169 18
        foreach ($reflection->getMethods() as $method)
170
        {
171 18
            $this->processMethod($method, $definition);
172
        }
173 18
    }
174
175
    /**
176
     * Process method of class
177
     *
178
     * @param \ReflectionMethod $method
179
     * @param Definition        $definition
180
     */
181 18
    protected function processMethod(\ReflectionMethod $method, Definition $definition)
182
    {
183 18
        $annotations = $this->reader->getMethodAnnotations($method);
184
185 18
        foreach ($annotations as $annotation)
186
        {
187 2
            if ($annotation instanceof AttributeAnnotation) {
188 2
                $this->validateMethodAttribute($annotation, $method);
189
190 2
                $attribute = $this->createAttributeByMethod($annotation, $method);
191 2
                $definition->addAttribute($attribute);
192
            }
193
        }
194 18
    }
195
196
    /**
197
     * Validate method with attribute definition
198
     *
199
     * @param  AttributeAnnotation $annotation
200
     * @param  \ReflectionMethod   $method
201
     * @throws \LogicException
202
     */
203 2
    protected function validateMethodAttribute(AttributeAnnotation $annotation, \ReflectionMethod $method)
204
    {
205 2
        if (! $method->isPublic()) {
206
            throw new \LogicException(sprintf(
207
                'Attribute annotation can be applied only to non public method "%s".',
208
                $method->getName()
0 ignored issues
show
Bug introduced by
Consider using $method->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
209
            ));
210
        }
211
212 2
        if ($annotation->getter !== null) {
213
            throw new \LogicException(sprintf(
214
                'The "getter" property of Attribute annotation applied to method "%s" is useless.',
215
                $method->getName()
0 ignored issues
show
Bug introduced by
Consider using $method->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
216
            ));
217
        }
218 2
    }
219
220
    /**
221
     * Process annotations of class
222
     *
223
     * @param \ReflectionClass $reflection
224
     * @param Definition       $definition
225
     */
226 18
    protected function processClassAnnotations(\ReflectionClass $reflection, Definition $definition)
227
    {
228 18
        $annotations = $this->reader->getClassAnnotations($reflection);
229
230 18
        foreach ($annotations as $annotation)
231
        {
232 8
            if ($annotation instanceof LinkAnnotation) {
233 7
                $link = $this->createLink($annotation);
234
235 7
                $definition->addLink($link);
236 7
                continue;
237
            }
238
239 7
            if ($annotation instanceof ResourceIdentifierAnnotation) {
240 7
                $this->handlerResourceIdentifier($annotation, $definition);
241
            }
242
        }
243 18
    }
244
245
    /**
246
     * Handler resource identifier
247
     *
248
     * @param ResourceIdentifierAnnotation $annotation
249
     * @param Definition                   $definition
250
     */
251 7
    protected function handlerResourceIdentifier(ResourceIdentifierAnnotation $annotation, Definition $definition)
252
    {
253 7
        if ($annotation->type !== null) {
254 7
            $definition->setType($annotation->type);
255
        }
256 7
    }
257
258
    /**
259
     * Create attribute by annotation of property
260
     *
261
     * @param  AttributeAnnotation $annotation
262
     * @param  \ReflectionProperty $property
263
     * @return Attribute
264
     */
265 2
    protected function createAttributeByProperty(AttributeAnnotation $annotation, \ReflectionProperty $property): Attribute
266
    {
267 2
        $name = ($annotation->name === null)
268 2
            ? $property->getName()
269 2
            : $annotation->name;
270
271 2
        $getter = ($annotation->getter === null)
272 2
            ? $this->resolveGetter($property)
273 2
            : $annotation->getter;
274
275 2
        $attribute = new Attribute($name, $getter);
276 2
        $attribute->setPropertyName($property->getName());
277
278 2
        if ($annotation->type !== null) {
279 2
            $this->processDataType($annotation->type, $attribute);
280
        }
281
282 2
        return $attribute;
283
    }
284
285
    /**
286
     * Create attribute by annotation of method
287
     *
288
     * @param  AttributeAnnotation $annotation
289
     * @param  \ReflectionMethod   $method
290
     * @return Attribute
291
     */
292 2
    protected function createAttributeByMethod(AttributeAnnotation $annotation, \ReflectionMethod $method): Attribute
293
    {
294 2
        $name = ($annotation->name === null)
295 2
            ? $this->resolveNameByMethod($method)
296 2
            : $annotation->name;
297
298 2
        $attribute = new Attribute($name, $method->getName());
0 ignored issues
show
Bug introduced by
Consider using $method->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
299
300 2
        if ($annotation->type !== null) {
301 2
            $this->processDataType($annotation->type, $attribute);
302
        }
303
304 2
        return $attribute;
305
    }
306
307
    /**
308
     * Resolve name of attribute by method
309
     *
310
     * @param  \ReflectionMethod $method
311
     * @return string
312
     */
313 2
    protected function resolveNameByMethod(\ReflectionMethod $method): string
314
    {
315 2
        $name = $method->getName();
0 ignored issues
show
Bug introduced by
Consider using $method->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
316
317 2
        if (preg_match('~^(?:get|is)(?<name>[a-z0-9_]+)~i', $name, $matches)) {
318 2
            return lcfirst($matches['name']);
319
        }
320
321
        return $name;
322
    }
323
324
    /**
325
     * Process data-type
326
     *
327
     * @param string    $definition
328
     * @param Attribute $attribute
329
     */
330 4
    protected function processDataType(string $definition, Attribute $attribute)
331
    {
332 4
        if (! preg_match(self::DATATYPE_PATTERN, $definition, $matches)) {
333
            throw new \LogicException(sprintf('Data-type definition "%s" is invalid.', $definition));
334
        }
335
336 4
        $attribute->setType($matches['type']);
337
338 4
        if (empty($matches['params'])) {
339
            return;
340
        }
341
342 4
        $parameters = explode(',', $matches['params']);
343 4
        $parameters = array_map('trim', $parameters);
344
345 4
        $attribute->setTypeParameters($parameters);
346 4
    }
347
348
    /**
349
     * Process relationship
350
     *
351
     * @param  RelationshipAnnotation $annotation
352
     * @param  \ReflectionProperty    $property
353
     * @return Relationship
354
     */
355 9
    protected function createRelationship(RelationshipAnnotation $annotation, \ReflectionProperty $property): Relationship
356
    {
357 9
        $name = ($annotation->name === null)
358 7
            ? $property->getName()
359 9
            : $annotation->name;
360
361 9
        $type = $this->resolveType($annotation);
362
363 9
        $getter = ($annotation->getter === null)
364 9
            ? $this->resolveGetter($property)
365 9
            : $annotation->getter;
366
367 9
        $relationship = new Relationship($name, $type, $getter);
368 9
        $relationship->setPropertyName($property->getName());
369
370 9
        $this->handleLinks($annotation, $relationship);
371 9
        $this->handleDataControl($annotation, $relationship);
372
373 9
        return $relationship;
374
    }
375
376
    /**
377
     * Resolve getter of related object
378
     *
379
     * @param  \ReflectionProperty $property
380
     * @return string
381
     */
382 11
    protected function resolveGetter(\ReflectionProperty $property): string
383
    {
384 11
        $name  = $property->getName();
385 11
        $class = $property->getDeclaringClass();
386
387 11
        foreach (['get', 'is'] as $prefix)
388
        {
389 11
            $getter = $prefix . ucfirst($name);
390
391 11
            if ($class->hasMethod($getter) && $class->getMethod($getter)->isPublic()) {
392 11
                return $getter;
393
            }
394
        }
395
396
        throw new \LogicException(sprintf(
397
            'Getter-method for the property "%s" cannot be resolved automatically. ' .
398
            'Probably there is no get%2$s() or is%2$s() method or it is not public.',
399
            $name, ucfirst($name)
400
        ));
401
    }
402
403
    /**
404
     * Handle links
405
     *
406
     * @param RelationshipAnnotation $annotation
407
     * @param Relationship           $relationship
408
     */
409 9
    protected function handleLinks(RelationshipAnnotation $annotation, Relationship $relationship)
410
    {
411 9
        foreach ($annotation->links as $linkAnnotation)
412
        {
413 7
            $link = $this->createLink($linkAnnotation);
414
415 7
            $relationship->addLink($link);
416
        }
417 9
    }
418
419
    /**
420
     * Create link by link's annotation
421
     *
422
     * @param  LinkAnnotation $annotation
423
     * @return Link
424
     */
425 8
    protected function createLink(LinkAnnotation $annotation): Link
426
    {
427 8
        if (! preg_match(self::RESOURCE_PATTERN, $annotation->resource, $matches)) {
428
            throw new \LogicException(sprintf('Invalid resource definition: "%s"', $annotation->resource));
429
        }
430
431 8
        $link = new Link(
432 8
            $annotation->name,
433 8
            $matches['repository'],
434 8
            $matches['link']
435
        );
436
437 8
        $link->setParameters($annotation->parameters);
438 8
        $link->setMetadata($annotation->metadata);
439
440 8
        return $link;
441
    }
442
443
    /**
444
     * Handle control of data-section
445
     *
446
     * @param RelationshipAnnotation $annotation
447
     * @param Relationship           $relationship
448
     */
449 9
    protected function handleDataControl(RelationshipAnnotation $annotation, Relationship $relationship)
450
    {
451 9
        $relationship->setIncludeData($annotation->dataAllowed);
452 9
        $relationship->setDataLimit($annotation->dataLimit);
453 9
    }
454
455
    /**
456
     * Resolve type of relationship
457
     *
458
     * @param  RelationshipAnnotation $annotation
459
     * @return int
460
     */
461 9
    protected function resolveType(RelationshipAnnotation $annotation): int
462
    {
463 9
        if ($annotation->type === RelationshipAnnotation::TYPE_ONE) {
464 2
            return Relationship::TYPE_X_TO_ONE;
465
        }
466
467 7
        if ($annotation->type === RelationshipAnnotation::TYPE_MANY) {
468 7
            return Relationship::TYPE_X_TO_MANY;
469
        }
470
471
        throw new \LogicException(sprintf('Invalid type of relation "%s" defined.', $annotation->type));
472
    }
473
}