Failed Conditions
Push — master ( de3989...3625f5 )
by Denis
11:16 queued 10s
created

QueryFilter   A

Complexity

Total Complexity 39

Size/Duplication

Total Lines 293
Duplicated Lines 0 %

Test Coverage

Coverage 90.91%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 103
c 2
b 0
f 0
dl 0
loc 293
ccs 100
cts 110
cp 0.9091
rs 9.28
wmc 39

11 Methods

Rating   Name   Duplication   Size   Complexity  
A getCurrentPage() 0 9 2
A getQueryFilterArgs() 0 19 3
A getFilter() 0 19 2
A getFilterData() 0 15 2
A getSearchBy() 0 15 2
B getFullSearchBy() 0 21 7
A getSimpleSearchBy() 0 21 6
B getSortData() 0 28 7
A replaceSearchByAliases() 0 5 3
A __construct() 0 20 4
A getData() 0 17 1
1
<?php declare(strict_types=1);
2
3
namespace Artprima\QueryFilterBundle\QueryFilter;
4
5
use Artprima\QueryFilterBundle\Exception\InvalidArgumentException;
6
use Artprima\QueryFilterBundle\Exception\UnexpectedValueException;
7
use Artprima\QueryFilterBundle\Query\Filter;
8
use Artprima\QueryFilterBundle\QueryFilter\Config\Alias;
9
use Artprima\QueryFilterBundle\QueryFilter\Config\ConfigInterface;
10
use Artprima\QueryFilterBundle\Response\ResponseInterface;
11
12
/**
13
 * Class QueryFilter
14
 *
15
 * @author Denis Voytyuk <[email protected]>
16
 */
