Failed Conditions
Pull Request — master (#6392)
by Alessandro
11:37
created

walkSelectStatementWithoutRowNumber()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 44
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 44
c 0
b 0
f 0
ccs 16
cts 16
cp 1
rs 8.5806
cc 4
eloc 16
nc 4
nop 2
crap 4
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\DBAL\Platforms\DB2Platform;
23
use Doctrine\DBAL\Platforms\OraclePlatform;
24
use Doctrine\DBAL\Platforms\PostgreSqlPlatform;
25
use Doctrine\DBAL\Platforms\SQLAnywherePlatform;
26
use Doctrine\DBAL\Platforms\SQLServerPlatform;
27
use Doctrine\ORM\Query\AST\OrderByClause;
28
use Doctrine\ORM\Query\AST\PartialObjectExpression;
29
use Doctrine\ORM\Query\AST\SelectExpression;
30
use Doctrine\ORM\Query\SqlWalker;
31
use Doctrine\ORM\Query\AST\SelectStatement;
32
33
/**
34
 * Wraps the query in order to select root entity IDs for pagination.
35
 *
36
 * Given a DQL like `SELECT u FROM User u` it will generate an SQL query like:
37
 * SELECT DISTINCT <id> FROM (<original SQL>) LIMIT x OFFSET y
38
 *
39
 * Works with composite keys but cannot deal with queries that have multiple
40
 * root entities (e.g. `SELECT f, b from Foo, Bar`)
41
 *
42
 * @author Sander Marechal <[email protected]>
43
 */
44
class LimitSubqueryOutputWalker extends SqlWalker
45
{
46
    /**
47
     * @var \Doctrine\DBAL\Platforms\AbstractPlatform
48
     */
49
    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...
50
51
    /**
52
     * @var \Doctrine\ORM\Query\ResultSetMapping
53
     */
54
    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...
55
56
    /**
57
     * @var array
58
     */
59
    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...
60
61
    /**
62
     * @var int
63
     */
64
    private $firstResult;
65
66
    /**
67
     * @var int
68
     */
69
    private $maxResults;
70
71
    /**
72
     * @var \Doctrine\ORM\EntityManager
73
     */
74
    private $em;
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...
75
76
    /**
77
     * The quote strategy.
78
     *
79
     * @var \Doctrine\ORM\Mapping\QuoteStrategy
80
     */
81
    private $quoteStrategy;
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...
82
83
    /**
84
     * @var array
85
     */
86
    private $orderByPathExpressions = [];
87
88
    /**
89
     * @var bool We don't want to add path expressions from sub-selects into the select clause of the containing query.
90
     *           This state flag simply keeps track on whether we are walking on a subquery or not
91
     */
92
    private $inSubSelect = false;
93
94
    /**
95
     * Constructor.
96
     *
97
     * Stores various parameters that are otherwise unavailable
98
     * because Doctrine\ORM\Query\SqlWalker keeps everything private without
99
     * accessors.
100
     *
101
     * @param \Doctrine\ORM\Query              $query
102
     * @param \Doctrine\ORM\Query\ParserResult $parserResult
103
     * @param array                            $queryComponents
104
     */
105 65
    public function __construct($query, $parserResult, array $queryComponents)
106
    {
107 65
        $this->platform = $query->getEntityManager()->getConnection()->getDatabasePlatform();
108 65
        $this->rsm = $parserResult->getResultSetMapping();
109 65
        $this->queryComponents = $queryComponents;
110
111
        // Reset limit and offset
112 65
        $this->firstResult = $query->getFirstResult();
113 65
        $this->maxResults = $query->getMaxResults();
114 65
        $query->setFirstResult(null)->setMaxResults(null);
115
116 65
        $this->em               = $query->getEntityManager();
117 65
        $this->quoteStrategy    = $this->em->getConfiguration()->getQuoteStrategy();
118
119 65
        parent::__construct($query, $parserResult, $queryComponents);
120 65
    }
121
122
    /**
123
     * Check if the platform supports the ROW_NUMBER window function.
124
     *
125
     * @return bool
126
     */
127 65
    private function platformSupportsRowNumber()
128
    {
129 65
        return $this->platform instanceof PostgreSqlPlatform
130 58
            || $this->platform instanceof SQLServerPlatform
131 58
            || $this->platform instanceof OraclePlatform
132 52
            || $this->platform instanceof SQLAnywherePlatform
133 52
            || $this->platform instanceof DB2Platform
134 52
            || (method_exists($this->platform, 'supportsRowNumberFunction')
135 65
                && $this->platform->supportsRowNumberFunction());
0 ignored issues
show
Bug introduced by
The method supportsRowNumberFunction() does not seem to exist on object<Doctrine\DBAL\Platforms\AbstractPlatform>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
136
    }
137
138
    /**
139
     * Rebuilds a select statement's order by clause for use in a
140
     * ROW_NUMBER() OVER() expression.
141
     *
142
     * @param SelectStatement $AST
143
     */
144 11
    private function rebuildOrderByForRowNumber(SelectStatement $AST)
145
    {
146 11
        $orderByClause = $AST->orderByClause;
147 11
        $selectAliasToExpressionMap = [];
148
        // Get any aliases that are available for select expressions.
149 11
        foreach ($AST->selectClause->selectExpressions as $selectExpression) {
150 11
            $selectAliasToExpressionMap[$selectExpression->fieldIdentificationVariable] = $selectExpression->expression;
151
        }
152
153
        // Rebuild string orderby expressions to use the select expression they're referencing
154 11
        foreach ($orderByClause->orderByItems as $orderByItem) {
155 11
            if (is_string($orderByItem->expression) && isset($selectAliasToExpressionMap[$orderByItem->expression])) {
156 7
                $orderByItem->expression = $selectAliasToExpressionMap[$orderByItem->expression];
157
            }
158
        }
159 11
        $func = new RowNumberOverFunction('dctrn_rownum');
160 11
        $func->orderByClause = $AST->orderByClause;
161 11
        $AST->selectClause->selectExpressions[] = new SelectExpression($func, 'dctrn_rownum', true);
162
163
        // No need for an order by clause, we'll order by rownum in the outer query.
164 11
        $AST->orderByClause = null;
165 11
    }
166
167
    /**
168
     * Walks down a SelectStatement AST node, wrapping it in a SELECT DISTINCT.
169
     *
170
     * @param SelectStatement $AST
171
     *
172
     * @return string
173
     *
174
     * @throws \RuntimeException
175
     */
176 65
    public function walkSelectStatement(SelectStatement $AST)
177
    {
178 65
        if ($this->platformSupportsRowNumber()) {
179 13
            return $this->walkSelectStatementWithRowNumber($AST);
180
        }
181 52
        return $this->walkSelectStatementWithoutRowNumber($AST);
182
    }
183
184
    /**
185
     * Walks down a SelectStatement AST node, wrapping it in a SELECT DISTINCT.
186
     * This method is for use with platforms which support ROW_NUMBER.
187
     *
188
     * @param SelectStatement $AST
189
     *
190
     * @return string
191
     *
192
     * @throws \RuntimeException
193
     */
194 13
    public function walkSelectStatementWithRowNumber(SelectStatement $AST)
195
    {
196 13
        $hasOrderBy = false;
197 13
        $outerOrderBy = ' ORDER BY dctrn_minrownum ASC';
198 13
        $orderGroupBy = '';
199 13
        if ($AST->orderByClause instanceof OrderByClause) {
200 11
            $hasOrderBy = true;
201 11
            $this->rebuildOrderByForRowNumber($AST);
202
        }
203
204 13
        $innerSql = $this->getInnerSQL($AST);
205
206 13
        $sqlIdentifier = $this->getSQLIdentifier($AST);
207
208 13
        if ($hasOrderBy) {
209 11
            $orderGroupBy = ' GROUP BY ' . implode(', ', $sqlIdentifier);
210 11
            $sqlIdentifier[] = 'MIN(' . $this->walkResultVariable('dctrn_rownum') . ') AS dctrn_minrownum';
211
        }
212
213
        // Build the counter query
214 13
        $sql = sprintf(
215 13
            'SELECT DISTINCT %s FROM (%s) dctrn_result',
216 13
            implode(', ', $sqlIdentifier),
217 13
            $innerSql
218
        );
219
220 13
        if ($hasOrderBy) {
221 11
            $sql .= $orderGroupBy . $outerOrderBy;
222
        }
223
224
        // Apply the limit and offset.
225 13
        $sql = $this->platform->modifyLimitQuery(
226 13
            $sql,
227 13
            $this->maxResults,
228 13
            $this->firstResult
229
        );
230
231
        // Add the columns to the ResultSetMapping. It's not really nice but
232
        // it works. Preferably I'd clear the RSM or simply create a new one
233
        // but that is not possible from inside the output walker, so we dirty
234
        // up the one we have.
235 13
        foreach ($sqlIdentifier as $property => $alias) {
236 13
            $this->rsm->addScalarResult($alias, $property);
237
        }
238
239 13
        return $sql;
240
    }
241
242
    /**
243
     * Walks down a SelectStatement AST node, wrapping it in a SELECT DISTINCT.
244
     * This method is for platforms which DO NOT support ROW_NUMBER.
245
     *
246
     * @param SelectStatement $AST
247
     * @param bool $addMissingItemsFromOrderByToSelect
248
     *
249
     * @return string
250
     *
251
     * @throws \RuntimeException
252
     */
253 52
    public function walkSelectStatementWithoutRowNumber(SelectStatement $AST, $addMissingItemsFromOrderByToSelect = true)
254
    {
255
        // We don't want to call this recursively!
256 52
        if ($AST->orderByClause instanceof OrderByClause && $addMissingItemsFromOrderByToSelect) {
257
            // In the case of ordering a query by columns from joined tables, we
258
            // must add those columns to the select clause of the query BEFORE
259
            // the SQL is generated.
260 44
            $this->addMissingItemsFromOrderByToSelect($AST);
261
        }
262
263
        // Remove order by clause from the inner query
264
        // It will be re-appended in the outer select generated by this method
265 52
        $orderByClause = $AST->orderByClause;
266 52
        $AST->orderByClause = null;
267
268 52
        $innerSql = $this->getInnerSQL($AST);
269
270 52
        $sqlIdentifier = $this->getSQLIdentifier($AST);
271
272
        // Build the counter query
273 52
        $sql = sprintf('SELECT DISTINCT %s FROM (%s) dctrn_result',
274 52
            implode(', ', $sqlIdentifier), $innerSql);
275
276
        // http://www.doctrine-project.org/jira/browse/DDC-1958
277 52
        $sql = $this->preserveSqlOrdering($sqlIdentifier, $innerSql, $sql, $orderByClause);
0 ignored issues
show
Bug introduced by
It seems like $orderByClause defined by $AST->orderByClause on line 265 can be null; however, Doctrine\ORM\Tools\Pagin...::preserveSqlOrdering() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
278
279
        // Apply the limit and offset.
280 52
        $sql = $this->platform->modifyLimitQuery(
281 52
            $sql, $this->maxResults, $this->firstResult
282
        );
283
284
        // Add the columns to the ResultSetMapping. It's not really nice but
285
        // it works. Preferably I'd clear the RSM or simply create a new one
286
        // but that is not possible from inside the output walker, so we dirty
287
        // up the one we have.
288 52
        foreach ($sqlIdentifier as $property => $alias) {
289 52
            $this->rsm->addScalarResult($alias, $property);
290
        }
291
292
        // Restore orderByClause
293 52
        $AST->orderByClause = $orderByClause;
294
295 52
        return $sql;
296
    }
297
298
    /**
299
     * Finds all PathExpressions in an AST's OrderByClause, and ensures that
300
     * the referenced fields are present in the SelectClause of the passed AST.
301
     *
302
     * @param SelectStatement $AST
303
     */
304 44
    private function addMissingItemsFromOrderByToSelect(SelectStatement $AST)
305
    {
306 44
        $this->orderByPathExpressions = [];
307
308
        // We need to do this in another walker because otherwise we'll end up
309
        // polluting the state of this one.
310 44
        $walker = clone $this;
311
312
        // This will populate $orderByPathExpressions via
313
        // LimitSubqueryOutputWalker::walkPathExpression, which will be called
314
        // as the select statement is walked. We'll end up with an array of all
315
        // path expressions referenced in the query.
316 44
        $walker->walkSelectStatementWithoutRowNumber($AST, false);
317 44
        $orderByPathExpressions = $walker->getOrderByPathExpressions();
318
319
        // Get a map of referenced identifiers to field names.
320 44
        $selects = [];
321 44
        foreach ($orderByPathExpressions as $pathExpression) {
322 40
            $idVar = $pathExpression->identificationVariable;
323 40
            $field = $pathExpression->field;
324 40
            if (!isset($selects[$idVar])) {
325 40
                $selects[$idVar] = [];
326
            }
327 40
            $selects[$idVar][$field] = true;
328
        }
329
330
        // Loop the select clause of the AST and exclude items from $select
331
        // that are already being selected in the query.
332 44
        foreach ($AST->selectClause->selectExpressions as $selectExpression) {
333 44
            if ($selectExpression instanceof SelectExpression) {
334 44
                $idVar = $selectExpression->expression;
335 44
                if (!is_string($idVar)) {
336 4
                    continue;
337
                }
338 44
                $field = $selectExpression->fieldIdentificationVariable;
339 44
                if ($field === null) {
340
                    // No need to add this select, as we're already fetching the whole object.
341 44
                    unset($selects[$idVar]);
342
                } else {
343
                    unset($selects[$idVar][$field]);
344
                }
345
            }
346
        }
347
348
        // Add select items which were not excluded to the AST's select clause.
349 44
        foreach ($selects as $idVar => $fields) {
350 9
            $AST->selectClause->selectExpressions[] = new SelectExpression(new PartialObjectExpression($idVar, array_keys($fields)), null, true);
351
        }
352 44
    }
353
354
    /**
355
     * Generates new SQL for statements with an order by clause
356
     *
357
     * @param array           $sqlIdentifier
358
     * @param string          $innerSql
359
     * @param string          $sql
360
     * @param OrderByClause   $orderByClause
361
     *
362
     * @return string
363
     */
364 52
    private function preserveSqlOrdering(array $sqlIdentifier, $innerSql, $sql, $orderByClause)
365
    {
366
        // If the sql statement has an order by clause, we need to wrap it in a new select distinct
367
        // statement
368 52
        if (! $orderByClause instanceof OrderByClause) {
369 8
            return $sql;
370
        }
371
372
        // Rebuild the order by clause to work in the scope of the new select statement
373
        /* @var array $orderBy an array of rebuilt order by items */
374 44
        $orderBy = $this->rebuildOrderByClauseForOuterScope($orderByClause);
375
376
        // Build the select distinct statement
377 44
        $sql = sprintf(
378 44
            'SELECT DISTINCT %s FROM (%s) dctrn_result ORDER BY %s',
379 44
            implode(', ', $sqlIdentifier),
380 44
            $innerSql,
381 44
            implode(', ', $orderBy)
382
        );
383
384 44
        return $sql;
385
    }
386
387
    /**
388
     * Generates a new order by clause that works in the scope of a select query wrapping the original
389
     *
390
     * @param OrderByClause $orderByClause
391
     * @return array
392
     */
393 44
    private function rebuildOrderByClauseForOuterScope(OrderByClause $orderByClause)
394
    {
395
        $dqlAliasToSqlTableAliasMap
396
            = $searchPatterns
397
            = $replacements
398
            = $dqlAliasToClassMap
399
            = $selectListAdditions
0 ignored issues
show
Unused Code introduced by
$selectListAdditions is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
400
            = $orderByItems
401 44
            = [];
402
403
        // Generate DQL alias -> SQL table alias mapping
404 44
        foreach(array_keys($this->rsm->aliasMap) as $dqlAlias) {
405 44
            $dqlAliasToClassMap[$dqlAlias] = $class = $this->queryComponents[$dqlAlias]['metadata'];
406 44
            $dqlAliasToSqlTableAliasMap[$dqlAlias] = $this->getSQLTableAlias($class->getTableName(), $dqlAlias);
407
        }
408
409
        // Pattern to find table path expressions in the order by clause
410 44
        $fieldSearchPattern = '/(?<![a-z0-9_])%s\.%s(?![a-z0-9_])/i';
411
412
        // Generate search patterns for each field's path expression in the order by clause
413 44
        foreach($this->rsm->fieldMappings as $fieldAlias => $fieldName) {
414 44
            $dqlAliasForFieldAlias = $this->rsm->columnOwnerMap[$fieldAlias];
415 44
            $class = $dqlAliasToClassMap[$dqlAliasForFieldAlias];
416
417
            // If the field is from a joined child table, we won't be ordering
418
            // on it.
419 44
            if (!isset($class->fieldMappings[$fieldName])) {
420 1
                continue;
421
            }
422
423 44
            $fieldMapping = $class->fieldMappings[$fieldName];
424
425
            // Get the proper column name as will appear in the select list
426 44
            $columnName = $this->quoteStrategy->getColumnName(
427 44
                $fieldName,
428 44
                $dqlAliasToClassMap[$dqlAliasForFieldAlias],
429 44
                $this->em->getConnection()->getDatabasePlatform()
430
            );
431
432
            // Get the SQL table alias for the entity and field
433 44
            $sqlTableAliasForFieldAlias = $dqlAliasToSqlTableAliasMap[$dqlAliasForFieldAlias];
434 44
            if (isset($fieldMapping['declared']) && $fieldMapping['declared'] !== $class->name) {
435
                // Field was declared in a parent class, so we need to get the proper SQL table alias
436
                // for the joined parent table.
437 2
                $otherClassMetadata = $this->em->getClassMetadata($fieldMapping['declared']);
438 2
                if (!$otherClassMetadata->isMappedSuperclass) {
439 1
                    $sqlTableAliasForFieldAlias = $this->getSQLTableAlias($otherClassMetadata->getTableName(), $dqlAliasForFieldAlias);
440
                }
441
            }
442
443
            // Compose search/replace patterns
444 44
            $searchPatterns[] = sprintf($fieldSearchPattern, $sqlTableAliasForFieldAlias, $columnName);
445 44
            $replacements[] = $fieldAlias;
446
        }
447
448 44
        foreach($orderByClause->orderByItems as $orderByItem) {
449
            // Walk order by item to get string representation of it
450 44
            $orderByItemString = $this->walkOrderByItem($orderByItem);
451
452
            // Replace path expressions in the order by clause with their column alias
453 44
            $orderByItemString = preg_replace($searchPatterns, $replacements, $orderByItemString);
454
455 44
            $orderByItems[] = $orderByItemString;
456
        }
457
458 44
        return $orderByItems;
459
    }
460
461
    /**
462
     * getter for $orderByPathExpressions
463
     *
464
     * @return array
465
     */
466 44
    public function getOrderByPathExpressions()
467
    {
468 44
        return $this->orderByPathExpressions;
469
    }
470
471
    /**
472
     * @param SelectStatement $AST
473
     *
474
     * @return string
475
     *
476
     * @throws \Doctrine\ORM\OptimisticLockException
477
     * @throws \Doctrine\ORM\Query\QueryException
478
     */
479 65
    private function getInnerSQL(SelectStatement $AST)
480
    {
481
        // Set every select expression as visible(hidden = false) to
482
        // make $AST have scalar mappings properly - this is relevant for referencing selected
483
        // fields from outside the subquery, for example in the ORDER BY segment
484 65
        $hiddens = [];
485
486 65
        foreach ($AST->selectClause->selectExpressions as $idx => $expr) {
487 65
            $hiddens[$idx] = $expr->hiddenAliasResultVariable;
488 65
            $expr->hiddenAliasResultVariable = false;
489
        }
490
491 65
        $innerSql = parent::walkSelectStatement($AST);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (walkSelectStatement() instead of getInnerSQL()). Are you sure this is correct? If so, you might want to change this to $this->walkSelectStatement().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
492
493
        // Restore hiddens
494 65
        foreach ($AST->selectClause->selectExpressions as $idx => $expr) {
495 65
            $expr->hiddenAliasResultVariable = $hiddens[$idx];
496
        }
497
498 65
        return $innerSql;
499
    }
500
501
    /**
502
     * @param SelectStatement $AST
503
     *
504
     * @return array
505
     */
506 65
    private function getSQLIdentifier(SelectStatement $AST)
507
    {
508
        // Find out the SQL alias of the identifier column of the root entity.
509
        // It may be possible to make this work with multiple root entities but that
510
        // would probably require issuing multiple queries or doing a UNION SELECT.
511
        // So for now, it's not supported.
512
513
        // Get the root entity and alias from the AST fromClause.
514 65
        $from = $AST->fromClause->identificationVariableDeclarations;
515 65
        if (count($from) !== 1) {
516
            throw new \RuntimeException('Cannot count query which selects two FROM components, cannot make distinction');
517
        }
518
519 65
        $fromRoot       = reset($from);
520 65
        $rootAlias      = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable;
521 65
        $rootClass      = $this->queryComponents[$rootAlias]['metadata'];
522 65
        $rootIdentifier = $rootClass->identifier;
523
524
        // For every identifier, find out the SQL alias by combing through the ResultSetMapping
525 65
        $sqlIdentifier = [];
526 65 View Code Duplication
        foreach ($rootIdentifier as $property) {
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...
527 65
            if (isset($rootClass->fieldMappings[$property])) {
528 64
                foreach (array_keys($this->rsm->fieldMappings, $property) as $alias) {
529 64
                    if ($this->rsm->columnOwnerMap[$alias] == $rootAlias) {
530 64
                        $sqlIdentifier[$property] = $alias;
531
                    }
532
                }
533
            }
534
535 65
            if (isset($rootClass->associationMappings[$property])) {
536 1
                $joinColumn = $rootClass->associationMappings[$property]['joinColumns'][0]['name'];
537
538 1
                foreach (array_keys($this->rsm->metaMappings, $joinColumn) as $alias) {
539 1
                    if ($this->rsm->columnOwnerMap[$alias] == $rootAlias) {
540 1
                        $sqlIdentifier[$property] = $alias;
541
                    }
542
                }
543
            }
544
        }
545
546 65
        if (count($sqlIdentifier) === 0) {
547
            throw new \RuntimeException('The Paginator does not support Queries which only yield ScalarResults.');
548
        }
549
550 65 View Code Duplication
        if (count($rootIdentifier) != count($sqlIdentifier)) {
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...
551
            throw new \RuntimeException(sprintf(
552
                'Not all identifier properties can be found in the ResultSetMapping: %s',
553
                implode(', ', array_diff($rootIdentifier, array_keys($sqlIdentifier)))
554
            ));
555
        }
556
557 65
        return $sqlIdentifier;
558
    }
559
560
    /**
561
     * {@inheritdoc}
562
     */
563 55
    public function walkPathExpression($pathExpr)
564
    {
565 55
        if (!$this->inSubSelect && !$this->platformSupportsRowNumber() && !in_array($pathExpr, $this->orderByPathExpressions)) {
566 42
            $this->orderByPathExpressions[] = $pathExpr;
567
        }
568
569 55
        return parent::walkPathExpression($pathExpr);
570
    }
571
572
    /**
573
     * {@inheritdoc}
574
     */
575 7
    public function walkSubSelect($subselect)
576
    {
577 7
        $this->inSubSelect = true;
578
579 7
        $sql = parent::walkSubselect($subselect);
580
581 7
        $this->inSubSelect = false;
582
583 7
        return $sql;
584
    }
585
}
586