• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

api-platform / core / 14008635868

22 Mar 2025 12:39PM UTC coverage: 8.52% (+0.005%) from 8.515%
14008635868

Pull #7042

github

web-flow
Merge fdd88ef56 into 47a6dffbb
Pull Request #7042: Purge parent collections in inheritance cases

4 of 9 new or added lines in 1 file covered. (44.44%)

540 existing lines in 57 files now uncovered.

13394 of 157210 relevant lines covered (8.52%)

22.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.19
/src/Doctrine/Orm/Extension/FilterEagerLoadingExtension.php
1
<?php
2

3
/*
4
 * This file is part of the API Platform project.
5
 *
6
 * (c) Kévin Dunglas <dunglas@gmail.com>
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\Doctrine\Orm\Extension;
15

16
use ApiPlatform\Doctrine\Orm\Util\QueryBuilderHelper;
17
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
18
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
19
use ApiPlatform\Metadata\Operation;
20
use ApiPlatform\Metadata\ResourceClassResolverInterface;
21
use Doctrine\ORM\EntityManagerInterface;
22
use Doctrine\ORM\Mapping\ClassMetadata;
23
use Doctrine\ORM\Query\Expr\Join;
24
use Doctrine\ORM\QueryBuilder;
25

26
/**
27
 * Fixes filters on OneToMany associations
28
 * https://github.com/api-platform/core/issues/944.
29
 */
30
final class FilterEagerLoadingExtension implements QueryCollectionExtensionInterface
31
{
32
    public function __construct(private readonly bool $forceEager = true, private readonly ?ResourceClassResolverInterface $resourceClassResolver = null)
33
    {
34
    }
503✔
35

36
    /**
37
     * {@inheritdoc}
38
     */
39
    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, ?string $resourceClass = null, ?Operation $operation = null, array $context = []): void
40
    {
41
        if (null === $resourceClass) {
503✔
42
            throw new InvalidArgumentException('The "$resourceClass" parameter must not be null');
×
43
        }
44

45
        $em = $queryBuilder->getEntityManager();
503✔
46
        $classMetadata = $em->getClassMetadata($resourceClass);
503✔
47

48
        $forceEager = $operation?->getForceEager() ?? $this->forceEager;
503✔
49

50
        if (!$forceEager && !$this->hasFetchEagerAssociation($em, $classMetadata)) {
503✔
UNCOV
51
            return;
1✔
52
        }
53

54
        // If no where part, nothing to do
55
        $wherePart = $queryBuilder->getDQLPart('where');
502✔
56

57
        if (!$wherePart) {
502✔
58
            return;
271✔
59
        }
60

61
        $joinParts = $queryBuilder->getDQLPart('join');
243✔
62
        $originAlias = $queryBuilder->getRootAliases()[0];
243✔
63

64
        if (!$joinParts || !isset($joinParts[$originAlias])) {
243✔
65
            return;
193✔
66
        }
67

UNCOV
68
        $queryBuilderClone = clone $queryBuilder;
53✔
UNCOV
69
        $queryBuilderClone->resetDQLPart('where');
53✔
UNCOV
70
        $changedWhereClause = false;
53✔
71

UNCOV
72
        if (!$classMetadata->isIdentifierComposite) {
53✔
UNCOV
73
            $replacementAlias = $queryNameGenerator->generateJoinAlias($originAlias);
52✔
UNCOV
74
            $in = $this->getQueryBuilderWithNewAliases($queryBuilder, $queryNameGenerator, $originAlias, $replacementAlias);
52✔
75

UNCOV
76
            if ($classMetadata->containsForeignIdentifier) {
52✔
UNCOV
77
                $identifier = current($classMetadata->getIdentifier());
1✔
UNCOV
78
                $in->select("IDENTITY($replacementAlias.$identifier)");
1✔
UNCOV
79
                $queryBuilderClone->andWhere($queryBuilderClone->expr()->in("$originAlias.$identifier", $in->getDQL()));
1✔
80
            } else {
UNCOV
81
                $in->select($replacementAlias);
51✔
UNCOV
82
                $queryBuilderClone->andWhere($queryBuilderClone->expr()->in($originAlias, $in->getDQL()));
51✔
83
            }
84

UNCOV
85
            $changedWhereClause = true;
52✔
86
        } else {
87
            // 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
UNCOV
88
            foreach ($classMetadata->getIdentifier() as $identifier) {
1✔
UNCOV
89
                if (!$classMetadata->hasAssociation($identifier)) {
1✔
90
                    continue;
×
91
                }
92

UNCOV
93
                $replacementAlias = $queryNameGenerator->generateJoinAlias($originAlias);
1✔
UNCOV
94
                $in = $this->getQueryBuilderWithNewAliases($queryBuilder, $queryNameGenerator, $originAlias, $replacementAlias);
1✔
UNCOV
95
                $in->select("IDENTITY($replacementAlias.$identifier)");
1✔
UNCOV
96
                $queryBuilderClone->andWhere($queryBuilderClone->expr()->in("$originAlias.$identifier", $in->getDQL()));
1✔
UNCOV
97
                $changedWhereClause = true;
1✔
98
            }
99
        }
100

UNCOV
101
        if (false === $changedWhereClause) {
53✔
102
            return;
×
103
        }
104

UNCOV
105
        $queryBuilder->resetDQLPart('where');
53✔
UNCOV
106
        $queryBuilder->add('where', $queryBuilderClone->getDQLPart('where'));
53✔
107
    }
