Passed
Push — master ( de0f46...445da7 )
by Alex
01:11 queued 12s
created

QueryFilterManager   A

Complexity

Total Complexity 29

Size/Duplication

Total Lines 238
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 92
dl 0
loc 238
rs 10
c 1
b 0
f 0
wmc 29

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A createFilter() 0 9 2
A applyFilter() 0 33 5
B filter() 0 18 7
B applySort() 0 28 6
A getQueryBuilder() 0 20 3
A createQueryBuilderProxy() 0 3 1
A createMetadataProxy() 0 9 2
A createSort() 0 9 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Arp\DoctrineQueryFilter;
6
7
use Arp\DoctrineQueryFilter\Exception\QueryFilterManagerException;
8
use Arp\DoctrineQueryFilter\Filter\Exception\FilterException;
9
use Arp\DoctrineQueryFilter\Filter\Exception\FilterFactoryException;
10
use Arp\DoctrineQueryFilter\Filter\FilterFactoryInterface;
11
use Arp\DoctrineQueryFilter\Filter\FilterInterface;
12
use Arp\DoctrineQueryFilter\Metadata\Metadata;
13
use Arp\DoctrineQueryFilter\Metadata\MetadataInterface;
14
use Arp\DoctrineQueryFilter\Sort\Exception\SortException;
15
use Arp\DoctrineQueryFilter\Sort\Exception\SortFactoryException;
16
use Arp\DoctrineQueryFilter\Sort\Field;
17
use Arp\DoctrineQueryFilter\Sort\SortFactoryInterface;
18
use Arp\DoctrineQueryFilter\Sort\SortInterface;
19
use Doctrine\ORM\EntityManager;
20
use Doctrine\ORM\QueryBuilder as DoctrineQueryBuilder;
21
22
/**
23
 * @author  Alex Patterson <[email protected]>
24
 * @package Arp\DoctrineQueryFilter
25
 */
