Passed
Push — master ( d72b98...60d9d9 )
by Denis
02:19
created

QueryFilter::getSimpleSearchBy()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 18
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 5.2742

Importance

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