QueryFilter::filter()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 9
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 19
rs 9.9666
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Bugloos\QueryFilterBundle\Service;
6
7
use Bugloos\QueryFilterBundle\Enum\ColumnType;
8
use Closure;
9
use Doctrine\ORM\EntityManagerInterface;
10
use Doctrine\ORM\Mapping\ClassMetadata;
11
use Doctrine\ORM\QueryBuilder;
12
use JsonException;
13
use Bugloos\QueryFilterBundle\FilterHandler\Contract\WithRelationInterface;
14
use Bugloos\QueryFilterBundle\FilterHandler\Factory\FilterFactory;
15
use Bugloos\QueryFilterBundle\Traits\QueryFilterTrait;
16
use Psr\Cache\CacheItemInterface;
17
use Psr\Cache\InvalidArgumentException;
18
use Symfony\Contracts\Cache\CacheInterface;
19
20
/**
21
 * @author Milad Ghofrani <[email protected]>
22
 */
23
class QueryFilter
24
{
25
    use QueryFilterTrait;
26
27
    private const DEFAULT_CACHE_TIME = 3600;
28
29
    private const SEPARATOR = '.';
30
31
    private EntityManagerInterface $entityManager;
32
33
    private FilterFactory $filterFactory;
34
35
    private CacheInterface $cache;
36
37
    private string $rootAlias;
38
39
    private string $rootEntity;
40
41
    private ClassMetadata $rootClass;
42
43
    private QueryBuilder $query;
44
45
    private string $cacheKey;
46
47
    private array $filters = [];
48
49
    private array $mapper = [];
50
51
    private array $strategies = [];
52
53
    private array $types = [];
54
55
    private array $constants = [];
56
57
    private ?int $cacheTime = null;
58
59
    private bool $withOr = false;
60
61
    private int $defaultCacheTime;
62
63
    private string $separator;
64
65
    public function __construct(
66
        EntityManagerInterface $entityManager,
67
        CacheInterface $cache,
68
        FilterFactory $filterFactory,
69
        $defaultCacheTime = self::DEFAULT_CACHE_TIME,
70
        $separator = self::SEPARATOR
71
    ) {
72
        $this->entityManager = $entityManager;
73
        $this->cache = $cache;
74
        $this->filterFactory = $filterFactory;
75
        $this->defaultCacheTime = $defaultCacheTime;
76
        $this->separator = $separator;
77
    }
78
79
    public function for(QueryBuilder $query): self
80
    {
81
        $this->initializeRootQueryConfig($query);
82
83
        return $this;
84
    }
85
86
    /**
87
     * @param $filters
88
     *
89
     * @return self
90
     *
91
     * @throws JsonException
92
     */
93
    public function parameters($filters): self
94
    {
95
        if (empty($filters)) {
96
            return $this;
97
        }
98
99
        if (!\is_array($filters)) {
100
            throw new \InvalidArgumentException(
101
                'Filter parameters should be an array type'
102
            );
103
        }
104
105
        // Remove empty value from array
106
        $this->filters = array_filter($filters, static function ($var) {
107
            return null !== $var && '' !== $var;
108
        });
109
110
        // Create cache key by request
111
        $this->createCacheKey($this->filters);
112
113
        return $this;
114
    }
115
116
    /**
117
     * @throws InvalidArgumentException
118
     */
119
    public function filter(): QueryBuilder
120
    {
121
        // Early return if array is empty
122
        if (empty($this->filters)) {
123
            return $this->query;
124
        }
125
        // Calculate and cache fields.
126
        [$filterParameters, $filterWhereClauses, $relationJoins] = $this->cache->get(
127
            $this->cacheKey,
128
            $this->addWhereClauses()
129
        );
130
131
        $this->applyRelationJoinToQuery($relationJoins);
132
133
        $this->applyWhereClausesToQuery($filterWhereClauses);
134
135
        $this->applyParametersToQuery($filterParameters);
136
137
        return $this->query;
138
    }
139
140
    private function applyWhereClausesToQuery($filterWhereClauses): void
141
    {
142
        if (!empty($filterWhereClauses)) {
143
            if ($this->withOr) {
144
                $this->query->andWhere($this->query->expr()->orX()->addMultiple($filterWhereClauses));
145
            } else {
146
                $this->query->andWhere($this->query->expr()->andx()->addMultiple($filterWhereClauses));
147
            }
148
        }
149
    }
150
151
    private function applyParametersToQuery($filterParameters): void
152
    {
153
        foreach ($filterParameters as $parameterName => $parameterValue) {
154
            $this->query->setParameter($parameterName, $parameterValue);
155
        }
156
    }
157
158
    private function applyRelationJoinToQuery($relationJoins): void
159
    {
160
        // Remove exist joined from a list
161
        $filteredJoins = array_diff($relationJoins, $this->query->getAllAliases());
162
163
        // Add a left join to query which does not exist in the query
164
        if (!empty($filteredJoins)) {
165
            foreach ($filteredJoins as $property => $column) {
166
                $this->query->addSelect($column);
167
                $this->query->leftJoin($property, $column);
168
            }
169
        }
170
    }
171
172
    /**
173
     * @param $array
174
     *
175
     * @throws JsonException
176
     */
177
    private function createCacheKey($array): void
178
    {
179
        $this->cacheKey = md5($this->rootEntity.json_encode($array, \JSON_THROW_ON_ERROR));
180
    }
181
182
    /**
183
     * @author Milad Ghofrani <[email protected]>
184
     */
185
    private function addWhereClauses(): Closure
186
    {
187
        return function (CacheItemInterface $item) {
188
            $item->expiresAfter($this->cacheTime ?: $this->defaultCacheTime);
189
190
            $filterParameters = [];
191
            $filterWhereClauses = [];
192
            $relationJoins = [];
193
194
            foreach ($this->filters as $parameter => $value) {
195
                // Check user set a strategy for this $parameter
196
                $strategy = $this->strategies[$parameter] ?? null;
197
198
                // Check user set a type for this $parameter
199
                $type = $this->types[$parameter] ?? null;
200
201
                // Check $parameter exists in mapper
202
                $checkedParameter = (\array_key_exists($parameter, $this->mapper))
203
                    ? $this->mapper[$parameter] : $parameter;
204
205
                $relationsAndFieldName = explode($this->separator, $checkedParameter);
206
207
                $filteringHandler = $this->filterFactory->createFilterHandler($relationsAndFieldName);
208
209
                $filterParameter = $filteringHandler->filterParameter(
210
                    $this->rootAlias,
211
                    $this->rootClass,
212
                    $relationsAndFieldName,
213
                    $type
214
                );
215
216
                if($type !== ColumnType::NULLABLE){
217
                    $filterParameters[$filterParameter] = $filteringHandler->filterValue($value, $strategy);
218
                }
219
220
                $whereClause = $filteringHandler->filterWhereClause(
221
                    $this->rootAlias,
222
                    $relationsAndFieldName,
223
                    $filterParameter,
224
                    $strategy,
225
                    $value
226
                );
227
228
                if(\array_key_exists($parameter, $this->constants) && empty($this->constants[$parameter]) === false){
229
                    $whereClause = "( " . $whereClause;
230
                    $whereClause .= " AND " . $this->constants[$parameter];
231
                    $whereClause .= " )";
232
                }
233
234
                $filterWhereClauses[] = $whereClause;
235
236
                if ($filteringHandler instanceof WithRelationInterface) {
237
                    $relationJoins = $filteringHandler->relationJoin(
238
                        $relationJoins,
239
                        $this->rootAlias,
240
                        $this->rootClass,
241
                        $relationsAndFieldName
242
                    );
243
                }
244
            }
245
246
            return [$filterParameters, $filterWhereClauses, $relationJoins];
247
        };
248
    }
249
250
    private function initializeRootQueryConfig($query): void
251
    {
252
        $rootEntities = $query->getRootEntities();
253
        $rootAliasArray = $query->getRootAliases();
254
255
        if (!isset($rootEntities[0], $rootAliasArray[0])) {
256
            throw new \InvalidArgumentException('Root Alias not defined correctly.');
257
        }
258
259
        $this->query = $query;
260
        $this->rootAlias = $rootAliasArray[0];
261
        $this->rootEntity = $rootEntities[0];
262
        $this->rootClass = $this->entityManager->getClassMetadata($this->rootEntity);
263
    }
264
}
265