Completed
Push — master ( d0bde7...345612 )
by Antoine
26s queued 11s
created

getQueryBuilderWithNewAliases()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 50
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 33
nc 4
nop 4
dl 0
loc 50
rs 8.7697
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\Extension;
15
16
use ApiPlatform\Core\Api\ResourceClassResolver;
17
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\EagerLoadingTrait;
18
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryBuilderHelper;
19
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
20
use ApiPlatform\Core\Exception\InvalidArgumentException;
21
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
22
use Doctrine\ORM\Query\Expr\Join;
23
use Doctrine\ORM\QueryBuilder;
24
25
/**
26
 * Fixes filters on OneToMany associations
27
 * https://github.com/api-platform/core/issues/944.
28
 */
29
final class FilterEagerLoadingExtension implements ContextAwareQueryCollectionExtensionInterface
30
{
31
    use EagerLoadingTrait;
0 ignored issues
show
Bug introduced by
The trait ApiPlatform\Core\Bridge\...\Util\EagerLoadingTrait requires the property $name which is not provided by ApiPlatform\Core\Bridge\...erEagerLoadingExtension.
Loading history...
32
33
    private $resourceClassResolver;
34
35
    public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, bool $forceEager = true, ResourceClassResolver $resourceClassResolver = null)
36
    {
37
        $this->resourceMetadataFactory = $resourceMetadataFactory;
38
        $this->forceEager = $forceEager;
39
        $this->resourceClassResolver = $resourceClassResolver;
40
    }
41
42
    /**
43
     * {@inheritdoc}
44
     */
45
    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass = null, string $operationName = null, array $context = [])
46
    {
47
        if (null === $resourceClass) {
48
            throw new InvalidArgumentException('The "$resourceClass" parameter must not be null');
49
        }
50
51
        $em = $queryBuilder->getEntityManager();
52
        $classMetadata = $em->getClassMetadata($resourceClass);
53
54
        if (!$this->shouldOperationForceEager($resourceClass, ['collection_operation_name' => $operationName]) && !$this->hasFetchEagerAssociation($em, $classMetadata)) {
55
            return;
56
        }
57
58
        //If no where part, nothing to do
59
        $wherePart = $queryBuilder->getDQLPart('where');
60
61
        if (!$wherePart) {
62
            return;
63
        }
64
65
        $joinParts = $queryBuilder->getDQLPart('join');
66
        $originAlias = $queryBuilder->getRootAliases()[0];
67
68
        if (!$joinParts || !isset($joinParts[$originAlias])) {
69
            return;
70
        }
71
72
        $queryBuilderClone = clone $queryBuilder;
73
        $queryBuilderClone->resetDQLPart('where');
74
        $changedWhereClause = false;
75
76
        if (!$classMetadata->isIdentifierComposite) {
77
            $replacementAlias = $queryNameGenerator->generateJoinAlias($originAlias);
78
            $in = $this->getQueryBuilderWithNewAliases($queryBuilder, $queryNameGenerator, $originAlias, $replacementAlias);
79
            $in->select($replacementAlias);
80
            $queryBuilderClone->andWhere($queryBuilderClone->expr()->in($originAlias, $in->getDQL()));
81
            $changedWhereClause = true;
82
        } else {
83
            // Because Doctrine doesn't support WHERE ( foo, bar ) IN () (https://github.com/doctrine/doctrine2/issues/5238), we are building as many subqueries as they are identifiers
84
            foreach ($classMetadata->getIdentifier() as $identifier) {
85
                if (!$classMetadata->hasAssociation($identifier)) {
86
                    continue;
87
                }
88
89
                $replacementAlias = $queryNameGenerator->generateJoinAlias($originAlias);
90
                $in = $this->getQueryBuilderWithNewAliases($queryBuilder, $queryNameGenerator, $originAlias, $replacementAlias);
91
                $in->select("IDENTITY($replacementAlias.$identifier)");
92
                $queryBuilderClone->andWhere($queryBuilderClone->expr()->in("$originAlias.$identifier", $in->getDQL()));
93
                $changedWhereClause = true;
94
            }
95
        }
96
97
        if (false === $changedWhereClause) {
98
            return;
99
        }
100
101
        $queryBuilder->resetDQLPart('where');
102
        $queryBuilder->add('where', $queryBuilderClone->getDQLPart('where'));
103
    }
