Passed
Push — master ( e52344...df819b )
by Rafael
09:21
created

resolveDefinitionInterfaces()   C

Complexity

Conditions 8
Paths 18

Size

Total Lines 29
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 8

Importance

Changes 0
Metric Value
dl 0
loc 29
c 0
b 0
f 0
ccs 18
cts 18
cp 1
rs 5.3846
cc 8
eloc 18
nc 18
nop 3
crap 8
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\Type\Definition\EndpointAwareInterface;
27
use Ynlo\GraphQLBundle\Util\TypeUtil;
28
29
/**
30
 * Parse the ObjectType annotation to fetch object definitions
31
 */
32
class ObjectTypeAnnotationParser implements AnnotationParserInterface
33
{
34
    use AnnotationReaderAwareTrait;
35
36
    /**
37
     * @var TaggedServices
38
     */
39
    protected $taggedServices;
40
    /**
41
     * @var Endpoint
42
     */
43
    protected $endpoint;
44
45
    /**
46
     * ObjectTypeAnnotationParser constructor.
47
     *
48
     * @param TaggedServices $taggedServices
49
     */
50 1
    public function __construct(TaggedServices $taggedServices)
51
    {
52 1
        $this->taggedServices = $taggedServices;
53 1
    }
54
55
    /**
56
     * {@inheritdoc}
57
     */
58 1
    public function supports($annotation): bool
59
    {
60 1
        return $annotation instanceof Annotation\ObjectType || $annotation instanceof Annotation\InputObjectType;
61
    }
62
63
    /**
64
     * {@inheritdoc}
65
     */
66 1
    public function parse($annotation, \ReflectionClass $refClass, Endpoint $endpoint)
67
    {
68 1
        $this->endpoint = $endpoint;
69
70 1
        if ($annotation instanceof Annotation\ObjectType) {
71 1
            $objectDefinition = new ObjectDefinition();
72
        } else {
73 1
            $objectDefinition = new InputObjectDefinition();
74
        }
75
76 1
        $objectDefinition->setName($annotation->name);
77 1
        $objectDefinition->setExclusionPolicy($annotation->exclusionPolicy);
78 1
        $objectDefinition->setClass($refClass->name);
79
80 1
        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...
81 1
            preg_match('/\w+$/', $refClass->getName(), $matches);
82 1
            $objectDefinition->setName($matches[0] ?? '');
83
        }
84
85 1
        if ($endpoint->hasType($objectDefinition->getName())) {
86
            return;
87
        }
88
89 1
        $objectDefinition->setClass($refClass->getName());
90 1
        $objectDefinition->setDescription($annotation->description);
91
92 1
        if ($objectDefinition instanceof ImplementorInterface) {
93 1
            $this->resolveDefinitionInterfaces($refClass, $objectDefinition, $endpoint);
94
        }
95
96 1
        $this->loadInheritedProperties($refClass, $objectDefinition);
97 1
        $this->resolveFields($refClass, $objectDefinition);
98 1
        $endpoint->addType($objectDefinition);
99 1
    }
100
101
    /**
102
     * @param \ReflectionClass     $refClass
103
     * @param ImplementorInterface $implementor
104
     * @param Endpoint             $endpoint
105
     */
106 1
    protected function resolveDefinitionInterfaces(\ReflectionClass $refClass, ImplementorInterface $implementor, Endpoint $endpoint)
107
    {
108 1
        $interfaceDefinitions = $this->extractInterfaceDefinitions($refClass);
109 1
        foreach ($interfaceDefinitions as $interfaceDefinition) {
110 1
            $implementor->addInterface($interfaceDefinition->getName());
111
112 1
            if (!$endpoint->hasType($interfaceDefinition->getName())) {
113 1
                $endpoint->addType($interfaceDefinition);
114
            } else {
115 1
                $interfaceDefinition = $endpoint->getType($interfaceDefinition->getName());
116
            }
117
118 1
            $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

118
            $interfaceDefinition->/** @scrutinizer ignore-call */ 
119
                                  addImplementor($implementor->getName());
Loading history...
119 1
            $this->copyFieldsFromInterface($interfaceDefinition, $implementor);
120
        }
121
122
        //support interface inheritance
123
        //Interface inheritance is implemented in GraphQL
124
        //@see https://github.com/facebook/graphql/issues/295
125
        //BUT, GraphQLBundle use this feature in some places like extensions etc.
126 1
        foreach ($interfaceDefinitions as $interfaceDefinition) {
127 1
            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...
128 1
                $childInterface = new \ReflectionClass($interfaceDefinition->getClass());
129 1
                $parentDefinitions = $this->extractInterfaceDefinitions($childInterface);
130 1
                foreach ($parentDefinitions as $parentDefinition) {
131 1
                    if ($endpoint->hasType($parentDefinition->getName())) {
132 1
                        $existentParentDefinition = $endpoint->getType($parentDefinition->getName());
133 1
                        if ($existentParentDefinition instanceof InterfaceDefinition) {
134 1
                            $existentParentDefinition->addImplementor($interfaceDefinition->getName());
135
                        }
136
                    }
137
                }
138
            }
139
        }
