Failed Conditions
Push — master ( 2ade86...13f838 )
by Jonathan
18s
created

ORM/Tools/Pagination/CountOutputWalker.php (3 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ORM\Tools\Pagination;
21
22
use Doctrine\ORM\Query\SqlWalker;
23
use Doctrine\ORM\Query\AST\SelectStatement;
24
25
/**
26
 * Wraps the query in order to accurately count the root objects.
27
 *
28
 * Given a DQL like `SELECT u FROM User u` it will generate an SQL query like:
29
 * SELECT COUNT(*) (SELECT DISTINCT <id> FROM (<original SQL>))
30
 *
31
 * Works with composite keys but cannot deal with queries that have multiple
32
 * root entities (e.g. `SELECT f, b from Foo, Bar`)
33
 *
34
 * @author Sander Marechal <[email protected]>
35
 */
36
class CountOutputWalker extends SqlWalker
37
{
38
    /**
39
     * @var \Doctrine\DBAL\Platforms\AbstractPlatform
40
     */
41
    private $platform;
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
42
43
    /**
44
     * @var \Doctrine\ORM\Query\ResultSetMapping
45
     */
46
    private $rsm;
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
47
48
    /**
49
     * @var array
50
     */
51
    private $queryComponents;
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
52
53
    /**
54
     * Constructor.
55
     *
56
     * Stores various parameters that are otherwise unavailable
57
     * because Doctrine\ORM\Query\SqlWalker keeps everything private without
58
     * accessors.
59
     *
60
     * @param \Doctrine\ORM\Query              $query
61
     * @param \Doctrine\ORM\Query\ParserResult $parserResult
62
     * @param array                            $queryComponents
63
     */
64 13
    public function __construct($query, $parserResult, array $queryComponents)
65
    {
66 13
        $this->platform = $query->getEntityManager()->getConnection()->getDatabasePlatform();
67 13
        $this->rsm = $parserResult->getResultSetMapping();
68 13
        $this->queryComponents = $queryComponents;
69
70 13
        parent::__construct($query, $parserResult, $queryComponents);
71 13
    }
72
73
    /**
74
     * Walks down a SelectStatement AST node, wrapping it in a COUNT (SELECT DISTINCT).
75
     *
76
     * Note that the ORDER BY clause is not removed. Many SQL implementations (e.g. MySQL)
77
     * are able to cache subqueries. By keeping the ORDER BY clause intact, the limitSubQuery
78
     * that will most likely be executed next can be read from the native SQL cache.
79
     *
80
     * @param SelectStatement $AST
81
     *
82
     * @return string
83
     *
84
     * @throws \RuntimeException
85
     */
86 13
    public function walkSelectStatement(SelectStatement $AST)
87
    {
88 13
        if ($this->platform->getName() === "mssql") {
89
            $AST->orderByClause = null;
90
        }
91
92 13
        $sql = parent::walkSelectStatement($AST);
93
94 13
        if ($AST->groupByClause) {
95 3
            return sprintf(
96 3
                'SELECT %s AS dctrn_count FROM (%s) dctrn_table',
97 3
                $this->platform->getCountExpression('*'),
98 3
                $sql
99
            );
100
        }
101
102
        // Find out the SQL alias of the identifier column of the root entity
103
        // It may be possible to make this work with multiple root entities but that
104
        // would probably require issuing multiple queries or doing a UNION SELECT
105
        // so for now, It's not supported.
106
107
        // Get the root entity and alias from the AST fromClause
108 10
        $from = $AST->fromClause->identificationVariableDeclarations;
109 10
        if (count($from) > 1) {
110
            throw new \RuntimeException("Cannot count query which selects two FROM components, cannot make distinction");
111
        }
112
113 10
        $fromRoot       = reset($from);
114 10
        $rootAlias      = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable;
115 10
        $rootClass      = $this->queryComponents[$rootAlias]['metadata'];
116 10
        $rootIdentifier = $rootClass->identifier;
117
118
        // For every identifier, find out the SQL alias by combing through the ResultSetMapping
119 10
        $sqlIdentifier = [];
120 10
        foreach ($rootIdentifier as $property) {
121 10
            if (isset($rootClass->fieldMappings[$property])) {
122 9
                foreach (array_keys($this->rsm->fieldMappings, $property) as $alias) {
123 9
                    if ($this->rsm->columnOwnerMap[$alias] == $rootAlias) {
124 9
                        $sqlIdentifier[$property] = $alias;
125
                    }
126
                }
127
            }
128
129 10
            if (isset($rootClass->associationMappings[$property])) {
130 1
                $joinColumn = $rootClass->associationMappings[$property]['joinColumns'][0]['name'];
131
132 1
                foreach (array_keys($this->rsm->metaMappings, $joinColumn) as $alias) {
133 1
                    if ($this->rsm->columnOwnerMap[$alias] == $rootAlias) {
134 10
                        $sqlIdentifier[$property] = $alias;
135
                    }
136
                }
137
            }
138
        }
139
140 10
        if (count($rootIdentifier) != count($sqlIdentifier)) {
141
            throw new \RuntimeException(sprintf(
142
                'Not all identifier properties can be found in the ResultSetMapping: %s',
143
                implode(', ', array_diff($rootIdentifier, array_keys($sqlIdentifier)))
144
            ));
145
        }
146
147
        // Build the counter query
148 10
        return sprintf('SELECT %s AS dctrn_count FROM (SELECT DISTINCT %s FROM (%s) dctrn_result) dctrn_table',
149 10
            $this->platform->getCountExpression('*'),
150 10
            implode(', ', $sqlIdentifier),
151 10
            $sql
152
        );
153
    }
154
}
155