Passed
Push — master ( 64b758...e85a2d )
by Rafael
08:23
created

ObjectTypeAnnotationParser::getClassMethods()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 0
cts 0
cp 0
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 1
crap 6
1
<?php
2
/*******************************************************************************
3
 *  This file is part of the GraphQL Bundle package.
4
 *
5
 *  (c) YnloUltratech <[email protected]>
6
 *
7
 *  For the full copyright and license information, please view the LICENSE
8
 *  file that was distributed with this source code.
9
 ******************************************************************************/
10
11
namespace Ynlo\GraphQLBundle\Definition\Loader\Annotation;
12
13
use Symfony\Component\DependencyInjection\Definition;
14
use Ynlo\GraphQLBundle\Annotation;
15
use Ynlo\GraphQLBundle\Component\TaggedServices\TaggedServices;
16
use Ynlo\GraphQLBundle\Definition\ArgumentDefinition;
17
use Ynlo\GraphQLBundle\Definition\FieldDefinition;
18
use Ynlo\GraphQLBundle\Definition\FieldsAwareDefinitionInterface;
19
use Ynlo\GraphQLBundle\Definition\ImplementorInterface;
20
use Ynlo\GraphQLBundle\Definition\InputObjectDefinition;
21
use Ynlo\GraphQLBundle\Definition\InterfaceDefinition;
22
use Ynlo\GraphQLBundle\Definition\Loader\Annotation\FieldDecorator\FieldDefinitionDecoratorInterface;
23
use Ynlo\GraphQLBundle\Definition\ObjectDefinition;
24
use Ynlo\GraphQLBundle\Definition\ObjectDefinitionInterface;
25
use Ynlo\GraphQLBundle\Definition\Registry\Endpoint;
26
use Ynlo\GraphQLBundle\Resolver\FieldExpressionResolver;
27
use Ynlo\GraphQLBundle\Type\Definition\EndpointAwareInterface;
28
use Ynlo\GraphQLBundle\Util\TypeUtil;
29
30
/**
31
 * Parse the ObjectType annotation to fetch object definitions
32
 */
