Passed
Pull Request — 2.4 (#2639)
by Han Hui
04:05
created

QueryBuilderHelper::getExistingJoin()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 17
rs 10
c 0
b 0
f 0
cc 4
nc 4
nop 4
1
<?php
2
3
/*
4
 * This file is part of the API Platform project.
5
 *
6
 * (c) Kévin Dunglas <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace ApiPlatform\Core\Bridge\Doctrine\Orm\Util;
15
16
use Doctrine\Common\Persistence\ManagerRegistry;
17
use Doctrine\ORM\Query\Expr\Join;
18
use Doctrine\ORM\QueryBuilder;
19
20
/**
21
 * @author Vincent Chalamon <[email protected]>
22
 *
23
 * @internal
24
 */
25
final class QueryBuilderHelper
26
{
27
    private function __construct()
28
    {
29
    }
30
31
    /**
32
     * Adds a join to the QueryBuilder if none exists.
33
     */
34
    public static function addJoinOnce(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $alias, string $association, string $joinType = null, string $conditionType = null, string $condition = null, string $originAlias = null): string
35
    {
36
        $join = self::getExistingJoin($queryBuilder, $alias, $association, $originAlias);
37
38
        if (null !== $join) {
39
            return $join->getAlias();
40
        }
41
42
        $associationAlias = $queryNameGenerator->generateJoinAlias($association);
43
        $query = "$alias.$association";
44
45
        if (Join::LEFT_JOIN === $joinType || QueryChecker::hasLeftJoin($queryBuilder)) {
46
            $queryBuilder->leftJoin($query, $associationAlias, $conditionType, $condition);
47
        } else {
48
            $queryBuilder->innerJoin($query, $associationAlias, $conditionType, $condition);
49
        }
50
51
        return $associationAlias;
52
    }
53
54
    /**
55
     * Gets the entity class name by an alias used in the QueryBuilder.
56
     */
57
    public static function getEntityClassByAlias(string $alias, QueryBuilder $queryBuilder, ManagerRegistry $managerRegistry): string
58
    {
59
        if (!\in_array($alias, $queryBuilder->getAllAliases(), true)) {
60
            throw new \LogicException(sprintf('The alias "%s" does not exist in the QueryBuilder.', $alias));
61
        }
62
63
        $rootAliasMap = self::mapRootAliases($queryBuilder->getRootAliases(), $queryBuilder->getRootEntities());
64
65
        if (isset($rootAliasMap[$alias])) {
66
            return $rootAliasMap[$alias];
67
        }
68
69
        $metadata = null;
70
71
        foreach (self::traverseJoins($alias, $queryBuilder, $managerRegistry) as [$metadata]) {
0 ignored issues
show
Unused Code introduced by
This foreach statement is empty and can be removed.

This check looks for foreach loops that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

Consider removing the loop.

Loading history...
72
            // do nothing, but we will use the $metadata variable
73
        }
74
75
        if (null === $metadata) {
76
            throw new \LogicException(sprintf('The alias "%s" does not exist in the QueryBuilder.', $alias));
77
        }
78
79
        return $metadata->getName();
80
    }
81
82
    /**
83
     * Finds the root alias for an alias used in the QueryBuilder.
84
     */
85
    public static function findRootAlias(string $alias, QueryBuilder $queryBuilder): string
86
    {
87
        if (!\in_array($alias, $queryBuilder->getAllAliases(), true)) {
88
            throw new \LogicException(sprintf('The alias "%s" does not exist in the QueryBuilder.', $alias));
89
        }
90
91
        if (\in_array($alias, $queryBuilder->getRootAliases(), true)) {
92
            return $alias;
93
        }
94
95
        foreach ($queryBuilder->getDQLPart('join') as $rootAlias => $joins) {
96
            foreach ($joins as $join) {
97
                if ($alias === $join->getAlias()) {
98
                    return $rootAlias;
99
                }
100
            }
101
        }
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return string. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
102
    }
103
104
    /**
105
     * Traverses through the joins for an alias used in the QueryBuilder.
106
     *
107
     * @return \Generator<string, array>
108
     */
109
    public static function traverseJoins(string $alias, QueryBuilder $queryBuilder, ManagerRegistry $managerRegistry): \Generator
110
    {
111
        $rootAliasMap = self::mapRootAliases($queryBuilder->getRootAliases(), $queryBuilder->getRootEntities());
112
113
        $joinParts = $queryBuilder->getDQLPart('join');
114
        $rootAlias = self::findRootAlias($alias, $queryBuilder);
115
116
        $joinAliasMap = self::mapJoinAliases($joinParts[$rootAlias]);
117
118
        $aliasMap = array_merge($rootAliasMap, $joinAliasMap);
119
120
        $apexEntityClass = null;
121
        $associationStack = [];
122
        $aliasStack = [];
123
        $currentAlias = $alias;
124
125
        while (null === $apexEntityClass) {
126
            if (!isset($aliasMap[$currentAlias])) {
127
                throw new \LogicException(sprintf('Unknown alias "%s".', $currentAlias));
128
            }
129
130
            if (\is_string($aliasMap[$currentAlias])) {
131
                $aliasStack[] = $currentAlias;
132
                $apexEntityClass = $aliasMap[$currentAlias];
133
            } else {
134
                [$parentAlias, $association] = $aliasMap[$currentAlias];
135
136
                $associationStack[] = $association;
137
                $aliasStack[] = $currentAlias;
138
                $currentAlias = $parentAlias;
139
            }
140
        }
141
142
        $entityClass = $apexEntityClass;
143
        $association = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $association is dead and can be removed.
Loading history...
144
145
        while (null !== ($alias = array_pop($aliasStack))) {
146
            $metadata = $managerRegistry
147
                ->getManagerForClass($entityClass)
148
                ->getClassMetadata($entityClass);
149
150
            $association = array_pop($associationStack);
151
152
            yield $alias => [
153
                $metadata,
154
                $association,
155
            ];
156
157
            if (null !== $association) {
158
                $entityClass = $metadata->getAssociationTargetClass($association);
159
            }
160
        }
161
    }
162
163
    /**
164
     * Gets the existing join from QueryBuilder DQL parts.
165
     */
166
    private static function getExistingJoin(QueryBuilder $queryBuilder, string $alias, string $association, string $originAlias = null): ?Join
167
    {
168
        $parts = $queryBuilder->getDQLPart('join');
169
        $rootAlias = $originAlias ?? $queryBuilder->getRootAliases()[0];
170
171
        if (!isset($parts[$rootAlias])) {
172
            return null;
173
        }
174
175
        foreach ($parts[$rootAlias] as $join) {
176
            /** @var Join $join */
177
            if (sprintf('%s.%s', $alias, $association) === $join->getJoin()) {
178
                return $join;
179
            }
180
        }
181
182
        return null;
183
    }
184
185
    /**
186
     * Maps the root aliases to root entity classes.
187
     *
188
     * @return array<string, string>
189
     */
190
    private static function mapRootAliases(array $rootAliases, array $rootEntities): array
191
    {
192
        $aliasMap = array_combine($rootAliases, $rootEntities);
193
        if (false === $aliasMap) {
194
            throw new \LogicException('Number of root aliases and root entities do not match.');
195
        }
196
197
        return $aliasMap;
198
    }
199
200
    /**
201
     * Maps the join aliases to the parent alias and association, or the entity class.
202
     *
203
     * @return array<string, string[]|string>
204
     */
205
    private static function mapJoinAliases(iterable $joins): array
206
    {
207
        $aliasMap = [];
208
209
        foreach ($joins as $join) {
210
            $alias = $join->getAlias();
211
            $relationship = $join->getJoin();
212
213
            if (false !== strpos($relationship, '.')) {
214
                $aliasMap[$alias] = explode('.', $relationship);
215
            } else {
216
                $aliasMap[$alias] = $relationship;
217
            }
218
        }
219
220
        return $aliasMap;
221
    }
222
}
223