Passed
Push — master ( 77e09c...64b758 )
by Rafael
07:59
created

ObjectTypeAnnotationParser::isExposed()   B

Complexity

Conditions 7
Paths 9

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 7.0368

Importance

Changes 0
Metric Value
cc 7
eloc 10
nc 9
nop 2
dl 0
loc 20
ccs 10
cts 11
cp 0.9091
crap 7.0368
rs 8.2222
c 0
b 0
f 0
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
                $fieldsAwareDefinition->addField(clone $field);
199
            }
200
        }
201 21
    }
202
203
    /**
204
     * Extract all fields for given definition
205
     *
206
     * @param \ReflectionClass          $refClass
207
     * @param ObjectDefinitionInterface $objectDefinition
208
     */
209 21
    protected function resolveFields(\ReflectionClass $refClass, ObjectDefinitionInterface $objectDefinition)
210
    {
211 21
        $props = array_merge($this->getClassProperties($refClass), $this->getClassMethods($refClass));
212
213 21
        $fieldDecorators = $this->getFieldDecorators();
214
215 21
        foreach ($props as $prop) {
216 21
            if ($this->isExposed($objectDefinition, $prop)) {
217 21
                $field = new FieldDefinition();
218 21
                foreach ($fieldDecorators as $fieldDecorator) {
219 21
                    $fieldDecorator->decorateFieldDefinition($prop, $field, $objectDefinition);
220
                }
221
222 21
                if ($objectDefinition->hasField($field->getName())) {
223 21
                    $field = $objectDefinition->getField($field->getName());
224
                } else {
225 21
                    $objectDefinition->addField($field);
226
                }
227
228 21
                $field->setOriginName($prop->name);
229 21
                $field->setOriginType(\get_class($prop));
230
231
                //resolve field arguments
232 21
                if ($prop instanceof \ReflectionMethod) {
233 21
                    $argAnnotations = $this->reader->getMethodAnnotations($prop);
234 21
                    foreach ($argAnnotations as $argAnnotation) {
235 21
                        if ($argAnnotation instanceof Annotation\Argument) {
236 21
                            $arg = new ArgumentDefinition();
237 21
                            $arg->setName($argAnnotation->name);
238 21
                            $arg->setDescription($argAnnotation->description);
239 21
                            $arg->setInternalName($argAnnotation->internalName);
240 21
                            $arg->setDefaultValue($argAnnotation->defaultValue);
241 21
                            $arg->setType(TypeUtil::normalize($argAnnotation->type));
242 21
                            $arg->setList(TypeUtil::isTypeList($argAnnotation->type));
243 21
                            $arg->setNonNullList(TypeUtil::isTypeNonNullList($argAnnotation->type));
244 21
                            $arg->setNonNull(TypeUtil::isTypeNonNull($argAnnotation->type));
245 21
                            $field->addArgument($arg);
246
                        }
247
                    }
248
                }
249
            }
250
        }
251
252
        //load overrides
253 21
        $annotations = $this->reader->getClassAnnotations($refClass);
254 21
        foreach ($annotations as $annotation) {
255 21
            if ($annotation instanceof Annotation\OverrideField) {
256 21
                if ($objectDefinition->hasField($annotation->name)) {
257 21
                    $fieldDefinition = $objectDefinition->getField($annotation->name);
258 21
                    if ($annotation->hidden === true) {
259
                        $objectDefinition->removeField($annotation->name);
260
                        continue;
261
                    }
262 21
                    if ($annotation->description) {
263 21
                        $fieldDefinition->setDescription($annotation->description);
264
                    }
265 21
                    if ($annotation->deprecationReason || $annotation->deprecationReason === false) {
266
                        $fieldDefinition->setDeprecationReason($annotation->deprecationReason);
267
                    }
268 21
                    if ($annotation->type) {
269 21
                        $fieldDefinition->setType($annotation->type);
270
                    }
271 21
                    if ($annotation->alias) {
272 21
                        $fieldDefinition->setName($annotation->alias);
273
                    }
274
                } else {
275
                    $error = sprintf(
276
                        'The object definition "%s" does not have any field called "%s" in any of its parents definitions.',
277
                        $objectDefinition->getName(),
278
                        $annotation->name
279
                    );
280 21
                    throw new \InvalidArgumentException($error);
281
                }
282
            }
283
        }
284
285
        //load virtual fields
286 21
        $annotations = $this->reader->getClassAnnotations($refClass);
287 21
        foreach ($annotations as $annotation) {
288 21
            if ($annotation instanceof Annotation\VirtualField) {
289
                if (!$objectDefinition->hasField($annotation->name)) {
290
                    $fieldDefinition = new FieldDefinition();
291
                    $fieldDefinition->setName($annotation->name);
292
                    $fieldDefinition->setDescription($annotation->description);
293
                    $fieldDefinition->setDeprecationReason($annotation->deprecationReason);
294
                    $fieldDefinition->setType(TypeUtil::normalize($annotation->type));
295
                    $fieldDefinition->setNonNull(TypeUtil::isTypeNonNull($annotation->type));
296
                    $fieldDefinition->setNonNullList(TypeUtil::isTypeNonNullList($annotation->type));
297
                    $fieldDefinition->setList(TypeUtil::isTypeList($annotation->type));
298
                    $fieldDefinition->setMeta('expression', $annotation->expression);
299
                    $fieldDefinition->setResolver(FieldExpressionResolver::class);
300
                    $objectDefinition->addField($fieldDefinition);
301
                } else {
302
                    $fieldDefinition = $objectDefinition->getField($annotation->name);
303
                    if ($fieldDefinition->getResolver() === FieldExpressionResolver::class) {
304
                        continue;
305
                    }
306
                    $error = sprintf(
307
                        'The object definition "%s" already has a field called "%s".',
308
                        $objectDefinition->getName(),
309
                        $annotation->name
310
                    );
311 21
                    throw new \InvalidArgumentException($error);
312
                }
313
            }
314
        }
315 21
    }
