Passed
Pull Request — master (#9)
by Alex
02:41
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
        $fieldAlias = $this->resolveFieldAlias($queryBuilder, $criteria['field']);
50
51
        $joinAlias = $criteria['alias'] ?? null;
52
        if (null === $joinAlias) {
53
            throw new InvalidArgumentException(
54
                sprintf(
55
                    'The required \'field\' criteria value is missing for filter \'%s\'',
56
                    static::class
57
                )
58
            );
59
        }
60
61
        $condition = null;
62
        $conditionType = null;
63
64
        if (isset($criteria['conditions'])) {
65
            $mapping = $this->getAssociationMapping($metadata, $fieldName);
66
            $condition = $this->getCondition(
67
                $queryBuilder,
68
                $mapping['targetEntity'],
69
                $joinAlias,
70
                $criteria['conditions']
71
            );
72
73
            $conditionType = $criteria['condition_type'] ?? null;
74
            if (is_string($conditionType)) {
75
                $conditionType = JoinConditionType::tryFrom($criteria['condition_type']);
76
            }
77
        }
78
79
        $this->applyJoin(
80
            $queryBuilder,
81
            $fieldAlias . '.' . $fieldName,
82
            $joinAlias,
83
            $condition,
84
            $conditionType,
85
            $criteria['index_by'] ?? null
86
        );
87
    }
88
89
    /**
90
     * @throws FilterException
91
     */
92
    private function getCondition(
93
        QueryBuilderInterface $queryBuilder,
94
        string $targetEntity,
95
        string $joinAlias,
96
        mixed $conditions
97
    ): ?string {
98
        if (is_string($conditions)) {
99
            return $conditions;
100
        }
101
102
        if ($conditions instanceof Base) {
103
            return (string)$conditions;
104
        }
105
106
        $condition = null;
107
        if (is_array($conditions)) {
108
            $tempQueryBuilder = $this->filterJoinCriteria(
109
                $queryBuilder->createQueryBuilder(),
110
                $targetEntity,
111
                ['filters' => $this->createJoinFilters($conditions, $joinAlias)]
112
            );
113
            $condition = $this->mergeJoinConditions($queryBuilder, $tempQueryBuilder);
114
        }
115
116
        return isset($condition) ? (string)$condition : null;
117
    }
118
119
    /**
120
     * @param MetadataInterface $metadata
121
     * @param string $fieldName
122
     *
123
     * @return array<mixed>
124
     *
125
     * @throws InvalidArgumentException
126
     */
127
    private function getAssociationMapping(MetadataInterface $metadata, string $fieldName): array
128
    {
129
        try {
130
            return $metadata->getAssociationMapping($fieldName);
131
        } catch (MetadataException $e) {
132
            throw new InvalidArgumentException(
133
                sprintf(
134
                    'Failed to load association field mapping for field \'%s::%s\' in filter \'%s\'',
135
                    $metadata->getName(),
136
                    $fieldName,
137
                    static::class
138
                ),
139
                $e->getCode(),
140
                $e
141
            );
142
        }
143
    }
144
145
    /**
146
     * @param QueryBuilderInterface $qb
147
     * @param string $targetEntity
148
     * @param array<mixed> $criteria
149
     *
150
     * @return QueryBuilderInterface
151
     * @throws FilterException
152
     */
153
    private function filterJoinCriteria(
154
        QueryBuilderInterface $qb,
155
        string $targetEntity,
156
        array $criteria
157
    ): QueryBuilderInterface {
158
        try {
159
            $this->queryFilterManager->filter($qb, $targetEntity, $criteria);
160
        } catch (QueryFilterManagerException $e) {
161
            throw new FilterException(
162
                sprintf(
163
                    'Failed to apply query filter \'%s\' conditions for target entity \'%s\': %s',
164
                    static::class,
165
                    $targetEntity,
166
                    $e->getMessage()
167
                ),
168
                $e->getCode(),
169
                $e
170
            );
171
        }
172
173
        return $qb;
174
    }
175
176
    /**
177
     * @param array<mixed> $conditions
178
     * @param string $alias
179
     * @param array<mixed> $criteria
180
     *
181
     * @return array<mixed>
182
     */
183
    private function createJoinFilters(array $conditions, string $alias, array $criteria = []): array
184
    {
185
        // Use the join alias as the default alias for conditions
186
        foreach ($conditions as $index => $condition) {
187
            if (is_array($condition) && empty($condition['alias'])) {
188
                $conditions[$index]['alias'] = $alias;
189
            }
190
        }
191
192
        return [
193
            [
194
                'name' => AndX::class,
195
                'conditions' => $conditions,
196
                'where' => $criteria['filters']['where'] ?? null,
197
            ],
198
        ];
199
    }
200
201
    private function mergeJoinConditions(QueryBuilderInterface $queryBuilder, QueryBuilderInterface $qb): ?Composite
202
    {
203
        $parts = $qb->getQueryParts();
204
205
        if (!isset($parts['where'])) {
206
            return null;
207
        }
208
209
        if ($parts['where'] instanceof DoctrineAndx) {
210
            $condition = $queryBuilder->expr()->andX();
211
        } elseif ($parts['where'] instanceof DoctrineOrX) {
212
            $condition = $queryBuilder->expr()->orX();
213
        } else {
214
            return null;
215
        }
216
217
        $condition->addMultiple($parts['where']->getParts());
218
        $queryBuilder->mergeParameters($qb);
219
220
        return $condition;
221
    }
222
223
    protected function resolveFieldAlias(QueryBuilderInterface $queryBuilder, string $fieldName): string
224
    {
225
        $parts = explode('.', $fieldName);
226
        return $this->getAlias($queryBuilder, count($parts) > 1 ? $parts[0] : null);
227
    }
228
}
229