LimitSubqueryOutputWalker::walkPathExpression()   A
last analyzed

Complexity

Conditions 4
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 4.25

Importance

Changes 0
Metric Value
cc 4
eloc 4
nc 2
nop 1
dl 0
loc 8
rs 9.2
c 0
b 0
f 0
ccs 3
cts 4
cp 0.75
crap 4.25
1
<?php
2
/**
3
 * Doctrine ORM
4
 *
5
 * LICENSE
6
 *
7
 * This source file is subject to the new BSD license that is bundled
8
 * with this package in the file LICENSE.txt.
9
 * If you did not receive a copy of the license and are unable to
10
 * obtain it through the world-wide-web, please send an email
11
 * to [email protected] so I can send you a copy immediately.
12
 */
13
14
namespace Eccube\Doctrine\ORM\Tools\Pagination;
15
16
use Doctrine\DBAL\Platforms\DB2Platform;
17
use Doctrine\DBAL\Platforms\OraclePlatform;
18
use Doctrine\DBAL\Platforms\PostgreSqlPlatform;
19
use Doctrine\DBAL\Platforms\SQLAnywherePlatform;
20
use Doctrine\DBAL\Platforms\SQLServerPlatform;
21
use Doctrine\ORM\Query\AST\ArithmeticExpression;
22
use Doctrine\ORM\Query\AST\ArithmeticFactor;
23
use Doctrine\ORM\Query\AST\ArithmeticTerm;
24
use Doctrine\ORM\Query\AST\Literal;
25
use Doctrine\ORM\Query\AST\OrderByClause;
26
use Doctrine\ORM\Query\AST\OrderByItem;
27
use Doctrine\ORM\Query\AST\PartialObjectExpression;
28
use Doctrine\ORM\Query\AST\PathExpression;
29
use Doctrine\ORM\Query\AST\SelectExpression;
30
use Doctrine\ORM\Query\AST\SimpleArithmeticExpression;
31
use Doctrine\ORM\Query\Expr\OrderBy;
32
use Doctrine\ORM\Query\Expr\Select;
33
use Doctrine\ORM\Query\SqlWalker;
34
use Doctrine\ORM\Query\AST\SelectStatement;
35
36
/**
37
 * Wraps the query in order to select root entity IDs for pagination.
38
 *
39
 * Given a DQL like `SELECT u FROM User u` it will generate an SQL query like:
40
 * SELECT DISTINCT <id> FROM (<original SQL>) LIMIT x OFFSET y
41
 *
42
 * Works with composite keys but cannot deal with queries that have multiple
43
 * root entities (e.g. `SELECT f, b from Foo, Bar`)
44
 *
45
 * @author Sander Marechal <[email protected]>
46
 */