140 1
    }
141
142
    /**
143
     * @param \ReflectionClass $refClass
144
     *
145
     * @return InterfaceDefinition[]
146
     */
147 1
    protected function extractInterfaceDefinitions(\ReflectionClass $refClass)
148
    {
149 1
        $int = $refClass->getInterfaces();
150 1
        $definitions = [];
151 1
        foreach ($int as $intRef) {
152
            /** @var Annotation\InterfaceType $intAnnot */
153 1
            $intAnnot = $this->reader->getClassAnnotation(
154 1
                $intRef,
155 1
                Annotation\InterfaceType::class
156
            );
157
158 1
            if ($intAnnot) {
159 1
                $intDef = new InterfaceDefinition();
160 1
                $intDef->setName($intAnnot->name);
161 1
                $intDef->setClass($intRef->getName());
162 1
                $intDef->setDescription($intAnnot->description);
163 1
                $this->resolveFields($intRef, $intDef);
164 1
                if (!$intDef->getName() && preg_match('/\w+$/', $intRef->getName(), $matches)) {
165 1
                    $intDef->setName(preg_replace('/Interface$/', null, $matches[0]));
166
                }
167
168 1
                $definitions[] = $intDef;
169
            }
170
        }
171
172 1
        return $definitions;
173
    }
174
175
    /**
176
     * @param \ReflectionClass          $refClass
177
     * @param ObjectDefinitionInterface $objectDefinition
178
     */
179 1
    protected function loadInheritedProperties(\ReflectionClass $refClass, ObjectDefinitionInterface $objectDefinition)
180
    {
181 1
        while ($parent = $refClass->getParentClass()) {
182 1
            $this->resolveFields($refClass, $objectDefinition);
183 1
            $refClass = $parent;
184
        }
185 1
    }
186
187
    /**
188
     * Copy all fields from interface to given object implementor
189
     *
190
     * @param InterfaceDefinition            $intDef
191
     * @param FieldsAwareDefinitionInterface $fieldsAwareDefinition
192
     */
193 1
    protected function copyFieldsFromInterface(InterfaceDefinition $intDef, FieldsAwareDefinitionInterface $fieldsAwareDefinition)
194
    {
195 1
        foreach ($intDef->getFields() as $field) {
196 1
            if (!$fieldsAwareDefinition->hasField($field->getName())) {
197 1
                $fieldsAwareDefinition->addField(clone $field);
198
            }
199
        }
200 1
    }
201
202
    /**
203
     * Extract all fields for given definition
204
     *
205
     * @param \ReflectionClass          $refClass
206
     * @param ObjectDefinitionInterface $objectDefinition
207
     */
208 1
    protected function resolveFields(\ReflectionClass $refClass, ObjectDefinitionInterface $objectDefinition)
209
    {
210 1
        $props = array_merge($this->getClassProperties($refClass), $this->getClassMethods($refClass));
211
212 1
        $fieldDecorators = $this->getFieldDecorators();
213
214 1
        foreach ($props as $prop) {
215 1
            if ($this->isExposed($objectDefinition, $prop)) {
216 1
                $field = new FieldDefinition();
217 1
                foreach ($fieldDecorators as $fieldDecorator) {
218 1
                    $fieldDecorator->decorateFieldDefinition($prop, $field, $objectDefinition);
219
                }
220
221 1
                if ($objectDefinition->hasField($field->getName())) {
222 1
                    $field = $objectDefinition->getField($field->getName());
223
                } else {
224 1
                    $objectDefinition->addField($field);
225
                }
226
227 1
                $field->setOriginName($prop->name);
228 1
                $field->setOriginType(\get_class($prop));
229
230
                //resolve field arguments
231 1
                if ($prop instanceof \ReflectionMethod) {
232 1
                    $argAnnotations = $this->reader->getMethodAnnotations($prop);
233 1
                    foreach ($argAnnotations as $argAnnotation) {
234 1
                        if ($argAnnotation instanceof Annotation\Argument) {
235 1
                            $arg = new ArgumentDefinition();
236 1
                            $arg->setName($argAnnotation->name);
237 1
                            $arg->setDescription($argAnnotation->description);
238 1
                            $arg->setInternalName($argAnnotation->internalName);
239 1
                            $arg->setDefaultValue($argAnnotation->defaultValue);
240 1
                            $arg->setType(TypeUtil::normalize($argAnnotation->type));
241 1
                            $arg->setList(TypeUtil::isTypeList($argAnnotation->type));
242 1
                            $arg->setNonNullList(TypeUtil::isTypeNonNullList($argAnnotation->type));
243 1
                            $arg->setNonNull(TypeUtil::isTypeNonNull($argAnnotation->type));
244 1
                            $field->addArgument($arg);
245
                        }
246
                    }
247
                }
248
            }
249
        }
250
251
        //load overrides
252 1
        $annotations = $this->reader->getClassAnnotations($refClass);
253 1
        foreach ($annotations as $annotation) {
254 1
            if ($annotation instanceof Annotation\OverrideField) {
255 1
                if ($objectDefinition->hasField($annotation->name)) {
256 1
                    $fieldDefinition = $objectDefinition->getField($annotation->name);
257 1
                    if ($annotation->hidden === true) {
258
                        $objectDefinition->removeField($annotation->name);
259
                        continue;
260
                    }
261 1
                    if ($annotation->description) {
262 1
                        $fieldDefinition->setDescription($annotation->description);
263
                    }
264 1
                    if ($annotation->deprecationReason || $annotation->deprecationReason === false) {
265
                        $fieldDefinition->setDeprecationReason($annotation->deprecationReason);
266
                    }
267 1
                    if ($annotation->type) {
268 1
                        $fieldDefinition->setType($annotation->type);
269
                    }
270 1
                    if ($annotation->alias) {
271 1
                        $fieldDefinition->setName($annotation->alias);
272
                    }
273
                }
274
            }
275
        }
276 1
    }
