Completed
Push — master ( 219da4...020dbf )
by Kévin
06:06
created

FilterEagerLoadingExtension::applyToCollection()   D

Complexity

Conditions 9
Paths 5

Size

Total Lines 44
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 44
rs 4.909
c 0
b 0
f 0
cc 9
eloc 27
nc 5
nop 4
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\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
17
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
18
use Doctrine\ORM\EntityManager;
19
use Doctrine\ORM\Mapping\ClassMetadataInfo;
20
use Doctrine\ORM\Query\Expr\Join;
21
use Doctrine\ORM\QueryBuilder;
22
23
/**
24
 * Fixes filters on OneToMany associations
25
 * https://github.com/api-platform/core/issues/944.
26
 */
27
final class FilterEagerLoadingExtension implements QueryCollectionExtensionInterface
28
{
29
    private $resourceMetadataFactory;
30
    private $forceEager;
31
32
    public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, $forceEager = true)
33
    {
34
        $this->resourceMetadataFactory = $resourceMetadataFactory;
35
        $this->forceEager = $forceEager;
36
    }
37
38
    /**
39
     * {@inheritdoc}
40
     */
41
    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
42
    {
43
        $em = $queryBuilder->getEntityManager();
44
        $classMetadata = $em->getClassMetadata($resourceClass);
45
46
        if (!$this->hasFetchEagerAssociation($em, $classMetadata) && (false === $this->forceEager || false === $this->isForceEager($resourceClass, ['collection_operation_name' => $operationName]))) {
47
            return;
48
        }
49
50
        //If no where part, nothing to do
51
        $wherePart = $queryBuilder->getDQLPart('where');
52
53
        if (!$wherePart) {
54
            return;
55
        }
56
57
        $joinParts = $queryBuilder->getDQLPart('join');
58
        $originAlias = 'o';
59
60
        if (!$joinParts || !isset($joinParts[$originAlias])) {
61
            return;
62
        }
63
64
        $queryBuilderClone = clone $queryBuilder;
65
        $queryBuilderClone->resetDQLPart('where');
66
67
        if (!$classMetadata->isIdentifierComposite) {
68
            $replacementAlias = $queryNameGenerator->generateJoinAlias($originAlias);
69
            $in = $this->getQueryBuilderWithNewAliases($queryBuilder, $queryNameGenerator, $originAlias, $replacementAlias);
70
            $in->select($replacementAlias);
71
            $queryBuilderClone->andWhere($queryBuilderClone->expr()->in($originAlias, $in->getDQL()));
72
        } else {
73
            // 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
74
            foreach ($classMetadata->identifier as $identifier) {
75
                $replacementAlias = $queryNameGenerator->generateJoinAlias($originAlias);
76
                $in = $this->getQueryBuilderWithNewAliases($queryBuilder, $queryNameGenerator, $originAlias, $replacementAlias);
77
                $in->select("IDENTITY($replacementAlias.$identifier)");
78
                $queryBuilderClone->andWhere($queryBuilderClone->expr()->in("$originAlias.$identifier", $in->getDQL()));
79
            }
80
        }
81
82
        $queryBuilder->resetDQLPart('where');
83
        $queryBuilder->add('where', $queryBuilderClone->getDQLPart('where'));
84
    }
85
86
    /**
87
     * Returns a clone of the given query builder where everything gets re-aliased.
88
     *
89
     * @param QueryBuilder                $queryBuilder
90
     * @param QueryNameGeneratorInterface $queryBuilder
91
     * @param string                      $originAlias  - the base alias
92
     * @param string                      $replacement  - the replacement for the base alias, will change the from alias
93
     */
94
    private function getQueryBuilderWithNewAliases(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $originAlias = 'o', string $replacement = 'o_2')
95
    {
96
        $queryBuilderClone = clone $queryBuilder;
97
98
        $joinParts = $queryBuilder->getDQLPart('join');
99
        $wherePart = $queryBuilder->getDQLPart('where');
100
101
        //reset parts
102
        $queryBuilderClone->resetDQLPart('join');
103
        $queryBuilderClone->resetDQLPart('where');
104
        $queryBuilderClone->resetDQLPart('orderBy');
105
        $queryBuilderClone->resetDQLPart('groupBy');
106
        $queryBuilderClone->resetDQLPart('having');
107
108
        //Change from alias
109
        $from = $queryBuilderClone->getDQLPart('from')[0];
110
        $queryBuilderClone->resetDQLPart('from');
111
        $queryBuilderClone->from($from->getFrom(), $replacement);
112
113
        $aliases = ["$originAlias."];
114
        $replacements = ["$replacement."];
115
116
        //Change join aliases
117
        foreach ($joinParts[$originAlias] as $joinPart) {
118
            $aliases[] = "{$joinPart->getAlias()}.";
119
            $alias = $queryNameGenerator->generateJoinAlias($joinPart->getAlias());
120
            $replacements[] = "$alias.";
121
            $join = new Join($joinPart->getJoinType(), str_replace($aliases, $replacements, $joinPart->getJoin()), $alias, $joinPart->getConditionType(), $joinPart->getCondition(), $joinPart->getIndexBy());
122
123
            $queryBuilderClone->add('join', [$join], true);
0 ignored issues
show
Documentation introduced by
array($join) is of type array<integer,object<Doc...\\Query\\Expr\\Join>"}>, but the function expects a object<Doctrine\ORM\Query\Expr\Base>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
124
        }
125
126
        $queryBuilderClone->add('where', str_replace($aliases, $replacements, (string) $wherePart));
0 ignored issues
show
Documentation introduced by
str_replace($aliases, $r...s, (string) $wherePart) is of type string, but the function expects a object<Doctrine\ORM\Query\Expr\Base>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
127
128
        return $queryBuilderClone;
129
    }
130
131
    /**
132
     * Does an operation force eager?
133
     *
134
     * @param string $resourceClass
135
     * @param array  $options
136
     *
137
     * @return bool
138
     */
139
    private function isForceEager(string $resourceClass, array $options): bool
140
    {
141
        $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
142
143
        if (isset($options['collection_operation_name'])) {
144
            $forceEager = $resourceMetadata->getCollectionOperationAttribute($options['collection_operation_name'], 'force_eager', null, true);
145
        } else {
146
            $forceEager = $resourceMetadata->getAttribute('force_eager');
147
        }
148
149
        return is_bool($forceEager) ? $forceEager : $this->forceEager;
150
    }
151
152
    private function hasFetchEagerAssociation(EntityManager $em, ClassMetadataInfo $classMetadata, &$checked = [])
153
    {
154
        $checked[] = $classMetadata->name;
155
156
        foreach ($classMetadata->associationMappings as $mapping) {
157
            if (ClassMetadataInfo::FETCH_EAGER === $mapping['fetch']) {
158
                return true;
159
            }
160
161
            $related = $em->getClassMetadata($mapping['targetEntity']);
162
163
            if (in_array($related->name, $checked, true)) {
164
                continue;
165
            }
166
167
            if (true === $this->hasFetchEagerAssociation($em, $related, $checked)) {
168
                return true;
169
            }
170
        }
171
172
        return false;
173
    }
174
}
175