Passed
Pull Request — 2.4 (#2639)
by Han Hui
05:45 queued 01:35
created

QueryBuilderHelper::mapJoinAliases()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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