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

api-platform / core / 15133993414

20 May 2025 09:30AM UTC coverage: 26.313% (-1.2%) from 27.493%
15133993414

Pull #7161

github

web-flow
Merge e2c03d45f into 5459ba375
Pull Request #7161: fix(metadata): infer parameter string type from schema

0 of 2 new or added lines in 1 file covered. (0.0%)

11019 existing lines in 363 files now uncovered.

12898 of 49018 relevant lines covered (26.31%)

34.33 hits per line

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

15.12
/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
    {
UNCOV
34
    }
104✔
35

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

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

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

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

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

UNCOV
57
        if (!$wherePart) {
104✔
UNCOV
58
            return;
48✔
59
        }
60

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

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

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

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

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

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

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

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

105
        $queryBuilder->resetDQLPart('where');
×
106
        $queryBuilder->add('where', $queryBuilderClone->getDQLPart('where'));
×
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
    {
116
        $checked[] = $classMetadata->name;
×
117

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

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

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

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

134
        return false;
×
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
    {
145
        $queryBuilderClone = clone $queryBuilder;
×
146

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

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

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

162
        $aliases = ["$originAlias."];
×
163
        $replacements = ["$replacement."];
×
164

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

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

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

194
        return $queryBuilderClone;
×
195
    }
196

197
    private function buildReplacePatterns(array $aliases): array
198
    {
199
        return array_map(static fn (string $alias): string => '/\b'.preg_quote($alias, '/').'/', $aliases);
×
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