Completed
Push — master ( ea7306...be932e )
by Antoine
22s queued 14s
created

QueryBuilderHelper::mapRootAliases()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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