Passed
Push — master ( f32e65...4e94c9 )
by Michael
02:27
created

AnnotationDefinitionProvider::handleDataControl()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 5
ccs 4
cts 4
cp 1
rs 9.4285
cc 1
eloc 3
nc 1
nop 2
crap 1
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');
0 ignored issues
show
Deprecated Code introduced by
The method Doctrine\Common\Annotati...egistry::registerFile() has been deprecated with message: this method is deprecated and will be removed in doctrine/annotations 2.0 autoloading should be deferred to the globally registered autoloader by then. For now, use @example AnnotationRegistry::registerLoader('class_exists')

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
52 1
            AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Attribute.php');
0 ignored issues
show
Deprecated Code introduced by
The method Doctrine\Common\Annotati...egistry::registerFile() has been deprecated with message: this method is deprecated and will be removed in doctrine/annotations 2.0 autoloading should be deferred to the globally registered autoloader by then. For now, use @example AnnotationRegistry::registerLoader('class_exists')

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
53 1
            AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Link.php');
0 ignored issues
show
Deprecated Code introduced by
The method Doctrine\Common\Annotati...egistry::registerFile() has been deprecated with message: this method is deprecated and will be removed in doctrine/annotations 2.0 autoloading should be deferred to the globally registered autoloader by then. For now, use @example AnnotationRegistry::registerLoader('class_exists')

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

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