316
317
    /**
318
     * @return array|FieldDefinitionDecoratorInterface[]
319
     */
320 21
    protected function getFieldDecorators(): array
321
    {
322
        /** @var Definition $resolversServiceDefinition */
323 21
        $decoratorsDef = $this->taggedServices
324 21
            ->findTaggedServices('graphql.field_definition_decorator');
325
326 21
        $decorators = [];
327 21
        foreach ($decoratorsDef as $decoratorDef) {
328 21
            $attr = $decoratorDef->getAttributes();
329 21
            $priority = 0;
330 21
            if (isset($attr['priority'])) {
331 21
                $priority = $attr['priority'];
332
            }
333
334 21
            $decorator = $decoratorDef->getService();
335
336 21
            if ($decorator instanceof EndpointAwareInterface) {
337
                $decorator->setEndpoint($this->endpoint);
338
            }
339
340 21
            $decorators[] = [$priority, $decorator];
341
        }
342
343
        //sort by priority
344 21
        usort(
345 21
            $decorators,
346 21
            function ($service1, $service2) {
347 21
                list($priority1) = $service1;
348 21
                list($priority2) = $service2;
349
350 21
                return version_compare($priority2, $priority1);
351 21
            }
352
        );
353
354 21
        return array_column($decorators, 1);
355
    }
356
357
    /**
358
     * Verify if a given property for given definition is exposed or not
359
     *
360
     * @param ObjectDefinitionInterface             $definition
361
     * @param \ReflectionMethod|\ReflectionProperty $prop
362
     *
363
     * @return boolean
364
     */
365 21
    protected function isExposed(ObjectDefinitionInterface $definition, $prop): bool
366
    {
367 21
        $exposed = $definition->getExclusionPolicy() === ObjectDefinitionInterface::EXCLUDE_NONE;
368
369 21
        if ($prop instanceof \ReflectionMethod) {
370 21
            $exposed = false;
371
372
            //implicit inclusion
373 21
            if ($this->getFieldAnnotation($prop, Annotation\Field::class)) {
374 21
                $exposed = true;
375
            }
376
        }
377
378 21
        if ($exposed && $this->getFieldAnnotation($prop, Annotation\Exclude::class)) {
379 21
            $exposed = false;
380 21
        } elseif (!$exposed && $this->getFieldAnnotation($prop, Annotation\Expose::class)) {
381
            $exposed = true;
382
        }
383
384 21
        return $exposed;
385
    }
386
387
    /**
388
     * Get field specific annotation matching given implementor
389
     *
390
     * @param \ReflectionMethod|\ReflectionProperty $prop
391
     * @param string                                $annotationClass
392
     *
393
     * @return mixed
394
     */
395 21
    protected function getFieldAnnotation($prop, string $annotationClass)
396
    {
397 21
        if ($prop instanceof \ReflectionProperty) {
398 21
            return $this->reader->getPropertyAnnotation($prop, $annotationClass);
399
        }
400
401 21
        return $this->reader->getMethodAnnotation($prop, $annotationClass);
402
    }
403
404
    /**
405
     * @param \ReflectionClass $refClass
406
     *
407
     * @return array
408
     */
409 21
    protected function getClassProperties(\ReflectionClass $refClass)
410
    {
411 21
        $props = [];
412 21
        foreach ($refClass->getProperties() as $prop) {
413 21
            $props[$prop->name] = $prop;
414
        }
415
416 21
        return $props;
417
    }
418
419
    /**
420
     * @param \ReflectionClass $refClass
421
     *
422
     * @return array
423
     */
424 21
    protected function getClassMethods(\ReflectionClass $refClass)
425
    {
426 21
        $methods = [];
427 21
        foreach ($refClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
428 21
            $methods[$method->name] = $method;
429
        }
430
431 21
        return $methods;
432
    }
433
}
434