17
class QueryFilter
18
{
19
    /**
20
     * @var string
21
     */
22
    private $responseClassName;
23
24
    /**
25
     * QueryFilter constructor.
26
     * @param string $responseClassName
27
     * @throws \ReflectionException
28
     * @throws InvalidArgumentException
29
     */
30 7
    public function __construct(string $responseClassName)
31
    {
32 7
        $refClass = new \ReflectionClass($responseClassName);
33 7
        if (!$refClass->implementsInterface(ResponseInterface::class)) {
34 1
            throw new InvalidArgumentException(sprintf(
35 1
                'Response class "%s" must implement "%s"',
36
                $responseClassName,
37 1
                ResponseInterface::class
38
            ));
39
        }
40
41 6
        $constructor = $refClass->getConstructor();
42 6
        if ($constructor !== null && $constructor->getNumberOfRequiredParameters() > 0) {
43 1
            throw new InvalidArgumentException(sprintf(
44 1
                'Response class "%s" must have a constructor without required parameters',
45
                $responseClassName
46
            ));
47
        }
48
49 5
        $this->responseClassName = $responseClassName;
50 5
    }
51
52
    /**
53
     * @param ConfigInterface $config
54
     * @return int current page number
55
     */
56 4
    private function getCurrentPage(ConfigInterface $config): int
57
    {
58 4
        $curPage = $config->getRequest()->getPageNum();
59
60 4
        if ($curPage < 1) {
61
            $curPage = 1;
62
        }
63
64 4
        return $curPage;
65
    }
66
67
    /**
68
     * @param ConfigInterface $config
69
     * @return array
70
     */
71 4
    private function getSortData(ConfigInterface $config): array
72
    {
73
        $sort = [
74 4
            'field' => $config->getRequest()->getSortBy(),
75 4
            'type' => $config->getRequest()->getSortDir(),
76
        ];
77
78 4
        if (!isset($sort['field'], $sort['type'])) {
79
            return $config->getSortColsDefault();
80
        }
81
82 4
        $isValidSortColumn = in_array($sort['field'], $config->getSortCols(), true);
83 4
        $isValidSortType = in_array($sort['type'], array('asc', 'desc'), true);
84
85 4
        if ($isValidSortColumn && $isValidSortType) {
86 3
            return array($sort['field'] => $sort['type']);
87
        }
88
89 1
        if ($config->isStrictColumns()) {
90 1
            if (!$isValidSortColumn) {
91 1
                throw new UnexpectedValueException(sprintf('Invalid sort column requested %s', $sort['field']));
92
            }
93
            if (!$isValidSortType) {
94
                throw new UnexpectedValueException(sprintf('Invalid sort type requested %s', $sort['type']));
95
            }
96
        }
97
98
        return $config->getSortColsDefault();
99
    }
100
101
    /**
102
     * @param string $field
103
     * @param array|string $val
104
     * @return Filter
105
     */
106 3
    private function getFilter(string $field, $val): Filter
107
    {
108 3
        $filter = new Filter();
109 3
        if (!is_array($val)) {
110
            $val = [
111 1
                'x' => $val,
112 1
                'type' => 'like',
113
            ];
114
        }
115
116 3
        $filter->setField($field);
117 3
        $filter->setType($val['type'] ?? 'like');
118 3
        $filter->setX($val['x'] ?? null);
119 3
        $filter->setY($val['y'] ?? null);
120 3
        $filter->setExtra($val['extra'] ?? null);
121 3
        $filter->setConnector($val['connector'] ?? 'and');
122 3
        $filter->setHaving((bool)($val['having'] ?? false));
123
124 3
        return $filter;
125
    }
126
127
    /**
128
     * @param array $allowedCols
129
     * @param array|null $search
130
     * @param bool $throw
131
     * @return Filter[]
132
     */
133 3
    private function getSimpleSearchBy(array $allowedCols, ?array $search, bool $throw): array
134
    {
135
        /** @var Filter[] $searchBy */
136 3
        $searchBy = [];
137
138 3
        if ($search === null) {
0 ignored issues
show
introduced by
The condition $search === null is always false.
Loading history...
139 1
            return $searchBy;
140
        }
141
142 2
        foreach ($search as $key => $val) {
143 2
            if (in_array($key, $allowedCols, true) && $val !== null) {
144 1
                $searchBy[] = $this->getFilter($key, $val);
145 1
                continue;
146
            }
147
148 1
            if ($throw) {
149 1
                throw new UnexpectedValueException(sprintf('Invalid filter column requested %s', $key));
150
            }
151
        }
152
153 1
        return $searchBy;
154
    }
155
156
    /**
157
     * @param array $allowedCols
158
     * @param array|null $search
159
     * @param bool $throw
160
     * @return Filter[]
161
     */
162 2
    private function getFullSearchBy(array $allowedCols, ?array $search, bool $throw): array
163
    {
164
        /** @var Filter[] $searchBy */
165 2
        $searchBy = [];
166
167 2
        if ($search === null) {
0 ignored issues
show
introduced by
The condition $search === null is always false.
Loading history...
168
            return $searchBy;
169
        }
170
171 2
        foreach ($search as $key => $data) {
172 2
            if (is_array($data) && isset($data['field']) && in_array($data['field'], $allowedCols, true)) {
173 2
                $searchBy[$key] = $this->getFilter($data['field'], $data);
174 2
                continue;
175
            }
176
177
            if ($throw) {
178
                throw new UnexpectedValueException(sprintf('Invalid filter column requested %s', $key));
179
            }
180
        }
181
182 2
        return $searchBy;
183
    }
184
185
    /**
186
     * @param Filter[] $searchBy
187
     * @param Alias[] $aliases
188
     */
189 4
    private function replaceSearchByAliases(array $searchBy, array $aliases)
190
    {
191 4
        foreach ($searchBy as $filter) {
192 3
            if (array_key_exists($filter->getField(), $aliases)) {
193 1
                $filter->setField($aliases[$filter->getField()]->getExpr());
194
            }
195
        }
196 4
    }
197
198
    /**
199
     * Get searchby data prepared for query builder
200
     *
201
     * If simple, $search must be set to:
202
     * <code>
203
     *     $this->searchData = array(
204
     *         'column_name1' => 'search_value1',
205
     *         'column_name2' => 'search_value2',
206
     *     );
207
     * </code>
208
     * All comparisons will be treated as "like" here.
209
     *
210
     * If not simple, $search must be set to:
211
     * <code>
212
     *     $this->searchData = array(
213
     *         array('field' => 'column_name1', 'type' => 'like', 'val' => 'search_value1'),
214
     *         array('field' => 'column_name2', 'type' => 'eq', 'val' => 'search_value2'),
215
     *     );
216
     * </code>
217
     *
218
     * For both cases GroupConcat columns the result will receive extra $searchBy["column_name1"]["having"] = true
219
     *
220
     * @param ConfigInterface $config
221
     * @return array
222
     */
223 5
    private function getSearchBy(ConfigInterface $config): array
224
    {
225
        // Get basic search by
226 5
        $searchBy = $config->getRequest()->isSimple()
227 3
            ? $this->getSimpleSearchBy($config->getSearchAllowedCols(), $config->getRequest()->getQuery(), $config->isStrictColumns())
228 4
            : $this->getFullSearchBy($config->getSearchAllowedCols(), $config->getRequest()->getQuery(), $config->isStrictColumns());
229
230
        // Set search aliases to more complicated expressions
231 4
        $this->replaceSearchByAliases($searchBy, $config->getSearchByAliases());
232
233
        // Set search extra filters (can be used to display entries for one particular entity,
234
        // or to add some extra conditions/filterings)
235 4
        $searchBy = array_merge($config->getSearchByExtra(), $searchBy);
236
237 4
        return $searchBy;
238
    }
239
240
    /**
241
     * @param ConfigInterface $config
242
     * @return QueryFilterArgs
243
     */
244 5
    private function getQueryFilterArgs(ConfigInterface $config): QueryFilterArgs
245
    {
246 5
        $searchBy = $this->getSearchBy($config);
247 4
        $currentPage = $this->getCurrentPage($config);
248 4
        $sortData = $this->getSortData($config);
249
250 3
        $limit = $config->getRequest()->getLimit();
251 3
        $allowedLimits = $config->getAllowedLimits();
252 3
        if ($limit === -1 || !in_array($limit, $allowedLimits, true)) {
253
            $limit = $config->getDefaultLimit();
254
        }
255
256 3
        $args = (new QueryFilterArgs())
257 3
            ->setSearchBy($searchBy)
258 3
            ->setSortBy($sortData)
259 3
            ->setLimit($limit)
260 3
            ->setOffset(($currentPage - 1) * $limit);
261
262 3
        return $args;
263
    }
264
265
    /**
266
     * @param ConfigInterface $config
267
     * @param QueryFilterArgs $args
268
     * @return QueryResult
269
     */
270 3
    private function getFilterData(ConfigInterface $config, QueryFilterArgs $args): QueryResult
271
    {
272
        // Query database to obtain corresponding entities
273 3
        $repositoryCallback = $config->getRepositoryCallback();
274
275
        // $repositoryCallback can be an array, but since PHP 7.0 it's possible to use it as a function directly
276
        // i.e. without using call_user_func[_array]().
277
        // For the reference: https://trowski.com/2015/06/20/php-callable-paradox/
278 3
        if (!\is_callable($repositoryCallback)) {
279
            throw new InvalidArgumentException('Repository callback is not callable');
280
        }
281
282 3
        $filterData = $repositoryCallback($args);
283
284 3
        return $filterData;
285
    }
286
287
    /**
288
     * Gets filtered data
289
     *
290
     * @param ConfigInterface $config
291
     * @return ResponseInterface
292
     */
293 5
    public function getData(ConfigInterface $config): ResponseInterface
294
    {
295 5
        $args = $this->getQueryFilterArgs($config);
296
297 3
        $startTime = microtime(true);
298 3
        $filterData = $this->getFilterData($config, $args);
299 3
        $duration = microtime(true) - $startTime;
300
301
        /** @var ResponseInterface $response */
302 3
        $response = new $this->responseClassName;
303 3
        $response->setData($filterData->getResult());
304 3
        $response->addMeta('total_records', $filterData->getTotalRows());
305 3
        $response->addMeta('metrics', array(
306 3
            'query_and_transformation' => $duration,
307
        ));
308
309 3
        return $response;
310
    }
311
}
312