33
class ObjectTypeAnnotationParser implements AnnotationParserInterface
34
{
35
    use AnnotationReaderAwareTrait;
36
37
    /**
38
     * @var TaggedServices
39
     */
40
    protected $taggedServices;
41
    /**
42
     * @var Endpoint
43
     */
44
    protected $endpoint;
45
46
    /**
47
     * ObjectTypeAnnotationParser constructor.
48
     *
49
     * @param TaggedServices $taggedServices
50
     */
51 21
    public function __construct(TaggedServices $taggedServices)
52
    {
53 21
        $this->taggedServices = $taggedServices;
54 21
    }
55
56
    /**
57
     * {@inheritdoc}
58
     */
59 21
    public function supports($annotation): bool
60
    {
61 21
        return $annotation instanceof Annotation\ObjectType || $annotation instanceof Annotation\InputObjectType;
62
    }
63
64
    /**
65
     * {@inheritdoc}
66
     */
67 21
    public function parse($annotation, \ReflectionClass $refClass, Endpoint $endpoint)
68
    {
69 21
        $this->endpoint = $endpoint;
70
71 21
        if ($annotation instanceof Annotation\ObjectType) {
72 21
            $objectDefinition = new ObjectDefinition();
73
        } else {
74 21
            $objectDefinition = new InputObjectDefinition();
75
        }
76
77 21
        $objectDefinition->setName($annotation->name);
78 21
        $objectDefinition->setExclusionPolicy($annotation->exclusionPolicy);
79 21
        $objectDefinition->setClass($refClass->name);
80
81 21
        if (!$objectDefinition->getName()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $objectDefinition->getName() of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
82 21
            preg_match('/\w+$/', $refClass->getName(), $matches);
83 21
            $objectDefinition->setName($matches[0] ?? '');
84
        }
85
86 21
        if ($endpoint->hasType($objectDefinition->getName())) {
87
            return;
88
        }
89
90 21
        $objectDefinition->setClass($refClass->getName());
91 21
        $objectDefinition->setDescription($annotation->description);
92
93 21
        if ($objectDefinition instanceof ImplementorInterface) {
94 21
            $this->resolveDefinitionInterfaces($refClass, $objectDefinition, $endpoint);
95
        }
96
97 21
        $this->loadInheritedProperties($refClass, $objectDefinition);
98 21
        $this->resolveFields($refClass, $objectDefinition);
99 21
        $endpoint->addType($objectDefinition);
100 21
    }
101
102
    /**
103
     * @param \ReflectionClass     $refClass
104
     * @param ImplementorInterface $implementor
105
     * @param Endpoint             $endpoint
106
     */
107 21
    protected function resolveDefinitionInterfaces(\ReflectionClass $refClass, ImplementorInterface $implementor, Endpoint $endpoint)
108
    {
109 21
        $interfaceDefinitions = $this->extractInterfaceDefinitions($refClass);
110 21
        foreach ($interfaceDefinitions as $interfaceDefinition) {
111 21
            $implementor->addInterface($interfaceDefinition->getName());
112
113 21
            if (!$endpoint->hasType($interfaceDefinition->getName())) {
114 21
                $endpoint->addType($interfaceDefinition);
115
            } else {
116 21
                $interfaceDefinition = $endpoint->getType($interfaceDefinition->getName());
117
            }
118
119 21
            $interfaceDefinition->addImplementor($implementor->getName());
0 ignored issues
show
Bug introduced by
The method addImplementor() does not exist on Ynlo\GraphQLBundle\Definition\DefinitionInterface. It seems like you code against a sub-type of Ynlo\GraphQLBundle\Definition\DefinitionInterface such as Ynlo\GraphQLBundle\Definition\InterfaceDefinition or Ynlo\GraphQLBundle\Definition\InterfaceDefinition. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

119
            $interfaceDefinition->/** @scrutinizer ignore-call */ 
120
                                  addImplementor($implementor->getName());
Loading history...
120 21
            $this->copyFieldsFromInterface($interfaceDefinition, $implementor);
121
        }
122
123
        //support interface inheritance
124
        //Interface inheritance is implemented in GraphQL
125
        //@see https://github.com/facebook/graphql/issues/295
126
        //BUT, GraphQLBundle use this feature in some places like extensions etc.
127 21
        foreach ($interfaceDefinitions as $interfaceDefinition) {
128 21
            if ($interfaceDefinition->getClass()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $interfaceDefinition->getClass() of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
129 21
                $childInterface = new \ReflectionClass($interfaceDefinition->getClass());
130 21
                $parentDefinitions = $this->extractInterfaceDefinitions($childInterface);
131 21
                foreach ($parentDefinitions as $parentDefinition) {
132 21
                    if ($endpoint->hasType($parentDefinition->getName())) {
133 21
                        $existentParentDefinition = $endpoint->getType($parentDefinition->getName());
134 21
                        if ($existentParentDefinition instanceof InterfaceDefinition) {
135 21
                            $existentParentDefinition->addImplementor($interfaceDefinition->getName());
136
                        }
137
                    }
138
                }
139
            }
140
        }
141 21
    }
142
143
    /**
144
     * @param \ReflectionClass $refClass
145
     *
146
     * @return InterfaceDefinition[]
147
     */
148 21
    protected function extractInterfaceDefinitions(\ReflectionClass $refClass)
149
    {
150 21
        $int = $refClass->getInterfaces();
151 21
        $definitions = [];
152 21
        foreach ($int as $intRef) {
153
            /** @var Annotation\InterfaceType $intAnnot */
154 21
            $intAnnot = $this->reader->getClassAnnotation(
155 21
                $intRef,
156 21
                Annotation\InterfaceType::class
157
            );
158
159 21
            if ($intAnnot) {
160 21
                $intDef = new InterfaceDefinition();
161 21
                $intDef->setName($intAnnot->name);
162 21
                $intDef->setClass($intRef->getName());
163 21
                $intDef->setDescription($intAnnot->description);
164 21
                $this->resolveFields($intRef, $intDef);
165 21
                if (!$intDef->getName() && preg_match('/\w+$/', $intRef->getName(), $matches)) {
166 21
                    $intDef->setName(preg_replace('/Interface$/', null, $matches[0]));
167
                }
168
169 21
                $definitions[] = $intDef;
170
            }
171
        }
172
173 21
        return $definitions;
174
    }
175
176
    /**
177
     * @param \ReflectionClass          $refClass
178
     * @param ObjectDefinitionInterface $objectDefinition
179
     */
180 21
    protected function loadInheritedProperties(\ReflectionClass $refClass, ObjectDefinitionInterface $objectDefinition)
181
    {
182 21
        while ($parent = $refClass->getParentClass()) {
183 21
            $this->resolveFields($refClass, $objectDefinition);
184 21
            $refClass = $parent;
185
        }
186 21
    }
187
188
    /**
189
     * Copy all fields from interface to given object implementor
190
     *
191
     * @param InterfaceDefinition            $intDef
192
     * @param FieldsAwareDefinitionInterface $fieldsAwareDefinition
193
     */
194 21
    protected function copyFieldsFromInterface(InterfaceDefinition $intDef, FieldsAwareDefinitionInterface $fieldsAwareDefinition)
195
    {
196 21
        foreach ($intDef->getFields() as $field) {
197 21
            if (!$fieldsAwareDefinition->hasField($field->getName())) {
198 21
                $newField = clone $field;
199
                $newField->addInheritedFrom($intDef->getName());
200
                $fieldsAwareDefinition->addField($newField);
201 21
            } else {
202
                $fieldsAwareDefinition->getField($field->getName())->addInheritedFrom($intDef->getName());
203
            }
204
        }
205
    }
206
207
    /**
208
     * Extract all fields for given definition
209 21
     *
210
     * @param \ReflectionClass          $refClass
211 21
     * @param ObjectDefinitionInterface $objectDefinition
212
     */
213 21
    protected function resolveFields(\ReflectionClass $refClass, ObjectDefinitionInterface $objectDefinition)
214
    {
215 21
        $props = array_merge($this->getClassProperties($refClass), $this->getClassMethods($refClass));
216 21
217 21
        $fieldDecorators = $this->getFieldDecorators();
218 21
219 21
        foreach ($props as $prop) {
220
            if ($this->isExposed($objectDefinition, $prop)) {
221
                $field = new FieldDefinition();
222 21
                foreach ($fieldDecorators as $fieldDecorator) {
223 21
                    $fieldDecorator->decorateFieldDefinition($prop, $field, $objectDefinition);
224
                }
225 21
226
                if ($objectDefinition->hasField($field->getName())) {
227
                    $field = $objectDefinition->getField($field->getName());
228 21
                } else {
229 21
                    $objectDefinition->addField($field);
230
                }
231
232 21
                $field->setOriginName($prop->name);
233 21
                $field->setOriginType(\get_class($prop));
234 21
235 21
                //resolve field arguments
236 21
                if ($prop instanceof \ReflectionMethod) {
237 21
                    $argAnnotations = $this->reader->getMethodAnnotations($prop);
238 21
                    foreach ($argAnnotations as $argAnnotation) {
239 21
                        if ($argAnnotation instanceof Annotation\Argument) {
240 21
                            $arg = new ArgumentDefinition();
241 21
                            $arg->setName($argAnnotation->name);
242 21
                            $arg->setDescription($argAnnotation->description);
243 21
                            $arg->setInternalName($argAnnotation->internalName);
244 21
                            $arg->setDefaultValue($argAnnotation->defaultValue);
245 21
                            $arg->setType(TypeUtil::normalize($argAnnotation->type));
246
                            $arg->setList(TypeUtil::isTypeList($argAnnotation->type));
247
                            $arg->setNonNullList(TypeUtil::isTypeNonNullList($argAnnotation->type));
248
                            $arg->setNonNull(TypeUtil::isTypeNonNull($argAnnotation->type));
249
                            $field->addArgument($arg);
250
                        }
251
                    }
252
                }
253 21
            }
254 21
        }
255 21
256 21
        //load overrides
257 21
        $annotations = $this->reader->getClassAnnotations($refClass);
258 21
        foreach ($annotations as $annotation) {
259
            if ($annotation instanceof Annotation\OverrideField) {
260
                if ($objectDefinition->hasField($annotation->name)) {
261
                    $fieldDefinition = $objectDefinition->getField($annotation->name);
262 21
                    if ($annotation->hidden === true) {
263 21
                        $objectDefinition->removeField($annotation->name);
264
                        continue;
265 21
                    }
266
                    if ($annotation->description) {
267
                        $fieldDefinition->setDescription($annotation->description);
268 21
                    }
269 21
                    if ($annotation->deprecationReason || $annotation->deprecationReason === false) {
270
                        $fieldDefinition->setDeprecationReason($annotation->deprecationReason);
271 21
                    }
272 21
                    if ($annotation->type) {
273
                        $fieldDefinition->setType($annotation->type);
274
                    }
275
                    if ($annotation->alias) {
276
                        $fieldDefinition->setName($annotation->alias);
277
                    }
278
                } else {
279
                    $error = sprintf(
280 21
                        'The object definition "%s" does not have any field called "%s" in any of its parents definitions.',
281
                        $objectDefinition->getName(),
282
                        $annotation->name
283
                    );
284
                    throw new \InvalidArgumentException($error);
285
                }
286 21
            }
287 21
        }
288 21
289
        //load virtual fields
290
        $annotations = $this->reader->getClassAnnotations($refClass);
291
        foreach ($annotations as $annotation) {
292
            if ($annotation instanceof Annotation\VirtualField) {
293
                if (!$objectDefinition->hasField($annotation->name)) {
294
                    $fieldDefinition = new FieldDefinition();
295
                    $fieldDefinition->setName($annotation->name);
296
                    $fieldDefinition->setDescription($annotation->description);
297
                    $fieldDefinition->setDeprecationReason($annotation->deprecationReason);
298
                    $fieldDefinition->setType(TypeUtil::normalize($annotation->type));
299
                    $fieldDefinition->setNonNull(TypeUtil::isTypeNonNull($annotation->type));
300
                    $fieldDefinition->setNonNullList(TypeUtil::isTypeNonNullList($annotation->type));
301
                    $fieldDefinition->setList(TypeUtil::isTypeList($annotation->type));
302
                    $fieldDefinition->setMeta('expression', $annotation->expression);
303
                    $fieldDefinition->setResolver(FieldExpressionResolver::class);
304
                    $objectDefinition->addField($fieldDefinition);
305
                } else {
306
                    $fieldDefinition = $objectDefinition->getField($annotation->name);
307
                    if ($fieldDefinition->getResolver() === FieldExpressionResolver::class) {
308
                        continue;
309
                    }
310
                    $error = sprintf(
311 21
                        'The object definition "%s" already has a field called "%s".',
312
                        $objectDefinition->getName(),
313
                        $annotation->name
314
                    );
315 21
                    throw new \InvalidArgumentException($error);
316
                }
317
            }
318
        }
319
    }
320 21
321
    /**
322
     * @return array|FieldDefinitionDecoratorInterface[]
323 21
     */
324 21
    protected function getFieldDecorators(): array
325
    {
326 21
        /** @var Definition $resolversServiceDefinition */
327 21
        $decoratorsDef = $this->taggedServices
328 21
            ->findTaggedServices('graphql.field_definition_decorator');
329 21
330 21
        $decorators = [];
331 21
        foreach ($decoratorsDef as $decoratorDef) {
332
            $attr = $decoratorDef->getAttributes();
333
            $priority = 0;
334 21
            if (isset($attr['priority'])) {
335
                $priority = $attr['priority'];
336 21
            }
337
338
            $decorator = $decoratorDef->getService();
339
340 21
            if ($decorator instanceof EndpointAwareInterface) {
341
                $decorator->setEndpoint($this->endpoint);
342
            }
343
344 21
            $decorators[] = [$priority, $decorator];
345 21
        }
346 21
347 21
        //sort by priority
348 21
        usort(
349
            $decorators,
350 21
            function ($service1, $service2) {
351 21
                list($priority1) = $service1;
352
                list($priority2) = $service2;
353
354 21
                return version_compare($priority2, $priority1);
355
            }
356
        );
357
358
        return array_column($decorators, 1);
359
    }
360
361
    /**
362
     * Verify if a given property for given definition is exposed or not
363
     *
364
     * @param ObjectDefinitionInterface             $definition
365 21
     * @param \ReflectionMethod|\ReflectionProperty $prop
366
     *
367 21
     * @return boolean
368
     */
369 21
    protected function isExposed(ObjectDefinitionInterface $definition, $prop): bool
370 21
    {
371
        $exposed = $definition->getExclusionPolicy() === ObjectDefinitionInterface::EXCLUDE_NONE;
372
373 21
        if ($prop instanceof \ReflectionMethod) {
374 21
            $exposed = false;
375
376
            //implicit inclusion
377
            if ($this->getFieldAnnotation($prop, Annotation\Field::class)) {
378 21
                $exposed = true;
379 21
            }
380 21
        }
381
382
        if ($exposed && $this->getFieldAnnotation($prop, Annotation\Exclude::class)) {
383
            $exposed = false;
384 21
        } elseif (!$exposed && $this->getFieldAnnotation($prop, Annotation\Expose::class)) {
385
            $exposed = true;
386
        }
387
388
        if (!$exposed) {
389
            //verify if the field belong to a interface
390
            //in this case is always exposed
391
            $inheritedInterfaceFields = [];
392
            foreach ($definition->getFields() as $field) {
393
                if ($field->getInheritedFrom()) {
394
                    $inheritedInterfaceFields[] = lcfirst(preg_replace('/^(get|set|has|is)/', null, $field->getOriginName()));
395 21
                }
396
            }
397 21
398 21
            $exposed = \in_array($prop->name, $inheritedInterfaceFields, true);
399
        }
400
401 21
        return $exposed;
402
    }
403
404
    /**
405
     * Get field specific annotation matching given implementor
406
     *
407
     * @param \ReflectionMethod|\ReflectionProperty $prop
408
     * @param string                                $annotationClass
409 21
     *
410
     * @return mixed
411 21
     */
412 21
    protected function getFieldAnnotation($prop, string $annotationClass)
413 21
    {
414
        if ($prop instanceof \ReflectionProperty) {
415
            return $this->reader->getPropertyAnnotation($prop, $annotationClass);
416 21
        }
417
418
        return $this->reader->getMethodAnnotation($prop, $annotationClass);
419
    }
420
421
    /**
422
     * @param \ReflectionClass $refClass
423
     *
424 21
     * @return array
425
     */
426 21
    protected function getClassProperties(\ReflectionClass $refClass)
427 21
    {
428 21
        $props = [];
429
        foreach ($refClass->getProperties() as $prop) {
430
            $props[$prop->name] = $prop;
431 21
        }
432
433
        return $props;
434
    }
435
436
    /**
437
     * @param \ReflectionClass $refClass
438
     *
439
     * @return array
440
     */
441
    protected function getClassMethods(\ReflectionClass $refClass)
442
    {
443
        $methods = [];
444
        foreach ($refClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
445
            $methods[$method->name] = $method;
446
        }
447
448
        return $methods;
449
    }
450
}
451