Completed
Push — master ( 1a118c...f720af )
by Kévin
03:46
created

FilterEagerLoadingExtension::applyToCollection()   C

Complexity

Conditions 8
Paths 5

Size

Total Lines 44
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

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