Passed
Push — master ( da3841...78234a )
by Michael
03:01
created

AnnotationDefinitionProvider::processMethods()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 7
ccs 4
cts 4
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 3
nc 2
nop 2
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
        $setter = ($annotation->setter === null)
276 2
            ? $this->resolveSetter($property)
277 2
            : $annotation->setter;
278
279 2
        $attribute = new Attribute($name, $getter);
280 2
        $attribute->setPropertyName($property->getName());
281
282 2
        if ($setter !== null) {
283 2
            $attribute->setSetter($setter);
284
        }
285
286 2
        if ($annotation->type !== null) {
287 2
            $this->processDataType($annotation->type, $attribute);
288
        }
289
290 2
        return $attribute;
291
    }
292
293
    /**
294
     * Create attribute by annotation of method
295
     *
296
     * @param  AttributeAnnotation $annotation
297
     * @param  \ReflectionMethod   $method
298
     * @return Attribute
299
     */
300 2
    protected function createAttributeByMethod(AttributeAnnotation $annotation, \ReflectionMethod $method): Attribute
301
    {
302 2
        $name = ($annotation->name === null)
303 2
            ? $this->resolveNameByMethod($method)
304 2
            : $annotation->name;
305
306 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...
307
308 2
        if ($annotation->type !== null) {
309 2
            $this->processDataType($annotation->type, $attribute);
310
        }
311
312 2
        return $attribute;
313
    }
314
315
    /**
316
     * Resolve name of attribute by method
317
     *
318
     * @param  \ReflectionMethod $method
319
     * @return string
320
     */
321 2
    protected function resolveNameByMethod(\ReflectionMethod $method): string
322
    {
323 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...
324
325 2
        if (preg_match('~^(?:get|is)(?<name>[a-z0-9_]+)~i', $name, $matches)) {
326 2
            return lcfirst($matches['name']);
327
        }
328
329
        return $name;
330
    }
331
332
    /**
333
     * Process data-type
334
     *
335
     * @param string    $definition
336
     * @param Attribute $attribute
337
     */
338 4
    protected function processDataType(string $definition, Attribute $attribute)
339
    {
340 4
        if (! preg_match(self::DATATYPE_PATTERN, $definition, $matches)) {
341
            throw new \LogicException(sprintf('Data-type definition "%s" is invalid.', $definition));
342
        }
343
344 4
        $attribute->setType($matches['type']);
345
346 4
        if (empty($matches['params'])) {
347
            return;
348
        }
349
350 4
        $parameters = explode(',', $matches['params']);
351 4
        $parameters = array_map('trim', $parameters);
352
353 4
        $attribute->setTypeParameters($parameters);
354 4
    }
355
356
    /**
357
     * Process relationship
358
     *
359
     * @param  RelationshipAnnotation $annotation
360
     * @param  \ReflectionProperty    $property
361
     * @return Relationship
362
     */
363 9
    protected function createRelationship(RelationshipAnnotation $annotation, \ReflectionProperty $property): Relationship
364
    {
365 9
        $name = ($annotation->name === null)
366 7
            ? $property->getName()
367 9
            : $annotation->name;
368
369 9
        $type = $this->resolveType($annotation);
370
371 9
        $getter = ($annotation->getter === null)
372 9
            ? $this->resolveGetter($property)
373 9
            : $annotation->getter;
374
375 9
        $relationship = new Relationship($name, $type, $getter);
376 9
        $relationship->setPropertyName($property->getName());
377
378 9
        $this->handleLinks($annotation, $relationship);
379 9
        $this->handleDataControl($annotation, $relationship);
380
381 9
        return $relationship;
382
    }
383
384
    /**
385
     * Resolve getter of related object
386
     *
387
     * @param  \ReflectionProperty $property
388
     * @return string
389
     */
390 11
    protected function resolveGetter(\ReflectionProperty $property)
391
    {
392 11
        $name  = $property->getName();
393 11
        $class = $property->getDeclaringClass();
394
395 11
        foreach (['get', 'is'] as $prefix)
396
        {
397 11
            $getter = $prefix . ucfirst($name);
398
399 11
            if ($class->hasMethod($getter) && $class->getMethod($getter)->isPublic()) {
400 11
                return $getter;
401
            }
402
        }
403
404
        throw new \LogicException(sprintf(
405
            'Getter-method for the property "%s" cannot be resolved automatically. ' .
406
            'Probably there is no get%2$s() or is%2$s() method or it is not public.',
407
            $name, ucfirst($name)
408
        ));
409
    }
410
411
    /**
412
     * Resolve getter of related object
413
     *
414
     * @param  \ReflectionProperty $property
415
     * @return string | null
416
     */
417 2
    protected function resolveSetter(\ReflectionProperty $property)
418
    {
419 2
        $name  = $property->getName();
420 2
        $class = $property->getDeclaringClass();
421
422 2
        $setter = 'set' . ucfirst($name);
423
424 2
        if ($class->hasMethod($setter) && $class->getMethod($setter)->isPublic()) {
425 2
            return $setter;
426
        }
427
    }
428
429
    /**
430
     * Handle links
431
     *
432
     * @param RelationshipAnnotation $annotation
433
     * @param Relationship           $relationship
434
     */
435 9
    protected function handleLinks(RelationshipAnnotation $annotation, Relationship $relationship)
436
    {
437 9
        foreach ($annotation->links as $linkAnnotation)
438
        {
439 7
            $link = $this->createLink($linkAnnotation);
440
441 7
            $relationship->addLink($link);
442
        }
443 9
    }
444
445
    /**
446
     * Create link by link's annotation
447
     *
448
     * @param  LinkAnnotation $annotation
449
     * @return Link
450
     */
451 8
    protected function createLink(LinkAnnotation $annotation): Link
452
    {
453 8
        if (! preg_match(self::RESOURCE_PATTERN, $annotation->resource, $matches)) {
454
            throw new \LogicException(sprintf('Invalid resource definition: "%s"', $annotation->resource));
455
        }
456
457 8
        $link = new Link(
458 8
            $annotation->name,
459 8
            $matches['repository'],
460 8
            $matches['link']
461
        );
462
463 8
        $link->setParameters($annotation->parameters);
464 8
        $link->setMetadata($annotation->metadata);
465
466 8
        return $link;
467
    }
468
469
    /**
470
     * Handle control of data-section
471
     *
472
     * @param RelationshipAnnotation $annotation
473
     * @param Relationship           $relationship
474
     */
475 9
    protected function handleDataControl(RelationshipAnnotation $annotation, Relationship $relationship)
476
    {
477 9
        $relationship->setIncludeData($annotation->dataAllowed);
478 9
        $relationship->setDataLimit($annotation->dataLimit);
479 9
    }
480
481
    /**
482
     * Resolve type of relationship
483
     *
484
     * @param  RelationshipAnnotation $annotation
485
     * @return int
486
     */
487 9
    protected function resolveType(RelationshipAnnotation $annotation): int
488
    {
489 9
        if ($annotation->type === RelationshipAnnotation::TYPE_ONE) {
490 2
            return Relationship::TYPE_X_TO_ONE;
491
        }
492
493 7
        if ($annotation->type === RelationshipAnnotation::TYPE_MANY) {
494 7
            return Relationship::TYPE_X_TO_MANY;
495
        }
496
497
        throw new \LogicException(sprintf('Invalid type of relation "%s" defined.', $annotation->type));
498
    }
499
}