26
class QueryFilterManager implements QueryFilterManagerInterface
27
{
28
    /**
29
     * @var FilterFactoryInterface
30
     */
31
    private FilterFactoryInterface $filterFactory;
32
33
    /**
34
     * @var SortFactoryInterface
35
     */
36
    private SortFactoryInterface $sortFactory;
37
38
    /**
39
     * @param FilterFactoryInterface $filterFactory
40
     * @param SortFactoryInterface   $sortFactory
41
     */
42
    public function __construct(FilterFactoryInterface $filterFactory, SortFactoryInterface $sortFactory)
43
    {
44
        $this->filterFactory = $filterFactory;
45
        $this->sortFactory = $sortFactory;
46
    }
47
48
    /**
49
     * Apply the query filters to the provided query builder instance
50
     *
51
     * @param DoctrineQueryBuilder|QueryBuilderInterface $queryBuilder
52
     * @param string                                     $entityName
53
     * @param array<mixed>                               $criteria
54
     *
55
     * @return DoctrineQueryBuilder
56
     *
57
     * @throws QueryFilterManagerException
58
     */
59
    public function filter($queryBuilder, string $entityName, array $criteria): DoctrineQueryBuilder
60
    {
61
        $queryBuilder = $this->getQueryBuilder($queryBuilder);
62
        $metadata = $this->createMetadataProxy($queryBuilder->getEntityManager(), $entityName);
63
64
        if (!empty($criteria['filters']) && is_array($criteria['filters'])) {
65
            foreach ($criteria['filters'] as $filterCriteria) {
66
                $this->applyFilter($queryBuilder, $metadata, $filterCriteria);
67
            }
68
        }
69
70
        if (!empty($criteria['sort']) && is_array($criteria['sort'])) {
71
            foreach ($criteria['sort'] as $sortCriteria) {
72
                $this->applySort($queryBuilder, $metadata, $sortCriteria);
73
            }
74
        }
75
76
        return $queryBuilder->getWrappedQueryBuilder();
77
    }
78
79
    /**
80
     * @param QueryBuilderInterface        $queryBuilder
81
     * @param MetadataInterface            $metadata
82
     * @param array<mixed>|FilterInterface $data
83
     *
84
     * @throws QueryFilterManagerException
85
     */
86
    private function applyFilter(QueryBuilderInterface $queryBuilder, MetadataInterface $metadata, $data): void
87
    {
88
        if ($data instanceof FilterInterface) {
89
            $filter = $data;
90
            $data = [];
91
        } elseif (is_array($data)) {
0 ignored issues
show
introduced by
The condition is_array($data) is always true.
Loading history...
92
            $filterName = $data['name'] ?? null;
93
94
            if (empty($filterName)) {
95
                throw new QueryFilterManagerException(
96
                    sprintf('The required \'name\' configuration option is missing in \'%s\'', static::class)
97
                );
98
            }
99
100
            $filter = $this->createFilter($filterName, $data['options'] ?? []);
101
        } else {
102
            throw new QueryFilterManagerException(
103
                sprintf(
104
                    'The \'data\' argument must be an \'array\' or object of type \'%s\'; \'%s\' provided in \'%s\'',
105
                    FilterInterface::class,
106
                    gettype($data),
107
                    static::class
108
                )
109
            );
110
        }
111
112
        try {
113
            $filter->filter($queryBuilder, $metadata, $data);
114
        } catch (FilterException $e) {
115
            throw new QueryFilterManagerException(
116
                sprintf('Failed to apply query filter for entity \'%s\': %s', $metadata->getName(), $e->getMessage()),
117
                $e->getCode(),
118
                $e
119
            );
120
        }
121
    }
122
123
    /**
124
     * Create a new filter matching $name with the provided $options
125
     *
126
     * @param string       $name
127
     * @param array<mixed> $options
128
     *
129
     * @return FilterInterface
130
     *
131
     * @throws QueryFilterManagerException
132
     */
133
    private function createFilter(string $name, array $options = []): FilterInterface
134
    {
135
        try {
136
            return $this->filterFactory->create($this, $name, $options);
137
        } catch (FilterFactoryException $e) {
138
            throw new QueryFilterManagerException(
139
                sprintf('Failed to create filter \'%s\': %s', $name, $e->getMessage()),
140
                $e->getCode(),
141
                $e
142
            );
143
        }
144
    }
145
146
    /**
147
     * @param QueryBuilderInterface            $queryBuilder
148
     * @param MetadataInterface                $metadata
149
     * @param array<mixed>|SortInterface|mixed $data
150
     *
151
     * @throws QueryFilterManagerException
152
     */
153
    private function applySort(QueryBuilderInterface $queryBuilder, MetadataInterface $metadata, $data): void
154
    {
155
        if ($data instanceof SortInterface) {
156
            $sort = $data;
157
            $data = [];
158
        } elseif (is_array($data)) {
159
            $sort = $this->createSort(
160
                empty($data['name']) ? Field::class : $data['name'],
161
                $data['options'] ?? []
162
            );
163
        } else {
164
            throw new QueryFilterManagerException(
165
                sprintf(
166
                    'The \'data\' argument must be an \'array\' or object of type \'%s\'; \'%s\' provided in \'%s\'',
167
                    SortInterface::class,
168
                    is_object($data) ? get_class($data) : gettype($data),
169
                    static::class
170
                )
171
            );
172
        }
173
174
        try {
175
            $sort->sort($queryBuilder, $metadata, $data);
176
        } catch (SortException $e) {
177
            throw new QueryFilterManagerException(
178
                sprintf('Failed to apply query sorting for entity \'%s\': %s', $metadata->getName(), $e->getMessage()),
179
                $e->getCode(),
180
                $e
181
            );
182
        }
183
    }
184
185
    /**
186
     * Create a new sorting filter matching $name with the provided $options
187
     *
188
     * @param string       $name
189
     * @param array<mixed> $options
190
     *
191
     * @return SortInterface
192
     *
193
     * @throws QueryFilterManagerException
194
     */
195
    private function createSort(string $name, array $options = []): SortInterface
196
    {
197
        try {
198
            return $this->sortFactory->create($this, $name, $options);
199
        } catch (SortFactoryException $e) {
200
            throw new QueryFilterManagerException(
201
                sprintf('Failed to create filter \'%s\': %s', $name, $e->getMessage()),
202
                $e->getCode(),
203
                $e
204
            );
205
        }
206
    }
207
208
    /**
209
     * @param QueryBuilderInterface|DoctrineQueryBuilder $queryBuilder
210
     *
211
     * @return QueryBuilderInterface
212
     *
213
     * @throws QueryFilterManagerException
214
     */
215
    private function getQueryBuilder($queryBuilder): QueryBuilderInterface
216
    {
217
        if ($queryBuilder instanceof DoctrineQueryBuilder) {
218
            $queryBuilder = $this->createQueryBuilderProxy($queryBuilder);
219
        }
220
221
        if (!$queryBuilder instanceof QueryBuilderInterface) {
0 ignored issues
show
introduced by
$queryBuilder is always a sub-type of Arp\DoctrineQueryFilter\QueryBuilderInterface.
Loading history...
222
            throw new QueryFilterManagerException(
223
                sprintf(
224
                    'The \'queryBuilder\' argument must be an object of type \'%s\' or \'%s\'; '
225
                    . '\'%s\' provided in \'%s\'',
226
                    QueryBuilderInterface::class,
227
                    DoctrineQueryBuilder::class,
228
                    get_class($queryBuilder),
229
                    static::class
230
                )
231
            );
232
        }
233
234
        return $queryBuilder;
235
    }
236
237
    /**
238
     * @param DoctrineQueryBuilder $queryBuilder
239
     *
240
     * @return QueryBuilderInterface
241
     */
242
    private function createQueryBuilderProxy(DoctrineQueryBuilder $queryBuilder): QueryBuilderInterface
243
    {
244
        return new QueryBuilder($queryBuilder);
245
    }
246
247
    /**
248
     * @param EntityManager $entityManager
249
     * @param string        $entityName
250
     *
251
     * @return MetadataInterface
252
     *
253
     * @throws QueryFilterManagerException
254
     */
255
    private function createMetadataProxy(EntityManager $entityManager, string $entityName): MetadataInterface
256
    {
257
        try {
258
            return new Metadata($entityManager->getClassMetadata($entityName));
259
        } catch (\Throwable $e) {
260
            throw new QueryFilterManagerException(
261
                sprintf('Failed to fetch entity metadata for class \'%s\': %s', $entityName, $e->getMessage()),
262
                $e->getCode(),
263
                $e
264
            );
265
        }
266
    }
267
}
268