277
278
    /**
279
     * @return array|FieldDefinitionDecoratorInterface[]
280
     */
281 1
    protected function getFieldDecorators(): array
282
    {
283
        /** @var Definition $resolversServiceDefinition */
284 1
        $decoratorsDef = $this->taggedServices
285 1
            ->findTaggedServices('graphql.field_definition_decorator');
286
287 1
        $decorators = [];
288 1
        foreach ($decoratorsDef as $decoratorDef) {
289 1
            $attr = $decoratorDef->getAttributes();
290 1
            $priority = 0;
291 1
            if (isset($attr['priority'])) {
292 1
                $priority = $attr['priority'];
293
            }
294
295 1
            $decorator = $decoratorDef->getService();
296
297 1
            if ($decorator instanceof EndpointAwareInterface) {
298
                $decorator->setEndpoint($this->endpoint);
299
            }
300
301 1
            $decorators[] = [$priority, $decorator];
302
        }
303
304
        //sort by priority
305 1
        usort(
306 1
            $decorators,
307 1
            function ($service1, $service2) {
308 1
                list($priority1) = $service1;
309 1
                list($priority2) = $service2;
310
311 1
                return version_compare($priority2, $priority1);
312 1
            }
313
        );
314
315 1
        return array_column($decorators, 1);
316
    }
317
318
    /**
319
     * Verify if a given property for given definition is exposed or not
320
     *
321
     * @param ObjectDefinitionInterface             $definition
322
     * @param \ReflectionMethod|\ReflectionProperty $prop
323
     *
324
     * @return boolean
325
     */
326 1
    protected function isExposed(ObjectDefinitionInterface $definition, $prop): bool
327
    {
328 1
        $exposed = $definition->getExclusionPolicy() === ObjectDefinitionInterface::EXCLUDE_NONE;
329
330 1
        if ($prop instanceof \ReflectionMethod) {
331 1
            $exposed = false;
332
333
            //implicit inclusion
334 1
            if ($this->getFieldAnnotation($prop, Annotation\Field::class)) {
335 1
                $exposed = true;
336
            }
337
        }
338
339 1
        if ($exposed && $this->getFieldAnnotation($prop, Annotation\Exclude::class)) {
340 1
            $exposed = false;
341 1
        } elseif (!$exposed && $this->getFieldAnnotation($prop, Annotation\Expose::class)) {
342
            $exposed = true;
343
        }
344
345 1
        return $exposed;
346
    }
347
348
    /**
349
     * Get field specific annotation matching given implementor
350
     *
351
     * @param \ReflectionMethod|\ReflectionProperty $prop
352
     * @param string                                $annotationClass
353
     *
354
     * @return mixed
355
     */
356 1
    protected function getFieldAnnotation($prop, string $annotationClass)
357
    {
358 1
        if ($prop instanceof \ReflectionProperty) {
359 1
            return $this->reader->getPropertyAnnotation($prop, $annotationClass);
360
        }
361
362 1
        return $this->reader->getMethodAnnotation($prop, $annotationClass);
363
    }
364
365
    /**
366
     * @param \ReflectionClass $refClass
367
     *
368
     * @return array
369
     */
370 1
    protected function getClassProperties(\ReflectionClass $refClass)
371
    {
372 1
        $props = [];
373 1
        foreach ($refClass->getProperties() as $prop) {
374 1
            $props[$prop->name] = $prop;
375
        }
376
377 1
        return $props;
378
    }
379
380
    /**
381
     * @param \ReflectionClass $refClass
382
     *
383
     * @return array
384
     */
385 1
    protected function getClassMethods(\ReflectionClass $refClass)
386
    {
387 1
        $methods = [];
388 1
        foreach ($refClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
389 1
            $methods[$method->name] = $method;
390
        }
391
392 1
        return $methods;
393
    }
394
}
395