104
105
    /**
106
     * Returns a clone of the given query builder where everything gets re-aliased.
107
     *
108
     * @param string $originAlias the base alias
109
     * @param string $replacement the replacement for the base alias, will change the from alias
110
     */
111
    private function getQueryBuilderWithNewAliases(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $originAlias = 'o', string $replacement = 'o_2'): QueryBuilder
112
    {
113
        $queryBuilderClone = clone $queryBuilder;
114
115
        $joinParts = $queryBuilder->getDQLPart('join');
116
        $wherePart = $queryBuilder->getDQLPart('where');
117
118
        //reset parts
119
        $queryBuilderClone->resetDQLPart('join');
120
        $queryBuilderClone->resetDQLPart('where');
121
        $queryBuilderClone->resetDQLPart('orderBy');
122
        $queryBuilderClone->resetDQLPart('groupBy');
123
        $queryBuilderClone->resetDQLPart('having');
124
125
        //Change from alias
126
        $from = $queryBuilderClone->getDQLPart('from')[0];
127
        $queryBuilderClone->resetDQLPart('from');
128
        $queryBuilderClone->from($from->getFrom(), $replacement);
129
130
        $aliases = ["$originAlias."];
131
        $replacements = ["$replacement."];
132
133
        //Change join aliases
134
        foreach ($joinParts[$originAlias] as $joinPart) {
135
            /** @var Join $joinPart */
136
            $joinString = str_replace($aliases, $replacements, $joinPart->getJoin());
137
            $pos = strpos($joinString, '.');
138
            if (false === $pos) {
139
                if (null !== $joinPart->getCondition() && null !== $this->resourceClassResolver && $this->resourceClassResolver->isResourceClass($joinString)) {
140
                    $newAlias = $queryNameGenerator->generateJoinAlias($joinPart->getAlias());
141
                    $aliases[] = "{$joinPart->getAlias()}.";
142
                    $replacements[] = "$newAlias.";
143
                    $condition = str_replace($aliases, $replacements, $joinPart->getCondition());
144
                    $join = new Join($joinPart->getJoinType(), $joinPart->getJoin(), $newAlias, $joinPart->getConditionType(), $condition);
145
                    $queryBuilderClone->add('join', [$replacement => $join], true);
146
                }
147
148
                continue;
149
            }
150
            $alias = substr($joinString, 0, $pos);
151
            $association = substr($joinString, $pos + 1);
152
            $condition = str_replace($aliases, $replacements, $joinPart->getCondition());
153
            $newAlias = QueryBuilderHelper::addJoinOnce($queryBuilderClone, $queryNameGenerator, $alias, $association, $joinPart->getJoinType(), $joinPart->getConditionType(), $condition, $originAlias);
154
            $aliases[] = "{$joinPart->getAlias()}.";
155
            $replacements[] = "$newAlias.";
156
        }
157
158
        $queryBuilderClone->add('where', str_replace($aliases, $replacements, (string) $wherePart));
0 ignored issues
show
Bug introduced by
str_replace($aliases, $r...ts, (string)$wherePart) of type string is incompatible with the type array|object expected by parameter $dqlPart of Doctrine\ORM\QueryBuilder::add(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

158
        $queryBuilderClone->add('where', /** @scrutinizer ignore-type */ str_replace($aliases, $replacements, (string) $wherePart));
Loading history...
159
160
        return $queryBuilderClone;
161
    }
162
}
163