Passed
Push — master ( 1331f6...486ab4 )
by Michael
03:25
created

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