47
class LimitSubqueryOutputWalker extends SqlWalker
48
{
49
    /**
50
     * @var \Doctrine\DBAL\Platforms\AbstractPlatform
51
     */
52
    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...
53
54
    /**
55
     * @var \Doctrine\ORM\Query\ResultSetMapping
56
     */
57
    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...
58
59
    /**
60
     * @var array
61
     */
62
    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...
63
64
    /**
65
     * @var int
66
     */
67
    private $firstResult;
68
69
    /**
70
     * @var int
71
     */
72
    private $maxResults;
73
74
    /**
75
     * @var \Doctrine\ORM\EntityManager
76
     */
77
    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...
78
79
    /**
80
     * The quote strategy.
81
     *
82
     * @var \Doctrine\ORM\Mapping\QuoteStrategy
83
     */
84
    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...
85
86
    /**
87
     * @var array
88
     */
89
    private $orderByPathExpressions = array();
90
91
    /**
92
     * @var bool We don't want to add path expressions from sub-selects into the select clause of the containing query.
93
     *           This state flag simply keeps track on whether we are walking on a subquery or not
94
     */
95
    private $inSubSelect = false;
96
97
    /**
98
     * Constructor.
99
     *
100
     * Stores various parameters that are otherwise unavailable
101
     * because Doctrine\ORM\Query\SqlWalker keeps everything private without
102
     * accessors.
103
     *
104
     * @param \Doctrine\ORM\Query              $query
105
     * @param \Doctrine\ORM\Query\ParserResult $parserResult
106
     * @param array                            $queryComponents
107
     */
108 17
    public function __construct($query, $parserResult, array $queryComponents)
109
    {
110 17
        $this->platform = $query->getEntityManager()->getConnection()->getDatabasePlatform();
111 17
        $this->rsm = $parserResult->getResultSetMapping();
112 17
        $this->queryComponents = $queryComponents;
113
114
        // Reset limit and offset
115 17
        $this->firstResult = $query->getFirstResult();
116 17
        $this->maxResults = $query->getMaxResults();
117 17
        $query->setFirstResult(null)->setMaxResults(null);
118
119 17
        $this->em               = $query->getEntityManager();
120 17
        $this->quoteStrategy    = $this->em->getConfiguration()->getQuoteStrategy();
121
122 17
        parent::__construct($query, $parserResult, $queryComponents);
123
    }
124
125
    /**
126
     * Check if the platform supports the ROW_NUMBER window function.
127
     *
128
     * @return bool
129
     */
130 17
    private function platformSupportsRowNumber()
131
    {
132 17
        return $this->platform instanceof PostgreSqlPlatform
133 17
            || $this->platform instanceof SQLServerPlatform
134 17
            || $this->platform instanceof OraclePlatform
135 17
            || $this->platform instanceof SQLAnywherePlatform
136 17
            || $this->platform instanceof DB2Platform
137
            || (method_exists($this->platform, 'supportsRowNumberFunction')
138 17
                && $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...
139
    }
140
141
    /**
142
     * Rebuilds a select statement's order by clause for use in a
143
     * ROW_NUMBER() OVER() expression.
144
     *
145
     * @param SelectStatement $AST
146
     */
147 17
    private function rebuildOrderByForRowNumber(SelectStatement $AST)
148
    {
149 17
        $orderByClause = $AST->orderByClause;
150 17
        $selectAliasToExpressionMap = array();
151
        // Get any aliases that are available for select expressions.
152 17
        foreach ($AST->selectClause->selectExpressions as $selectExpression) {
153 17
            $selectAliasToExpressionMap[$selectExpression->fieldIdentificationVariable] = $selectExpression->expression;
154
        }
155
156
        // Rebuild string orderby expressions to use the select expression they're referencing
157 17
        foreach ($orderByClause->orderByItems as $orderByItem) {
158 17
            if (is_string($orderByItem->expression) && isset($selectAliasToExpressionMap[$orderByItem->expression])) {
159 17
                $orderByItem->expression = $selectAliasToExpressionMap[$orderByItem->expression];
160
            }
161
        }
162 17
        $func = new RowNumberOverFunction('dctrn_rownum');
163 17
        $func->orderByClause = $AST->orderByClause;
164 17
        $AST->selectClause->selectExpressions[] = new SelectExpression($func, 'dctrn_rownum', true);
165
166
        // No need for an order by clause, we'll order by rownum in the outer query.
167 17
        $AST->orderByClause = null;
168
    }
169
170
    /**
171
     * Walks down a SelectStatement AST node, wrapping it in a SELECT DISTINCT.
172
     *
173
     * @param SelectStatement $AST
174
     *
175
     * @return string
176
     *
177
     * @throws \RuntimeException
178
     */
179 17
    public function walkSelectStatement(SelectStatement $AST)
0 ignored issues
show
introduced by
Declare public methods first, then protected ones and finally private ones
Loading history...
180
    {
181 17
        if ($this->platformSupportsRowNumber()) {
182 17
            return $this->walkSelectStatementWithRowNumber($AST);
183
        }
184
        return $this->walkSelectStatementWithoutRowNumber($AST);
0 ignored issues
show
introduced by
Missing blank line before return statement
Loading history...
185
    }
186
187
    /**
188
     * Walks down a SelectStatement AST node, wrapping it in a SELECT DISTINCT.
189
     * This method is for use with platforms which support ROW_NUMBER.
190
     *
191
     * @param SelectStatement $AST
192
     *
193
     * @return string
194
     *
195
     * @throws \RuntimeException
196
     */
197 17
    public function walkSelectStatementWithRowNumber(SelectStatement $AST)
198
    {
199 17
        $hasOrderBy = false;
200 17
        $outerOrderBy = ' ORDER BY dctrn_minrownum ASC';
201 17
        $orderGroupBy = '';
202 17
        if ($AST->orderByClause instanceof OrderByClause) {
203 17
            $hasOrderBy = true;
204 17
            $this->rebuildOrderByForRowNumber($AST);
205
        }
206
207 17
        $innerSql = $this->getInnerSQL($AST);
208
209 17
        $sqlIdentifier = $this->getSQLIdentifier($AST);
210
211 17
        if ($hasOrderBy) {
212 17
            $orderGroupBy = ' GROUP BY ' . implode(', ', $sqlIdentifier);
0 ignored issues
show
Coding Style introduced by
Concat operator must not be surrounded by spaces
Loading history...
213 17
            $sqlIdentifier[] = 'MIN(' . $this->walkResultVariable('dctrn_rownum') . ') AS dctrn_minrownum';
0 ignored issues
show
Coding Style introduced by
Concat operator must not be surrounded by spaces
Loading history...
214
        }
215
216
        // Build the counter query
217 17
        $sql = sprintf(
218 17
            'SELECT DISTINCT %s FROM (%s) dctrn_result',
219 17
            implode(', ', $sqlIdentifier),
220
            $innerSql
221
        );
222
223 17
        if ($hasOrderBy) {
224 17
            $sql .= $orderGroupBy . $outerOrderBy;
0 ignored issues
show
Coding Style introduced by
Concat operator must not be surrounded by spaces
Loading history...
225
        }
226
227
        // Apply the limit and offset.
228 17
        $sql = $this->platform->modifyLimitQuery(
229
            $sql,
230 17
            $this->maxResults,
231 17
            $this->firstResult
232
        );
233
234
        // Add the columns to the ResultSetMapping. It's not really nice but
235
        // it works. Preferably I'd clear the RSM or simply create a new one
236
        // but that is not possible from inside the output walker, so we dirty
237
        // up the one we have.
238 17
        foreach ($sqlIdentifier as $property => $alias) {
239 17
            $this->rsm->addScalarResult($alias, $property);
240
        }
241
242 17
        return $sql;
243
    }
244
245
    /**
246
     * Walks down a SelectStatement AST node, wrapping it in a SELECT DISTINCT.
247
     * This method is for platforms which DO NOT support ROW_NUMBER.
248
     *
249
     * @param SelectStatement $AST
250
     * @param bool $addMissingItemsFromOrderByToSelect
0 ignored issues
show
introduced by
Expected 12 spaces after parameter type; 1 found
Loading history...
251
     *
252
     * @return string
253
     *
254
     * @throws \RuntimeException
255
     */
256
    public function walkSelectStatementWithoutRowNumber(SelectStatement $AST, $addMissingItemsFromOrderByToSelect = true)
257
    {
258
        // We don't want to call this recursively!
259
        if ($AST->orderByClause instanceof OrderByClause && $addMissingItemsFromOrderByToSelect) {
260
            // In the case of ordering a query by columns from joined tables, we
261
            // must add those columns to the select clause of the query BEFORE
262
            // the SQL is generated.
263
            $this->addMissingItemsFromOrderByToSelect($AST);
264
        }
265
266
        // Remove order by clause from the inner query
267
        // It will be re-appended in the outer select generated by this method
268
        $orderByClause = $AST->orderByClause;
269
        $AST->orderByClause = null;
270
271
        $innerSql = $this->getInnerSQL($AST);
272
273
        $sqlIdentifier = $this->getSQLIdentifier($AST);
274
275
        // Build the counter query
276
        $sql = sprintf('SELECT DISTINCT %s FROM (%s) dctrn_result',
277
            implode(', ', $sqlIdentifier), $innerSql);
278
279
        // http://www.doctrine-project.org/jira/browse/DDC-1958
280
        $sql = $this->preserveSqlOrdering($sqlIdentifier, $innerSql, $sql, $orderByClause);
0 ignored issues
show
Bug introduced by
It seems like $orderByClause defined by $AST->orderByClause on line 268 can be null; however, Eccube\Doctrine\ORM\Tool...::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...
281
282
        // Apply the limit and offset.
283
        $sql = $this->platform->modifyLimitQuery(
284
            $sql, $this->maxResults, $this->firstResult
285
        );
286
287
        // Add the columns to the ResultSetMapping. It's not really nice but
288
        // it works. Preferably I'd clear the RSM or simply create a new one
289
        // but that is not possible from inside the output walker, so we dirty
290
        // up the one we have.
291
        foreach ($sqlIdentifier as $property => $alias) {
292
            $this->rsm->addScalarResult($alias, $property);
293
        }
294
295
        // Restore orderByClause
296
        $AST->orderByClause = $orderByClause;
297
298
        return $sql;
299
    }
300
301
    /**
302
     * Finds all PathExpressions in an AST's OrderByClause, and ensures that
303
     * the referenced fields are present in the SelectClause of the passed AST.
304
     *
305
     * @param SelectStatement $AST
306
     */
307
    private function addMissingItemsFromOrderByToSelect(SelectStatement $AST)
308
    {
309
        $this->orderByPathExpressions = array();
310
311
        // We need to do this in another walker because otherwise we'll end up
312
        // polluting the state of this one.
313
        $walker = clone $this;
314
315
        // This will populate $orderByPathExpressions via
316
        // LimitSubqueryOutputWalker::walkPathExpression, which will be called
317
        // as the select statement is walked. We'll end up with an array of all
318
        // path expressions referenced in the query.
319
        $walker->walkSelectStatementWithoutRowNumber($AST, false);
320
        $orderByPathExpressions = $walker->getOrderByPathExpressions();
321
322
        // Get a map of referenced identifiers to field names.
323
        $selects = array();
324
        foreach ($orderByPathExpressions as $pathExpression) {
325
            $idVar = $pathExpression->identificationVariable;
326
            $field = $pathExpression->field;
327
            if (!isset($selects[$idVar])) {
328
                $selects[$idVar] = array();
329
            }
330
            $selects[$idVar][$field] = true;
331
        }
332
333
        // Loop the select clause of the AST and exclude items from $select
334
        // that are already being selected in the query.
335
        foreach ($AST->selectClause->selectExpressions as $selectExpression) {
336
            if ($selectExpression instanceof SelectExpression) {
337
                $idVar = $selectExpression->expression;
338
                if (!is_string($idVar)) {
339
                    continue;
340
                }
341
                $field = $selectExpression->fieldIdentificationVariable;
342
                if ($field === null) {
343
                    // No need to add this select, as we're already fetching the whole object.
344
                    unset($selects[$idVar]);
345
                } else {
346
                    unset($selects[$idVar][$field]);
347
                }
348
            }
349
        }
350
351
        // Add select items which were not excluded to the AST's select clause.
352
        foreach ($selects as $idVar => $fields) {
353
            $AST->selectClause->selectExpressions[] = new SelectExpression(new PartialObjectExpression($idVar, array_keys($fields)), null, true);
354
        }
355
    }
356
357
    /**
358
     * Generates new SQL for statements with an order by clause
359
     *
360
     * @param array           $sqlIdentifier
361
     * @param string          $innerSql
362
     * @param string          $sql
363
     * @param OrderByClause   $orderByClause
364
     *
365
     * @return string
366
     */
367
    private function preserveSqlOrdering(array $sqlIdentifier, $innerSql, $sql, $orderByClause)
368
    {
369
        // If the sql statement has an order by clause, we need to wrap it in a new select distinct
370
        // statement
371
        if (! $orderByClause instanceof OrderByClause) {
372
            return $sql;
373
        }
374
375
        // Rebuild the order by clause to work in the scope of the new select statement
376
        /* @var array $orderBy an array of rebuilt order by items */
377
        $orderBy = $this->rebuildOrderByClauseForOuterScope($orderByClause);
378
379
        $innerSqlIdentifier = $sqlIdentifier;
380
381
        foreach ($orderBy as $field) {
382
            $field = preg_replace('/((\S+)\s+(ASC|DESC)\s*,?)*/', '${2}', $field);
383
384
            // skip fields that are selected by identifiers,
385
            // if those are ordered by in the query
386
            if (in_array($field, $sqlIdentifier, true)) {
387
                continue;
388
            }
389
            $innerSqlIdentifier[] = $field;
390
        }
391
392
        // Build the innner select statement
393
        $sql = sprintf(
394
            'SELECT DISTINCT %s FROM (%s) dctrn_result_inner ORDER BY %s',
395
            implode(', ', $innerSqlIdentifier),
396
            $innerSql,
397
            implode(', ', $orderBy)
398
        );
399
400
        // now only select distinct identifier
401
        $sql = sprintf('SELECT DISTINCT %s FROM (%s) dctrn_result', implode(', ', $sqlIdentifier), $sql);
402
403
        return $sql;
404
    }
405
406
    /**
407
     * Generates a new order by clause that works in the scope of a select query wrapping the original
408
     *
409
     * @param OrderByClause $orderByClause
410
     * @return array
411
     */
412
    private function rebuildOrderByClauseForOuterScope(OrderByClause $orderByClause)
413
    {
414
        $dqlAliasToSqlTableAliasMap
415
            = $searchPatterns
416
            = $replacements
417
            = $dqlAliasToClassMap
418
            = $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...
419
            = $orderByItems
420
            = array();
421
422
        // Generate DQL alias -> SQL table alias mapping
423
        foreach(array_keys($this->rsm->aliasMap) as $dqlAlias) {
0 ignored issues
show
Coding Style introduced by
Expected 1 space after FOREACH keyword; 0 found
Loading history...
424
            $dqlAliasToClassMap[$dqlAlias] = $class = $this->queryComponents[$dqlAlias]['metadata'];
425
            $dqlAliasToSqlTableAliasMap[$dqlAlias] = $this->getSQLTableAlias($class->getTableName(), $dqlAlias);
426
        }
427
428
        // Pattern to find table path expressions in the order by clause
429
        $fieldSearchPattern = '/(?<![a-z0-9_])%s\.%s(?![a-z0-9_])/i';
430
431
        // Generate search patterns for each field's path expression in the order by clause
432
        foreach($this->rsm->fieldMappings as $fieldAlias => $fieldName) {
0 ignored issues
show
Coding Style introduced by
Expected 1 space after FOREACH keyword; 0 found
Loading history...
433
            $dqlAliasForFieldAlias = $this->rsm->columnOwnerMap[$fieldAlias];
434
            $class = $dqlAliasToClassMap[$dqlAliasForFieldAlias];
435
436
            // If the field is from a joined child table, we won't be ordering
437
            // on it.
438
            if (!isset($class->fieldMappings[$fieldName])) {
439
                continue;
440
            }
441
442
            $fieldMapping = $class->fieldMappings[$fieldName];
443
444
            // Get the proper column name as will appear in the select list
445
            $columnName = $this->quoteStrategy->getColumnName(
446
                $fieldName,
447
                $dqlAliasToClassMap[$dqlAliasForFieldAlias],
448
                $this->em->getConnection()->getDatabasePlatform()
449
            );
450
451
            // Get the SQL table alias for the entity and field
452
            $sqlTableAliasForFieldAlias = $dqlAliasToSqlTableAliasMap[$dqlAliasForFieldAlias];
453
            if (isset($fieldMapping['declared']) && $fieldMapping['declared'] !== $class->name) {
454
                // Field was declared in a parent class, so we need to get the proper SQL table alias
455
                // for the joined parent table.
456
                $otherClassMetadata = $this->em->getClassMetadata($fieldMapping['declared']);
457
                if (!$otherClassMetadata->isMappedSuperclass) {
458
                    $sqlTableAliasForFieldAlias = $this->getSQLTableAlias($otherClassMetadata->getTableName(), $dqlAliasForFieldAlias);
459
                    
0 ignored issues
show
introduced by
Please trim any trailing whitespace
Loading history...
Coding Style introduced by
Blank line found at end of control structure
Loading history...
460
                }
461
            }
462
463
            // Compose search/replace patterns
464
            $searchPatterns[] = sprintf($fieldSearchPattern, $sqlTableAliasForFieldAlias, $columnName);
465
            $replacements[] = $fieldAlias;
466
        }
467
468
        foreach($orderByClause->orderByItems as $orderByItem) {
0 ignored issues
show
Coding Style introduced by
Expected 1 space after FOREACH keyword; 0 found
Loading history...
469
            // Walk order by item to get string representation of it
470
            $orderByItemString = $this->walkOrderByItem($orderByItem);
471
472
            // Replace path expressions in the order by clause with their column alias
473
            $orderByItemString = preg_replace($searchPatterns, $replacements, $orderByItemString);
474
475
            $orderByItems[] = $orderByItemString;
476
        }
477
478
        return $orderByItems;
479
    }
480
481
    /**
482
     * getter for $orderByPathExpressions
483
     *
484
     * @return array
485
     */
486
    public function getOrderByPathExpressions()
0 ignored issues
show
introduced by
Declare public methods first, then protected ones and finally private ones
Loading history...
487
    {
488
        return $this->orderByPathExpressions;
489
    }
490
491
    /**
492
     * @param SelectStatement $AST
493
     *
494
     * @return string
495
     *
496
     * @throws \Doctrine\ORM\OptimisticLockException
497
     * @throws \Doctrine\ORM\Query\QueryException
498
     */
499 17
    private function getInnerSQL(SelectStatement $AST)
500
    {
501
        // Set every select expression as visible(hidden = false) to
502
        // make $AST have scalar mappings properly - this is relevant for referencing selected
503
        // fields from outside the subquery, for example in the ORDER BY segment
504 17
        $hiddens = array();
505
506 17
        foreach ($AST->selectClause->selectExpressions as $idx => $expr) {
507 17
            $hiddens[$idx] = $expr->hiddenAliasResultVariable;
508 17
            $expr->hiddenAliasResultVariable = false;
509
        }
510
511 17
        $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...
512
513
        // Restore hiddens
514 17
        foreach ($AST->selectClause->selectExpressions as $idx => $expr) {
515 17
            $expr->hiddenAliasResultVariable = $hiddens[$idx];
516
        }
517
518 17
        return $innerSql;
519
    }
520
521
    /**
522
     * @param SelectStatement $AST
523
     *
524
     * @return array
525
     */
526 17
    private function getSQLIdentifier(SelectStatement $AST)
527
    {
528
        // Find out the SQL alias of the identifier column of the root entity.
529
        // It may be possible to make this work with multiple root entities but that
530
        // would probably require issuing multiple queries or doing a UNION SELECT.
531
        // So for now, it's not supported.
532
533
        // Get the root entity and alias from the AST fromClause.
534 17
        $from = $AST->fromClause->identificationVariableDeclarations;
535 17
        if (count($from) !== 1) {
536
            throw new \RuntimeException('Cannot count query which selects two FROM components, cannot make distinction');
537
        }
538
539 17
        $fromRoot       = reset($from);
540 17
        $rootAlias      = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable;
541 17
        $rootClass      = $this->queryComponents[$rootAlias]['metadata'];
542 17
        $rootIdentifier = $rootClass->identifier;
543
544
        // For every identifier, find out the SQL alias by combing through the ResultSetMapping
545 17
        $sqlIdentifier = array();
546 17 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...
547 17
            if (isset($rootClass->fieldMappings[$property])) {
548 17
                foreach (array_keys($this->rsm->fieldMappings, $property) as $alias) {
549 17
                    if ($this->rsm->columnOwnerMap[$alias] == $rootAlias) {
550 17
                        $sqlIdentifier[$property] = $alias;
551
                    }
552
                }
553
            }
554
555 17
            if (isset($rootClass->associationMappings[$property])) {
556
                $joinColumn = $rootClass->associationMappings[$property]['joinColumns'][0]['name'];
557
558
                foreach (array_keys($this->rsm->metaMappings, $joinColumn) as $alias) {
559
                    if ($this->rsm->columnOwnerMap[$alias] == $rootAlias) {
560 17
                        $sqlIdentifier[$property] = $alias;
561
                    }
562
                }
563
            }
564
        }
565
566 17
        if (count($sqlIdentifier) === 0) {
567
            throw new \RuntimeException('The Paginator does not support Queries which only yield ScalarResults.');
568
        }
569
570 17 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...
571
            throw new \RuntimeException(sprintf(
572
                'Not all identifier properties can be found in the ResultSetMapping: %s',
573
                implode(', ', array_diff($rootIdentifier, array_keys($sqlIdentifier)))
574
            ));
575
        }
576
577 17
        return $sqlIdentifier;
578
    }
579
580
    /**
581
     * {@inheritdoc}
582
     */
583 17
    public function walkPathExpression($pathExpr)
0 ignored issues
show
introduced by
Declare public methods first, then protected ones and finally private ones
Loading history...
584
    {
585 17
        if (!$this->inSubSelect && !$this->platformSupportsRowNumber() && !in_array($pathExpr, $this->orderByPathExpressions)) {
586
            $this->orderByPathExpressions[] = $pathExpr;
587
        }
588
589 17
        return parent::walkPathExpression($pathExpr);
590
    }
591
592
    /**
593
     * {@inheritdoc}
594
     */
595 1
    public function walkSubSelect($subselect)
596
    {
597 1
        $this->inSubSelect = true;
598
599 1
        $sql = parent::walkSubselect($subselect);
600
601 1
        $this->inSubSelect = false;
602
603 1
        return $sql;
604
    }
605
}
606