Passed
Pull Request — master (#8)
by Alex
17:04
created

QueryFilterManager::createQueryBuilderProxy()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 1
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\EntityManagerInterface;
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
    private FilterFactoryInterface $filterFactory;
29
30
    private SortFactoryInterface $sortFactory;
31
32
    public function __construct(FilterFactoryInterface $filterFactory, SortFactoryInterface $sortFactory)
33
    {
34
        $this->filterFactory = $filterFactory;
35
        $this->sortFactory = $sortFactory;
36
    }
37
38
    /**
39
     * Apply the query filters to the provided query builder instance
40
     *
41
     * @param DoctrineQueryBuilder|QueryBuilderInterface $queryBuilder
42
     * @param string                                     $entityName
43
     * @param array<mixed>                               $criteria
44
     *
45
     * @return DoctrineQueryBuilder
46
     *
47
     * @throws QueryFilterManagerException
48
     */
49
    public function filter($queryBuilder, string $entityName, array $criteria): DoctrineQueryBuilder
50
    {
51
        $queryBuilder = $this->getQueryBuilder($queryBuilder);
52
        $metadata = $this->createMetadataProxy($queryBuilder->getEntityManager(), $entityName);
53
54
        foreach ($criteria['filters'] ?? [] as $filterCriteria) {
55
            $this->applyFilter($queryBuilder, $metadata, $filterCriteria);
56
        }
57
58
        foreach ($criteria['sort'] ?? [] as $sortCriteria) {
59
            $this->applySort($queryBuilder, $metadata, $sortCriteria);
60
        }
61
62
        return $queryBuilder->getWrappedQueryBuilder();
63
    }
64
65
    /**
66
     * @param QueryBuilderInterface        $queryBuilder
67
     * @param MetadataInterface            $metadata
68
     * @param array<mixed>|FilterInterface $data
69
     *
70
     * @throws QueryFilterManagerException
71
     */
72
    private function applyFilter(QueryBuilderInterface $queryBuilder, MetadataInterface $metadata, $data): void
73
    {
74
        if ($data instanceof FilterInterface) {
75
            $filter = $data;
76
            $data = [];
77
        } elseif (is_array($data)) {
0 ignored issues
show
introduced by
The condition is_array($data) is always true.
Loading history...
78
            $filterName = $data['name'] ?? null;
79
80
            if (empty($filterName)) {
81
                throw new QueryFilterManagerException(
82
                    sprintf('The required \'name\' configuration option is missing in \'%s\'', static::class)
83
                );
84
            }
85
86
            $filter = $this->createFilter($filterName, $data['options'] ?? []);
87
        } else {
88
            throw new QueryFilterManagerException(
89
                sprintf(
90
                    'The \'data\' argument must be an \'array\' or object of type \'%s\'; \'%s\' provided in \'%s\'',
91
                    FilterInterface::class,
92
                    gettype($data),
93
                    static::class
94
                )
95
            );
96
        }
97
98
        try {
99
            $filter->filter($queryBuilder, $metadata, $data);
100
        } catch (FilterException $e) {
101
            throw new QueryFilterManagerException(
102
                sprintf('Failed to apply query filter for entity \'%s\': %s', $metadata->getName(), $e->getMessage()),
103
                $e->getCode(),
104
                $e
105
            );
106
        }
107
    }
108
109
    /**
110
     * Create a new filter matching $name with the provided $options
111
     *
112
     * @param string       $name
113
     * @param array<mixed> $options
114
     *
115
     * @return FilterInterface
116
     *
117
     * @throws QueryFilterManagerException
118
     */
119
    private function createFilter(string $name, array $options = []): FilterInterface
120
    {
121
        try {
122
            return $this->filterFactory->create($this, $name, $options);
123
        } catch (FilterFactoryException $e) {
124
            throw new QueryFilterManagerException(
125
                sprintf('Failed to create filter \'%s\': %s', $name, $e->getMessage()),
126
                $e->getCode(),
127
                $e
128
            );
129
        }
130
    }
131
132
    /**
133
     * @param QueryBuilderInterface            $queryBuilder
134
     * @param MetadataInterface                $metadata
135
     * @param array<mixed>|SortInterface|mixed $data
136
     *
137
     * @throws QueryFilterManagerException
138
     */
139
    private function applySort(QueryBuilderInterface $queryBuilder, MetadataInterface $metadata, $data): void
140
    {
141
        if ($data instanceof SortInterface) {
142
            $sort = $data;
143
            $data = [];
144
        } elseif (is_array($data)) {
145
            $sort = $this->createSort(
146
                empty($data['name']) ? Field::class : $data['name'],
147
                $data['options'] ?? []
148
            );
149
        } else {
150
            throw new QueryFilterManagerException(
151
                sprintf(
152
                    'The \'data\' argument must be an \'array\' or object of type \'%s\'; \'%s\' provided in \'%s\'',
153
                    SortInterface::class,
154
                    is_object($data) ? get_class($data) : gettype($data),
155
                    static::class
156
                )
157
            );
158
        }
159
160
        try {
161
            $sort->sort($queryBuilder, $metadata, $data);
162
        } catch (SortException $e) {
163
            throw new QueryFilterManagerException(
164
                sprintf('Failed to apply query sorting for entity \'%s\': %s', $metadata->getName(), $e->getMessage()),
165
                $e->getCode(),
166
                $e
167
            );
168
        }
169
    }
170
171
    /**
172
     * Create a new sorting filter matching $name with the provided $options
173
     *
174
     * @param string       $name
175
     * @param array<mixed> $options
176
     *
177
     * @return SortInterface
178
     *
179
     * @throws QueryFilterManagerException
180
     */
181
    private function createSort(string $name, array $options = []): SortInterface
182
    {
183
        try {
184
            return $this->sortFactory->create($this, $name, $options);
185
        } catch (SortFactoryException $e) {
186
            throw new QueryFilterManagerException(
187
                sprintf('Failed to create filter \'%s\': %s', $name, $e->getMessage()),
188
                $e->getCode(),
189
                $e
190
            );
191
        }
192
    }
193
194
    /**
195
     * @param QueryBuilderInterface|DoctrineQueryBuilder $queryBuilder
196
     *
197
     * @return QueryBuilderInterface
198
     *
199
     * @throws QueryFilterManagerException
200
     */
201
    private function getQueryBuilder($queryBuilder): QueryBuilderInterface
202
    {
203
        if ($queryBuilder instanceof DoctrineQueryBuilder) {
204
            $queryBuilder = new QueryBuilder($queryBuilder);
205
        }
206
207
        if (!$queryBuilder instanceof QueryBuilderInterface) {
0 ignored issues
show
introduced by
$queryBuilder is always a sub-type of Arp\DoctrineQueryFilter\QueryBuilderInterface.
Loading history...
208
            throw new QueryFilterManagerException(
209
                sprintf(
210
                    'The \'queryBuilder\' argument must be an object of type \'%s\' or \'%s\'; '
211
                    . '\'%s\' provided in \'%s\'',
212
                    QueryBuilderInterface::class,
213
                    DoctrineQueryBuilder::class,
214
                    get_class($queryBuilder),
215
                    static::class
216
                )
217
            );
218
        }
219
220
        return $queryBuilder;
221
    }
222
223
    /**
224
     * @param EntityManagerInterface $entityManager
225
     * @param string                 $entityName
226
     *
227
     * @return MetadataInterface
228
     *
229
     * @throws QueryFilterManagerException
230
     */
231
    private function createMetadataProxy(EntityManagerInterface $entityManager, string $entityName): MetadataInterface
232
    {
233
        try {
234
            return new Metadata($entityManager->getClassMetadata($entityName));
235
        } catch (\Throwable $e) {
236
            throw new QueryFilterManagerException(
237
                sprintf('Failed to fetch entity metadata for class \'%s\': %s', $entityName, $e->getMessage()),
238
                $e->getCode(),
239
                $e
240
            );
241
        }
242
    }
243
}
244