Passed
Push — master ( d7cd6a...1331f6 )
by Michael
02:45
created

AnnotationDefinitionProvider::handleGetter()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 12
ccs 8
cts 8
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 7
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 15
    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 15
        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 15
    }
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 15
    public function __construct(Reader $reader)
72
    {
73 15
        self::registerAnnotations();
74
75 15
        $this->reader = $reader;
76 15
    }
77
78
    /**
79
     * {@inheritdoc}
80
     */
81 15
    public function getDefinition(string $class): Definition
82
    {
83 15
        if (! isset($this->definitionCache[$class])) {
84 15
            $reflection = new \ReflectionClass($class);
85
86 15
            $this->definitionCache[$class] = $this->createDefinition($reflection);
87
        }
88
89 15
        return $this->definitionCache[$class];
90
    }
91
92
    /**
93
     * Create definition for given class
94
     *
95
     * @param  \ReflectionClass $reflection
96
     * @return Definition
97
     */
98 15
    protected function createDefinition(\ReflectionClass $reflection): Definition
99
    {
100 15
        $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 15
        $this->processProperties($reflection, $definition);
103 15
        $this->processClassAnnotations($reflection, $definition);
104
105 15
        $parent = $reflection->getParentClass();
106
107 15
        if ($parent !== false) {
108 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...
109
        }
110
111 15
        return $definition;
112
    }
113
114
    /**
115
     * Process properties of class
116
     *
117
     * @param \ReflectionClass $reflection
118
     * @param Definition       $definition
119
     */
120 15
    protected function processProperties(\ReflectionClass $reflection, Definition $definition)
121
    {
122 15
        foreach ($reflection->getProperties() as $property)
123
        {
124 15
            $annotations = $this->reader->getPropertyAnnotations($property);
125
126 15
            foreach ($annotations as $annotation)
127
            {
128 11
                if ($annotation instanceof AttributeAnnotation) {
129 2
                    $attribute = $this->createAttribute($annotation, $property);
130
131 2
                    $definition->addAttribute($attribute);
132 2
                    continue;
133
                }
134
135 9
                if ($annotation instanceof RelationshipAnnotation) {
136 9
                    $relationship = $this->createRelationship($annotation, $property);
137
138 15
                    $definition->addRelationship($relationship);
139
                }
140
            }
141
        }
142 15
    }
143
144
    /**
145
     * Process annotations of class
146
     *
147
     * @param \ReflectionClass $reflection
148
     * @param Definition       $definition
149
     */
150 15
    protected function processClassAnnotations(\ReflectionClass $reflection, Definition $definition)
151
    {
152 15
        $annotations = $this->reader->getClassAnnotations($reflection);
153
154 15
        foreach ($annotations as $annotation)
155
        {
156 8
            if ($annotation instanceof LinkAnnotation) {
157 7
                $link = $this->createLink($annotation);
158
159 7
                $definition->addLink($link);
160 7
                continue;
161
            }
162
163 7
            if ($annotation instanceof ResourceIdentifierAnnotation) {
164 7
                $this->handlerResourceIdentifier($annotation, $definition);
165
            }
166
        }
167 15
    }
168
169
    /**
170
     * Handler resource identifier
171
     *
172
     * @param ResourceIdentifierAnnotation $annotation
173
     * @param Definition                   $definition
174
     */
175 7
    protected function handlerResourceIdentifier(ResourceIdentifierAnnotation $annotation, Definition $definition)
176
    {
177 7
        if ($annotation->type !== null) {
178 7
            $definition->setType($annotation->type);
179
        }
180 7
    }
181
182
    /**
183
     * Create attribute
184
     *
185
     * @param  AttributeAnnotation $annotation
186
     * @param  \ReflectionProperty $property
187
     * @return Attribute
188
     */
189 2
    protected function createAttribute(AttributeAnnotation $annotation, \ReflectionProperty $property)
190
    {
191 2
        $name = ($annotation->name === null)
192 2
            ? $property->getName()
193 2
            : $annotation->name;
194
195 2
        $getter = ($annotation->getter === null)
196 2
            ? $this->resolveGetter($property)
197 2
            : $annotation->getter;
198
199 2
        $attribute = new Attribute($name, $getter);
200 2
        $attribute->setPropertyName($property->getName());
201
202 2
        if ($annotation->type !== null) {
203 2
            $this->processDataType($annotation->type, $attribute);
204
        }
205
206 2
        return $attribute;
207
    }
208
209
    /**
210
     * Process data-type
211
     *
212
     * @param string    $definition
213
     * @param Attribute $attribute
214
     */
215 2
    protected function processDataType(string $definition, Attribute $attribute)
