Completed
Push — master ( b7907c...708c0e )
by Alexandr
15:31 queued 11:30
created

Reducer::doVisit()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 6.0163

Importance

Changes 0
Metric Value
dl 0
loc 27
rs 8.8657
c 0
b 0
f 0
ccs 12
cts 13
cp 0.9231
cc 6
nc 5
nop 3
crap 6.0163
1
<?php
2
/**
3
 * Date: 07.11.16
4
 *
5
 * @author Portey Vasil <[email protected]>
6
 */
7
8
namespace Youshido\GraphQL\Execution;
9
10
11
use Youshido\GraphQL\Execution\Context\ExecutionContextInterface;
12
use Youshido\GraphQL\Execution\Visitor\AbstractQueryVisitor;
13
use Youshido\GraphQL\Field\Field;
14
use Youshido\GraphQL\Field\FieldInterface;
15
use Youshido\GraphQL\Parser\Ast\Field as FieldAst;
16
use Youshido\GraphQL\Parser\Ast\FragmentReference;
17
use Youshido\GraphQL\Parser\Ast\Interfaces\FragmentInterface;
18
use Youshido\GraphQL\Parser\Ast\Mutation;
19
use Youshido\GraphQL\Parser\Ast\Query;
20
use Youshido\GraphQL\Type\AbstractType;
21
use Youshido\GraphQL\Type\Object\AbstractObjectType;
22
use Youshido\GraphQL\Type\Union\AbstractUnionType;
23
24
class Reducer
25
{
26
27
    /** @var  ExecutionContextInterface */
28
    private $executionContext;
29
30
    /**
31
     * Apply all of $reducers to this query.  Example reducer operations: checking for maximum query complexity,
32
     * performing look-ahead query planning, etc.
33
     *
34
     * @param ExecutionContextInterface $executionContext
35
     * @param AbstractQueryVisitor[]    $reducers
36
     */
37 2
    public function reduceQuery(ExecutionContextInterface $executionContext, array $reducers)
38
    {
39 2
        $this->executionContext = $executionContext;
40 2
        $schema                 = $executionContext->getSchema();
41
42 2
        foreach ($reducers as $reducer) {
43 2
            foreach ($executionContext->getRequest()->getAllOperations() as $operation) {
44 2
                $this->doVisit($operation, $operation instanceof Mutation ? $schema->getMutationType() : $schema->getQueryType(), $reducer);
45
            }
46
        }
47 2
    }
48
49
    /**
50
     * Entry point for the `walkQuery` routine.  Execution bounces between here, where the reducer's ->visit() method
51
     * is invoked, and `walkQuery` where we send in the scores from the `visit` call.
52
     *
53
     * @param Query                $query
54
     * @param AbstractType         $currentLevelSchema
55
     * @param AbstractQueryVisitor $reducer
56
     */
57 2
    protected function doVisit(Query $query, $currentLevelSchema, $reducer)
58
    {
59 2
        if (!($currentLevelSchema instanceof AbstractObjectType) || !$currentLevelSchema->hasField($query->getName())) {
60
            return;
61
        }
62
63 2
        if ($operationField = $currentLevelSchema->getField($query->getName())) {
64
65 2
            $coroutine = $this->walkQuery($query, $operationField);
66
67 2
            if ($results = $coroutine->current()) {
68 2
                $queryCost = 0;
69 2
                while ($results) {
70
                    // initial values come from advancing the generator via ->current, subsequent values come from ->send()
71 2
                    list($queryField, $astField, $childCost) = $results;
72
73
                    /**
74
                     * @var Query|FieldAst $queryField
75
                     * @var Field          $astField
76
                     */
77 2
                    $cost = $reducer->visit($queryField->getKeyValueArguments(), $astField->getConfig(), $childCost);
78 2
                    $queryCost += $cost;
79 2
                    $results = $coroutine->send($cost);
80
                }
81
            }
82
        }
83 2
    }
84
85
    /**
86
     * Coroutine to walk the query and schema in DFS manner (see AbstractQueryVisitor docs for more info) and yield a
87
     * tuple of (queryNode, schemaNode, childScore)
88
     *
89
     * childScore costs are accumulated via values sent into the coroutine.
90
     *
91
     * Most of the branching in this function is just to handle the different types in a query: Queries, Unions,
92
     * Fragments (anonymous and named), and Fields.  The core of the function is simple: recurse until we hit the base
93
     * case of a Field and yield that back up to the visitor up in `doVisit`.
94
     *
95
     * @param Query|Field|\Youshido\GraphQL\Parser\Ast\Interfaces\FragmentInterface $queryNode
96
     * @param FieldInterface                                                        $currentLevelAST
97
     *
98
     * @return \Generator
99
     */
100 2
    protected function walkQuery($queryNode, FieldInterface $currentLevelAST)
101
    {
102 2
        $childrenScore = 0;
103 2
        if (!($queryNode instanceof FieldAst)) {
104 2
            foreach ($queryNode->getFields() as $queryField) {
0 ignored issues
show
Bug introduced by
The method getFields does only exist in Youshido\GraphQL\Field\F...raphQL\Parser\Ast\Query, but not in Youshido\GraphQL\Parser\...faces\FragmentInterface.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
105 2
                if ($queryField instanceof FragmentInterface) {
106 1
                    if ($queryField instanceof FragmentReference) {
107
                        $queryField = $this->executionContext->getRequest()->getFragment($queryField->getName());
108
                    }
109
                    // the next 7 lines are essentially equivalent to `yield from $this->walkQuery(...)` in PHP7.
110
                    // for backwards compatibility this is equivalent.
111
                    // This pattern is repeated multiple times in this function, and unfortunately cannot be extracted or
112
                    // made less verbose.
113 1
                    $gen  = $this->walkQuery($queryField, $currentLevelAST);
0 ignored issues
show
Bug introduced by
It seems like $queryField defined by $this->executionContext-...$queryField->getName()) on line 107 can also be of type null or object<Youshido\GraphQL\Parser\Ast\Fragment>; however, Youshido\GraphQL\Execution\Reducer::walkQuery() does only seem to accept object<Youshido\GraphQL\...aces\FragmentInterface>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
114 1
                    $next = $gen->current();
115 1
                    while ($next) {
116 1
                        $received = (yield $next);
117 1
                        $childrenScore += (int)$received;
118 1
                        $next = $gen->send($received);
119
                    }
120
                } else {
121 2
                    $fieldType = $currentLevelAST->getType()->getNamedType();
122 2
                    if ($fieldType instanceof AbstractUnionType) {
123 1
                        foreach ($fieldType->getTypes() as $unionFieldType) {
124 1 View Code Duplication
                            if ($fieldAst = $unionFieldType->getField($queryField->getName())) {
0 ignored issues
show
Bug introduced by
The method getField does only exist in Youshido\GraphQL\Type\Object\AbstractObjectType, but not in Youshido\GraphQL\Type\Scalar\AbstractScalarType.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
125 1
                                $gen  = $this->walkQuery($queryField, $fieldAst);
126 1
                                $next = $gen->current();
127 1
                                while ($next) {
128 1
                                    $received = (yield $next);
129 1
                                    $childrenScore += (int)$received;
130 1
                                    $next = $gen->send($received);
131
                                }
132
                            }
133
                        }
134 1 View Code Duplication
                    } elseif ($fieldType instanceof AbstractObjectType && $fieldAst = $fieldType->getField($queryField->getName())) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
135 1
                        $gen  = $this->walkQuery($queryField, $fieldAst);
136 1
                        $next = $gen->current();
137 2
                        while ($next) {
138 1
                            $received = (yield $next);
139 1
                            $childrenScore += (int)$received;
140 1
                            $next = $gen->send($received);
141
                        }
142
                    }
143
                }
144
            }
145
        }
146
        // sanity check.  don't yield fragments; they don't contribute to cost
147 2
        if ($queryNode instanceof Query || $queryNode instanceof FieldAst) {
148
            // BASE CASE.  If we're here we're done recursing -
149
            // this node is either a field, or a query that we've finished recursing into.
150 2
            yield [$queryNode, $currentLevelAST, $childrenScore];
151
        }
152 2
    }
153
154
}