Passed
Pull Request — master (#5)
by Alex
03:18
created

QueryFilterManager::createMetadataProxy()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 7
c 1
b 0
f 0
dl 0
loc 9
rs 10
cc 2
nc 2
nop 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 $data
150
     *
151
     * @throws QueryFilterManagerException
152
     */
153
    public function applySort(QueryBuilderInterface $queryBuilder, MetadataInterface $metadata, $data): void
154
    {
155
        if ($data instanceof SortInterface) {
156
            $sort = $data;
157
            $data = [];
158
        } elseif (is_array($data)) {
0 ignored issues
show
introduced by
The condition is_array($data) is always true.
Loading history...
159
            $sortName = $data['name'] ?? Field::class;
160
161
            if (empty($sortName)) {
162
                throw new QueryFilterManagerException(
163
                    sprintf('The required \'name\' configuration option cannot be empty in \'%s\'', static::class)
164
                );
165
            }
166
            $sort = $this->createSort($sortName, $data['options'] ?? []);
167
        } else {
168
            throw new QueryFilterManagerException(
169
                sprintf(
170
                    'The \'data\' argument must be an \'array\' or object of type \'%s\'; \'%s\' provided in \'%s\'',
171
                    SortInterface::class,
172
                    gettype($data),
173
                    static::class
174
                )
175
            );
176
        }
177
178
        try {
179
            $sort->sort($queryBuilder, $metadata, $data);
180
        } catch (SortException $e) {
181
            throw new QueryFilterManagerException(
182
                sprintf('Failed to apply query sorting for entity \'%s\': %s', $metadata->getName(), $e->getMessage()),
183
                $e->getCode(),
184
                $e
185
            );
186
        }
187
    }
188
189
    /**
190
     * Create a new sorting filter matching $name with the provided $options
191
     *
192
     * @param string       $name
193
     * @param array<mixed> $options
194
     *
195
     * @return SortInterface
196
     *
197
     * @throws QueryFilterManagerException
198
     */
199
    private function createSort(string $name, array $options = []): SortInterface
200
    {
201
        try {
202
            return $this->sortFactory->create($this, $name, $options);
203
        } catch (SortFactoryException $e) {
204
            throw new QueryFilterManagerException(
205
                sprintf('Failed to create filter \'%s\': %s', $name, $e->getMessage()),
206
                $e->getCode(),
207
                $e
208
            );
209
        }
210
    }
211
212
    /**
213
     * @param QueryBuilderInterface|DoctrineQueryBuilder $queryBuilder
214
     *
215
     * @return QueryBuilderInterface
216
     *
217
     * @throws QueryFilterManagerException
218
     */
219
    private function getQueryBuilder($queryBuilder): QueryBuilderInterface
220
    {
221
        if ($queryBuilder instanceof DoctrineQueryBuilder) {
222
            $queryBuilder = $this->createQueryBuilderProxy($queryBuilder);
223
        }
224
225
        if (!$queryBuilder instanceof QueryBuilderInterface) {
0 ignored issues
show
introduced by
$queryBuilder is always a sub-type of Arp\DoctrineQueryFilter\QueryBuilderInterface.
Loading history...
226
            throw new QueryFilterManagerException(
227
                sprintf(
228
                    'The \'queryBuilder\' argument must be an object of type \'%s\' or \'%s\'; '
229
                    . '\'%s\' provided in \'%s\'',
230
                    QueryBuilderInterface::class,
231
                    DoctrineQueryBuilder::class,
232
                    get_class($queryBuilder),
233
                    static::class
234
                )
235
            );
236
        }
237
238
        return $queryBuilder;
239
    }
240
241
    /**
242
     * @param DoctrineQueryBuilder $queryBuilder
243
     *
244
     * @return QueryBuilderInterface
245
     */
246
    private function createQueryBuilderProxy(DoctrineQueryBuilder $queryBuilder): QueryBuilderInterface
247
    {
248
        return new QueryBuilder($queryBuilder);
249
    }
250
251
    /**
252
     * @param EntityManager $entityManager
253
     * @param string        $entityName
254
     *
255
     * @return MetadataInterface
256
     *
257
     * @throws QueryFilterManagerException
258
     */
259
    private function createMetadataProxy(EntityManager $entityManager, string $entityName): MetadataInterface
260
    {
261
        try {
262
            return new Metadata($entityManager->getClassMetadata($entityName));
263
        } catch (\Throwable $e) {
264
            throw new QueryFilterManagerException(
265
                sprintf('Failed to fetch entity metadata for class \'%s\': %s', $entityName, $e->getMessage()),
266
                $e->getCode(),
267
                $e
268
            );
269
        }
270
    }
271
}
272