216
    {
217 2
        if (! preg_match(self::DATATYPE_PATTERN, $definition, $matches)) {
218
            throw new \LogicException(sprintf('Data-type definition "%s" is invalid.', $definition));
219
        }
220
221 2
        $attribute->setType($matches['type']);
222
223 2
        if (empty($matches['params'])) {
224
            return;
225
        }
226
227 2
        $parameters = explode(',', $matches['params']);
228 2
        $parameters = array_map('trim', $parameters);
229
230 2
        $attribute->setTypeParameters($parameters);
231 2
    }
232
233
    /**
234
     * Process relationship
235
     *
236
     * @param  RelationshipAnnotation $annotation
237
     * @param  \ReflectionProperty    $property
238
     * @return Relationship
239
     */
240 9
    protected function createRelationship(RelationshipAnnotation $annotation, \ReflectionProperty $property): Relationship
241
    {
242 9
        $name = ($annotation->name === null)
243 7
            ? $property->getName()
244 9
            : $annotation->name;
245
246 9
        $type = $this->resolveType($annotation);
247
248 9
        $getter = ($annotation->getter === null)
249 9
            ? $this->resolveGetter($property)
250 9
            : $annotation->getter;
251
252 9
        $relationship = new Relationship($name, $type, $getter);
253 9
        $relationship->setPropertyName($property->getName());
254
255 9
        $this->handleLinks($annotation, $relationship);
256 9
        $this->handleDataControl($annotation, $relationship);
257
258 9
        return $relationship;
259
    }
260
261
    /**
262
     * Resolve getter of related object
263
     *
264
     * @param  \ReflectionProperty $property
265
     * @return string
266
     */
267 11
    protected function resolveGetter(\ReflectionProperty $property): string
268
    {
269 11
        $name  = $property->getName();
270 11
        $class = $property->getDeclaringClass();
271
272 11
        foreach (['get', 'is'] as $prefix)
273
        {
274 11
            $getter = $prefix . ucfirst($name);
275
276 11
            if ($class->hasMethod($getter) && $class->getMethod($getter)->isPublic()) {
277 11
                return $getter;
278
            }
279
        }
280
281
        throw new \LogicException(sprintf(
282
            'Getter-method for the property "%s" cannot be resolved automatically. ' .
283
            'Probably there is no get%2$s() or is%2$s() method or it is not public.',
284
            $name, ucfirst($name)
285
        ));
286
    }
287
288
    /**
289
     * Handle links
290
     *
291
     * @param RelationshipAnnotation $annotation
292
     * @param Relationship           $relationship
293
     */
294 9
    protected function handleLinks(RelationshipAnnotation $annotation, Relationship $relationship)
295
    {
296 9
        foreach ($annotation->links as $linkAnnotation)
297
        {
298 7
            $link = $this->createLink($linkAnnotation);
299
300 7
            $relationship->addLink($link);
301
        }
302 9
    }
303
304
    /**
305
     * Create link by link's annotation
306
     *
307
     * @param  LinkAnnotation $annotation
308
     * @return Link
309
     */
310 8
    protected function createLink(LinkAnnotation $annotation): Link
311
    {
312 8
        if (! preg_match(self::RESOURCE_PATTERN, $annotation->resource, $matches)) {
313
            throw new \LogicException(sprintf('Invalid resource definition: "%s"', $annotation->resource));
314
        }
315
316 8
        $link = new Link(
317 8
            $annotation->name,
318 8
            $matches['repository'],
319 8
            $matches['link']
320
        );
321
322 8
        $link->setParameters($annotation->parameters);
323 8
        $link->setMetadata($annotation->metadata);
324
325 8
        return $link;
326
    }
327
328
    /**
329
     * Handle control of data-section
330
     *
331
     * @param RelationshipAnnotation $annotation
332
     * @param Relationship           $relationship
333
     */
334 9
    protected function handleDataControl(RelationshipAnnotation $annotation, Relationship $relationship)
335
    {
336 9
        $relationship->setIncludeData($annotation->dataAllowed);
337 9
        $relationship->setDataLimit($annotation->dataLimit);
338 9
    }
339
340
    /**
341
     * Resolve type of relationship
342
     *
343
     * @param  RelationshipAnnotation $annotation
344
     * @return int
345
     */
346 9
    protected function resolveType(RelationshipAnnotation $annotation): int
347
    {
348 9
        if ($annotation->type === RelationshipAnnotation::TYPE_ONE) {
349 2
            return Relationship::TYPE_X_TO_ONE;
350
        }
351
352 7
        if ($annotation->type === RelationshipAnnotation::TYPE_MANY) {
353 7
            return Relationship::TYPE_X_TO_MANY;
354
        }
355
356
        throw new \LogicException(sprintf('Invalid type of relation "%s" defined.', $annotation->type));
357
    }
358
}