Passed
Pull Request — 2.4 (#2639)
by Han Hui
03:52
created

QueryBuilderHelper::addJoinOnce()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 10
dl 0
loc 18
rs 9.9332
c 0
b 0
f 0
cc 4
nc 3
nop 8

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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