108

109
    /**
110
     * Checks if the class has an associationMapping with FETCH=EAGER.
111
     *
112
     * @param array $checked array cache of tested metadata classes
113
     */
114
    private function hasFetchEagerAssociation(EntityManagerInterface $em, ClassMetadata $classMetadata, array &$checked = []): bool
115
    {
UNCOV
116
        $checked[] = $classMetadata->name;
1✔
117

UNCOV
118
        foreach ($classMetadata->getAssociationMappings() as $mapping) {
1✔
UNCOV
119
            if (ClassMetadata::FETCH_EAGER === $mapping['fetch']) {
1✔
120
                return true;
×
121
            }
122

UNCOV
123
            $related = $em->getClassMetadata($mapping['targetEntity']);
1✔
124

UNCOV
125
            if (\in_array($related->name, $checked, true)) {
1✔
UNCOV
126
                continue;
1✔
127
            }
128

UNCOV
129
            if (true === $this->hasFetchEagerAssociation($em, $related, $checked)) {
1✔
130
                return true;
×
131
            }
132
        }
133

UNCOV
134
        return false;
1✔
135
    }
136

137
    /**
138
     * Returns a clone of the given query builder where everything gets re-aliased.
139
     *
140
     * @param string $originAlias the base alias
141
     * @param string $replacement the replacement for the base alias, will change the from alias
142
     */
143
    private function getQueryBuilderWithNewAliases(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $originAlias = 'o', string $replacement = 'o_2'): QueryBuilder
144
    {
UNCOV
145
        $queryBuilderClone = clone $queryBuilder;
53✔
146

UNCOV
147
        $joinParts = $queryBuilder->getDQLPart('join');
53✔
UNCOV
148
        $wherePart = $queryBuilder->getDQLPart('where');
53✔
149

150
        // reset parts
UNCOV
151
        $queryBuilderClone->resetDQLPart('join');
53✔
UNCOV
152
        $queryBuilderClone->resetDQLPart('where');
53✔
UNCOV
153
        $queryBuilderClone->resetDQLPart('orderBy');
53✔
UNCOV
154
        $queryBuilderClone->resetDQLPart('groupBy');
53✔
UNCOV
155
        $queryBuilderClone->resetDQLPart('having');
53✔
156

157
        // Change from alias
UNCOV
158
        $from = $queryBuilderClone->getDQLPart('from')[0];
53✔
UNCOV
159
        $queryBuilderClone->resetDQLPart('from');
53✔
UNCOV
160
        $queryBuilderClone->from($from->getFrom(), $replacement);
53✔
161

UNCOV
162
        $aliases = ["$originAlias."];
53✔
UNCOV
163
        $replacements = ["$replacement."];
53✔
164

165
        // Change join aliases
UNCOV
166
        foreach ($joinParts[$originAlias] as $joinPart) {
53✔
167
            /** @var Join $joinPart */
UNCOV
168
            $joinString = preg_replace($this->buildReplacePatterns($aliases), $replacements, $joinPart->getJoin());
53✔
UNCOV
169
            $pos = strpos($joinString, '.');
53✔
UNCOV
170
            $joinCondition = (string) $joinPart->getCondition();
53✔
UNCOV
171
            if (false === $pos) {
53✔
UNCOV
172
                if ($joinCondition && $this->resourceClassResolver?->isResourceClass($joinString)) {
3✔
UNCOV
173
                    $newAlias = $queryNameGenerator->generateJoinAlias($joinPart->getAlias());
3✔
UNCOV
174
                    $aliases[] = "{$joinPart->getAlias()}.";
3✔
UNCOV
175
                    $replacements[] = "$newAlias.";
3✔
UNCOV
176
                    $condition = preg_replace($this->buildReplacePatterns($aliases), $replacements, $joinCondition);
3✔
UNCOV
177
                    $join = new Join($joinPart->getJoinType(), $joinPart->getJoin(), $newAlias, $joinPart->getConditionType(), $condition);
3✔
UNCOV
178
                    $queryBuilderClone->add('join', [$replacement => $join], true); // @phpstan-ignore-line
3✔
179
                }
180

UNCOV
181
                continue;
3✔
182
            }
UNCOV
183
            $alias = substr($joinString, 0, $pos);
50✔
UNCOV
184
            $association = substr($joinString, $pos + 1);
50✔
UNCOV
185
            $newAlias = $queryNameGenerator->generateJoinAlias($association);
50✔
UNCOV
186
            $aliases[] = "{$joinPart->getAlias()}.";
50✔
UNCOV
187
            $replacements[] = "$newAlias.";
50✔
UNCOV
188
            $condition = preg_replace($this->buildReplacePatterns($aliases), $replacements, $joinCondition);
50✔
UNCOV
189
            QueryBuilderHelper::addJoinOnce($queryBuilderClone, $queryNameGenerator, $alias, $association, $joinPart->getJoinType(), $joinPart->getConditionType(), $condition, $originAlias, $newAlias);
50✔
190
        }
191

UNCOV
192
        $queryBuilderClone->add('where', preg_replace($this->buildReplacePatterns($aliases), $replacements, (string) $wherePart));
53✔
193

UNCOV
194
        return $queryBuilderClone;
53✔
195
    }
196

197
    private function buildReplacePatterns(array $aliases): array
198
    {
UNCOV
199
        return array_map(static fn (string $alias): string => '/\b'.preg_quote($alias, '/').'/', $aliases);
53✔
200
    }
201
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc