Passed
Push — master ( 486ab4...2a2608 )
by Michael
03:12
created

processClassAnnotations()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 4

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 18
ccs 10
cts 10
cp 1
rs 9.2
cc 4
eloc 9
nc 4
nop 2
crap 4
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
            if (! $method->isPublic()) {
172
                throw new \LogicException('Attribute annotation can be applied only to public method.');
173
            }
174
175 18
            $this->processMethod($method, $definition);
176
        }
177 18
    }
178
179
    /**
180
     * Process method of class
181
     *
182
     * @param \ReflectionMethod $method
183
     * @param Definition        $definition
184
     */
185 18
    protected function processMethod(\ReflectionMethod $method, Definition $definition)
186
    {
187 18
        $annotations = $this->reader->getMethodAnnotations($method);
188
189 18
        foreach ($annotations as $annotation)
190
        {
191 2
            if ($annotation instanceof AttributeAnnotation) {
192 2
                $attribute = $this->createAttributeByMethod($annotation, $method);
193
194 2
                $definition->addAttribute($attribute);
195
            }
196
        }
197 18
    }
198
199
    /**
200
     * Process annotations of class
201
     *
202
     * @param \ReflectionClass $reflection
203
     * @param Definition       $definition
204
     */
205 18
    protected function processClassAnnotations(\ReflectionClass $reflection, Definition $definition)
206
    {
207 18
        $annotations = $this->reader->getClassAnnotations($reflection);
208
209 18
        foreach ($annotations as $annotation)
210
        {
211 8
            if ($annotation instanceof LinkAnnotation) {
212 7
                $link = $this->createLink($annotation);
213
214 7
                $definition->addLink($link);
215 7
                continue;
216
            }
217
218 7
            if ($annotation instanceof ResourceIdentifierAnnotation) {
219 7
                $this->handlerResourceIdentifier($annotation, $definition);
220
            }
221
        }
222 18
    }
223
224
    /**
225
     * Handler resource identifier
226
     *
227
     * @param ResourceIdentifierAnnotation $annotation
228
     * @param Definition                   $definition
229
     */
230 7
    protected function handlerResourceIdentifier(ResourceIdentifierAnnotation $annotation, Definition $definition)
231
    {
232 7
        if ($annotation->type !== null) {
233 7
            $definition->setType($annotation->type);
234
        }
235 7
    }
236
237
    /**
238
     * Create attribute by annotation of property
239
     *
240
     * @param  AttributeAnnotation $annotation
241
     * @param  \ReflectionProperty $property
242
     * @return Attribute
243
     */
244 2
    protected function createAttributeByProperty(AttributeAnnotation $annotation, \ReflectionProperty $property): Attribute
245
    {
246 2
        $name = ($annotation->name === null)
247 2
            ? $property->getName()
248 2
            : $annotation->name;
249
250 2
        $getter = ($annotation->getter === null)
251 2
            ? $this->resolveGetter($property)
252 2
            : $annotation->getter;
253
254 2
        $attribute = new Attribute($name, $getter);
255 2
        $attribute->setPropertyName($property->getName());
256
257 2
        if ($annotation->type !== null) {
258 2
            $this->processDataType($annotation->type, $attribute);
259
        }
260
261 2
        return $attribute;
262
    }
263
264
    /**
265
     * Create attribute by annotation of method
266
     *
267
     * @param  AttributeAnnotation $annotation
268
     * @param  \ReflectionMethod   $method
269
     * @return Attribute
270
     */
271 2
    protected function createAttributeByMethod(AttributeAnnotation $annotation, \ReflectionMethod $method): Attribute
272
    {
273 2
        if ($annotation->getter !== null) {
274
            throw new \LogicException('"getter" property of Attribute annotation applied to a method is useless.');
275
        }
276
277 2
        $name = ($annotation->name === null)
278 2
            ? $this->resolveNameByMethod($method)
279 2
            : $annotation->name;
280
281 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...
282
283 2
        if ($annotation->type !== null) {
284 2
            $this->processDataType($annotation->type, $attribute);
285
        }
286
287 2
        return $attribute;
288
    }
289
290
    /**
291
     * Resolve name of attribute by method
292
     *
293
     * @param  \ReflectionMethod $method
294
     * @return string
295
     */
296 2
    protected function resolveNameByMethod(\ReflectionMethod $method): string
297
    {
298 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...
299
300 2
        if (preg_match('~^(?:get|is)(?<name>[a-z0-9_]+)~i', $name, $matches)) {
301 2
            return lcfirst($matches['name']);
302
        }
303
304
        return $name;
305
    }
306
307
    /**
308
     * Process data-type
309
     *
310
     * @param string    $definition
311
     * @param Attribute $attribute
312
     */
313 4
    protected function processDataType(string $definition, Attribute $attribute)
314
    {
315 4
        if (! preg_match(self::DATATYPE_PATTERN, $definition, $matches)) {
316
            throw new \LogicException(sprintf('Data-type definition "%s" is invalid.', $definition));
317
        }
318
319 4
        $attribute->setType($matches['type']);
320
321 4
        if (empty($matches['params'])) {
322
            return;
323
        }
324
325 4
        $parameters = explode(',', $matches['params']);
326 4
        $parameters = array_map('trim', $parameters);
327
328 4
        $attribute->setTypeParameters($parameters);
329 4
    }
330
331
    /**
332
     * Process relationship
333
     *
334
     * @param  RelationshipAnnotation $annotation
335
     * @param  \ReflectionProperty    $property
336
     * @return Relationship
337
     */
338 9
    protected function createRelationship(RelationshipAnnotation $annotation, \ReflectionProperty $property): Relationship
339
    {
340 9
        $name = ($annotation->name === null)
341 7
            ? $property->getName()
342 9
            : $annotation->name;
343
344 9
        $type = $this->resolveType($annotation);
345
346 9
        $getter = ($annotation->getter === null)
347 9
            ? $this->resolveGetter($property)
348 9
            : $annotation->getter;
349
350 9
        $relationship = new Relationship($name, $type, $getter);
351 9
        $relationship->setPropertyName($property->getName());
352
353 9
        $this->handleLinks($annotation, $relationship);
354 9
        $this->handleDataControl($annotation, $relationship);
355
356 9
        return $relationship;
357
    }
358
359
    /**
360
     * Resolve getter of related object
361
     *
362
     * @param  \ReflectionProperty $property
363
     * @return string
364
     */
365 11
    protected function resolveGetter(\ReflectionProperty $property): string
366
    {
367 11
        $name  = $property->getName();
368 11
        $class = $property->getDeclaringClass();
369
370 11
        foreach (['get', 'is'] as $prefix)
371
        {
372 11
            $getter = $prefix . ucfirst($name);
373
374 11
            if ($class->hasMethod($getter) && $class->getMethod($getter)->isPublic()) {
375 11
                return $getter;
376
            }
377
        }
378
379
        throw new \LogicException(sprintf(
380
            'Getter-method for the property "%s" cannot be resolved automatically. ' .
381
            'Probably there is no get%2$s() or is%2$s() method or it is not public.',
382
            $name, ucfirst($name)
383
        ));
384
    }
385
386
    /**
387
     * Handle links
388
     *
389
     * @param RelationshipAnnotation $annotation
390
     * @param Relationship           $relationship
391
     */
392 9
    protected function handleLinks(RelationshipAnnotation $annotation, Relationship $relationship)
393
    {
394 9
        foreach ($annotation->links as $linkAnnotation)
395
        {
396 7
            $link = $this->createLink($linkAnnotation);
397
398 7
            $relationship->addLink($link);
399
        }
400 9
    }
401
402
    /**
403
     * Create link by link's annotation
404
     *
405
     * @param  LinkAnnotation $annotation
406
     * @return Link
407
     */
408 8
    protected function createLink(LinkAnnotation $annotation): Link
409
    {
410 8
        if (! preg_match(self::RESOURCE_PATTERN, $annotation->resource, $matches)) {
411
            throw new \LogicException(sprintf('Invalid resource definition: "%s"', $annotation->resource));
412
        }
413
414 8
        $link = new Link(
415 8
            $annotation->name,
416 8
            $matches['repository'],
417 8
            $matches['link']
418
        );
419
420 8
        $link->setParameters($annotation->parameters);
421 8
        $link->setMetadata($annotation->metadata);
422
423 8
        return $link;
424
    }
425
426
    /**
427
     * Handle control of data-section
428
     *
429
     * @param RelationshipAnnotation $annotation
430
     * @param Relationship           $relationship
431
     */
432 9
    protected function handleDataControl(RelationshipAnnotation $annotation, Relationship $relationship)
433
    {
434 9
        $relationship->setIncludeData($annotation->dataAllowed);
435 9
        $relationship->setDataLimit($annotation->dataLimit);
436 9
    }
437
438
    /**
439
     * Resolve type of relationship
440
     *
441
     * @param  RelationshipAnnotation $annotation
442
     * @return int
443
     */
444 9
    protected function resolveType(RelationshipAnnotation $annotation): int
445
    {
446 9
        if ($annotation->type === RelationshipAnnotation::TYPE_ONE) {
447 2
            return Relationship::TYPE_X_TO_ONE;
448
        }
449
450 7
        if ($annotation->type === RelationshipAnnotation::TYPE_MANY) {
451 7
            return Relationship::TYPE_X_TO_MANY;
452
        }
453
454
        throw new \LogicException(sprintf('Invalid type of relation "%s" defined.', $annotation->type));
455
    }
456
}