Failed Conditions
Push — master ( 8be1e3...e3936d )
by Marco
14s
created

LimitSubqueryWalker::walkSelectStatement()   B

Complexity

Conditions 8
Paths 7

Size

Total Lines 53
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 33
CRAP Score 8

Importance

Changes 0
Metric Value
cc 8
eloc 33
nc 7
nop 1
dl 0
loc 53
ccs 33
cts 33
cp 1
crap 8
rs 7.1199
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ORM\Tools\Pagination;
6
7
use Doctrine\DBAL\Types\Type;
8
use Doctrine\ORM\Mapping\AssociationMetadata;
9
use Doctrine\ORM\Mapping\ToManyAssociationMetadata;
10
use Doctrine\ORM\Query;
11
use Doctrine\ORM\Query\AST\Functions\IdentityFunction;
12
use Doctrine\ORM\Query\AST\PathExpression;
13
use Doctrine\ORM\Query\AST\SelectExpression;
14
use Doctrine\ORM\Query\AST\SelectStatement;
15
use Doctrine\ORM\Query\TreeWalkerAdapter;
16
17
/**
18
 * Replaces the selectClause of the AST with a SELECT DISTINCT root.id equivalent.
19
 */
20
class LimitSubqueryWalker extends TreeWalkerAdapter
21
{
22
    /**
23
     * ID type hint.
24
     */
25
    public const IDENTIFIER_TYPE = 'doctrine_paginator.id.type';
26
27
    /**
28
     * Counter for generating unique order column aliases.
29
     *
30
     * @var int
31
     */
32
    private $aliasCounter = 0;
33
34
    /**
35
     * Walks down a SelectStatement AST node, modifying it to retrieve DISTINCT ids
36
     * of the root Entity.
37
     *
38
     * @throws \RuntimeException
39
     */
40 33
    public function walkSelectStatement(SelectStatement $AST)
41
    {
42 33
        $queryComponents = $this->getQueryComponents();
43
        // Get the root entity and alias from the AST fromClause
44 33
        $from      = $AST->fromClause->identificationVariableDeclarations;
45 33
        $fromRoot  = reset($from);
46 33
        $rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable;
47 33
        $rootClass = $queryComponents[$rootAlias]['metadata'];
48
49 33
        $this->validate($AST);
50 31
        $property = $rootClass->getProperty($rootClass->getSingleIdentifierFieldName());
51
52 31
        if ($property instanceof AssociationMetadata) {
53 1
            throw new \RuntimeException(
54
                'Paginating an entity with foreign key as identifier only works when using the Output Walkers. ' .
55 1
                'Call Paginator#setUseOutputWalkers(true) before iterating the paginator.'
56
            );
57
        }
58
59 30
        $this->getQuery()->setHint(self::IDENTIFIER_TYPE, $property->getType());
60
61 30
        $pathExpression = new PathExpression(
62 30
            PathExpression::TYPE_STATE_FIELD | PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION,
63 30
            $rootAlias,
64 30
            $property->getName()
65
        );
66
67 30
        $pathExpression->type = PathExpression::TYPE_STATE_FIELD;
68
69 30
        $AST->selectClause->selectExpressions = [new SelectExpression($pathExpression, '_dctrn_id')];
70 30
        $AST->selectClause->isDistinct        = true;
71
72 30
        if (! isset($AST->orderByClause)) {
73 6
            return;
74
        }
75
76 24
        foreach ($AST->orderByClause->orderByItems as $item) {
77 24
            if ($item->expression instanceof PathExpression) {
78 20
                $AST->selectClause->selectExpressions[] = new SelectExpression(
79 20
                    $this->createSelectExpressionItem($item->expression),
80 20
                    '_dctrn_ord' . $this->aliasCounter++
81
                );
82
83 20
                continue;
84
            }
85
86 4
            if (is_string($item->expression) && isset($queryComponents[$item->expression])) {
87 3
                $qComp = $queryComponents[$item->expression];
88
89 3
                if (isset($qComp['resultVariable'])) {
90 3
                    $AST->selectClause->selectExpressions[] = new SelectExpression(
91 3
                        $qComp['resultVariable'],
92 4
                        $item->expression
93
                    );
94
                }
95
            }
96
        }
97 24
    }
98
99
    /**
100
     * Validate the AST to ensure that this walker is able to properly manipulate it.
101
     */
102 33
    private function validate(SelectStatement $AST)
103
    {
104
        // Prevent LimitSubqueryWalker from being used with queries that include
105
        // a limit, a fetched to-many join, and an order by condition that
106
        // references a column from the fetch joined table.
107 33
        $queryComponents = $this->getQueryComponents();
108 33
        $query           = $this->getQuery();
109 33
        $from            = $AST->fromClause->identificationVariableDeclarations;
110 33
        $fromRoot        = reset($from);
111
112 33
        if ($query instanceof Query && $query->getMaxResults() && $AST->orderByClause && count($fromRoot->joins)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $query->getMaxResults() of type null|integer is loosely compared to true; this is ambiguous if the integer can be 0. 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 integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
113
            // Check each orderby item.
114
            // TODO: check complex orderby items too...
115 10
            foreach ($AST->orderByClause->orderByItems as $orderByItem) {
116 10
                $expression = $orderByItem->expression;
117
118 10
                if ($expression instanceof PathExpression && isset($queryComponents[$expression->identificationVariable])) {
119 10
                    $queryComponent = $queryComponents[$expression->identificationVariable];
120
121 10
                    if (isset($queryComponent['parent']) && $queryComponent['relation'] instanceof ToManyAssociationMetadata) {
122 2
                        throw new \RuntimeException(
123
                            'Cannot select distinct identifiers from query with LIMIT and ORDER BY on a column from a '
124 10
                            . 'fetch joined to-many association. Use output walkers.'
125
                        );
126
                    }
127
                }
128
            }
129
        }
130 31
    }
131
132
    /**
133
     * Retrieve either an IdentityFunction (IDENTITY(u.assoc)) or a state field (u.name).
134
     *
135
     * @return \Doctrine\ORM\Query\AST\Functions\IdentityFunction
136
     */
137 20
    private function createSelectExpressionItem(PathExpression $pathExpression)
138
    {
139 20
        if ($pathExpression->type === PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION) {
140 1
            $identity = new IdentityFunction('identity');
141
142 1
            $identity->pathExpression = clone $pathExpression;
143
144 1
            return $identity;
145
        }
146
147 19
        return clone $pathExpression;
0 ignored issues
show
Bug Best Practice introduced by
The expression return clone $pathExpression returns the type Doctrine\ORM\Query\AST\PathExpression which is incompatible with the documented return type Doctrine\ORM\Query\AST\Functions\IdentityFunction.
Loading history...
148
    }
149
}
150