Passed
Pull Request — 2.4 (#2639)
by Han Hui
05:45 queued 01:35
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->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