Passed
Pull Request — master (#9)
by Alex
12:47
created

AbstractJoin::getCondition()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 25
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 12
nc 6
nop 4
dl 0
loc 25
rs 9.5555
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Arp\DoctrineQueryFilter\Filter;
6
7
use Arp\DoctrineQueryFilter\Enum\JoinConditionType;
8
use Arp\DoctrineQueryFilter\Exception\QueryFilterManagerException;
9
use Arp\DoctrineQueryFilter\Filter\Exception\FilterException;
10
use Arp\DoctrineQueryFilter\Filter\Exception\InvalidArgumentException;
11
use Arp\DoctrineQueryFilter\Metadata\Exception\MetadataException;
12
use Arp\DoctrineQueryFilter\Metadata\MetadataInterface;
13
use Arp\DoctrineQueryFilter\QueryBuilderInterface;
14
use Doctrine\ORM\Query\Expr\Andx as DoctrineAndX;
15
use Doctrine\ORM\Query\Expr\Base;
16
use Doctrine\ORM\Query\Expr\Composite;
17
use Doctrine\ORM\Query\Expr\Orx as DoctrineOrX;
18
19
abstract class AbstractJoin extends AbstractFilter
20
{
21
    /**
22
     * @param QueryBuilderInterface $queryBuilder
23
     * @param string $fieldName
24
     * @param string $alias
25
     * @param null|string|Composite|Base $condition
26
     * @param JoinConditionType|null $joinConditionType
27
     * @param string|null $indexBy
28
     */
29
    abstract protected function applyJoin(
30
        QueryBuilderInterface $queryBuilder,
31
        string $fieldName,
32
        string $alias,
33
        mixed $condition = null,
34
        ?JoinConditionType $joinConditionType = null,
35
        ?string $indexBy = null
36
    ): void;
37
38
    /**
39
     * @param QueryBuilderInterface $queryBuilder
40
     * @param MetadataInterface $metadata
41
     * @param array<mixed> $criteria
42
     *
43
     * @throws InvalidArgumentException
44
     * @throws FilterException
45
     */
46
    public function filter(QueryBuilderInterface $queryBuilder, MetadataInterface $metadata, array $criteria): void
47
    {
48
        $fieldName = $this->resolveFieldName($metadata, $criteria);
49
        $queryAlias = $this->getAlias($queryBuilder, $criteria['alias'] ?? null);
50
51
        $condition = null;
52
        if (isset($criteria['conditions'])) {
53
            $mapping = $this->getAssociationMapping($metadata, $fieldName);
54
            $condition = $this->getCondition(
55
                $queryBuilder,
56
                $mapping['targetEntity'],
57
                $queryAlias,
58
                $criteria['conditions']
59
            );
60
        }
61
62
        $conditionType = is_string($criteria['condition_type'])
63
            ? JoinConditionType::tryFrom($criteria['condition_type'])
64
            : $criteria['condition_type'];
65
66
        $this->applyJoin(
67
            $queryBuilder,
68
            $queryBuilder->getRootAlias() . '.' . $fieldName,
69
            $queryAlias,
70
            $condition,
71
            $conditionType,
72
            $criteria['index_by'] ?? null
73
        );
74
    }
75
76
    /**
77
     * @throws FilterException
78
     */
79
    private function getCondition(
80
        QueryBuilderInterface $queryBuilder,
81
        string $targetEntity,
82
        string $queryAlias,
83
        mixed $conditions
84
    ): ?string {
85
        if (is_string($conditions)) {
86
            return $conditions;
87
        }
88
89
        if ($conditions instanceof Base) {
90
            return (string)$conditions;
91
        }
92
93
        $condition = null;
94
        if (is_array($conditions)) {
95
            $tempQueryBuilder = $this->filterJoinCriteria(
96
                $queryBuilder->createQueryBuilder(),
97
                $targetEntity,
98
                ['filters' => $this->createJoinFilters($conditions, $queryAlias)]
99
            );
100
            $condition = $this->mergeJoinConditions($queryBuilder, $tempQueryBuilder);
101
        }
102
103
        return isset($condition) ? (string)$condition : null;
104
    }
105
106
    /**
107
     * @param MetadataInterface $metadata
108
     * @param string $fieldName
109
     *
110
     * @return array<mixed>
111
     *
112
     * @throws InvalidArgumentException
113
     */
114
    private function getAssociationMapping(MetadataInterface $metadata, string $fieldName): array
115
    {
116
        try {
117
            return $metadata->getAssociationMapping($fieldName);
118
        } catch (MetadataException $e) {
119
            throw new InvalidArgumentException(
120
                sprintf(
121
                    'Failed to load association field mapping for field \'%s::%s\' in filter \'%s\'',
122
                    $metadata->getName(),
123
                    $fieldName,
124
                    static::class
125
                ),
126
                $e->getCode(),
127
                $e
128
            );
129
        }
130
    }
131
132
    /**
133
     * @param QueryBuilderInterface $qb
134
     * @param string $targetEntity
135
     * @param array<mixed> $criteria
136
     *
137
     * @return QueryBuilderInterface
138
     * @throws FilterException
139
     */
140
    private function filterJoinCriteria(
141
        QueryBuilderInterface $qb,
142
        string $targetEntity,
143
        array $criteria
144
    ): QueryBuilderInterface {
145
        try {
146
            $this->queryFilterManager->filter($qb, $targetEntity, $criteria);
147
        } catch (QueryFilterManagerException $e) {
148
            throw new FilterException(
149
                sprintf(
150
                    'Failed to apply query filter \'%s\' conditions for target entity \'%s\': %s',
151
                    static::class,
152
                    $targetEntity,
153
                    $e->getMessage()
154
                ),
155
                $e->getCode(),
156
                $e
157
            );
158
        }
159
160
        return $qb;
161
    }
162
163
    /**
164
     * @param array<mixed> $conditions
165
     * @param string $alias
166
     * @param array<mixed> $criteria
167
     *
168
     * @return array<mixed>
169
     */
170
    private function createJoinFilters(array $conditions, string $alias, array $criteria = []): array
171
    {
172
        // Use the join alias as the default alias for conditions
173
        foreach ($conditions as $index => $condition) {
174
            if (is_array($condition) && empty($condition['alias'])) {
175
                $conditions[$index]['alias'] = $alias;
176
            }
177
        }
178
179
        return [
180
            [
181
                'name' => AndX::class,
182
                'conditions' => $conditions,
183
                'where' => $criteria['filters']['where'] ?? null,
184
            ],
185
        ];
186
    }
187
188
    /**
189
     * @param QueryBuilderInterface $queryBuilder
190
     * @param QueryBuilderInterface $qb
191
     *
192
     * @return Composite|null
193
     */
194
    private function mergeJoinConditions(QueryBuilderInterface $queryBuilder, QueryBuilderInterface $qb): ?Composite
195
    {
196
        $parts = $qb->getQueryParts();
197
198
        if (!isset($parts['where'])) {
199
            return null;
200
        }
201
202
        if ($parts['where'] instanceof DoctrineAndx) {
203
            $condition = $queryBuilder->expr()->andX();
204
        } elseif ($parts['where'] instanceof DoctrineOrX) {
205
            $condition = $queryBuilder->expr()->orX();
206
        } else {
207
            return null;
208
        }
209
210
        $condition->addMultiple($parts['where']->getParts());
211
        $queryBuilder->mergeParameters($qb);
212
213
        return $condition;
214
    }
215
}
216