Failed Conditions
Push — master ( 97035c...8e1da1 )
by Denis
02:24
created

QueryFilter::replaceSearchByAliases()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 3
nc 3
nop 2
dl 0
loc 5
ccs 4
cts 4
cp 1
crap 3
rs 9.4285
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 $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 1
            if (strpos($key, 'GroupConcat') !== false) {
134 1
                $searchBy[count($searchBy) - 1]->setHaving(true);
135
            }
136
        }
137
138 1
        return $searchBy;
139
    }
140
141
    /**
142
     * @param array $allowedCols
143
     * @param array|null $search
144
     * @return Filter[]
145
     */
146 2
    private function getFullSearchBy(array $allowedCols, ?array $search): array
147
    {
148
        /** @var Filter[] $searchBy */
149 2
        $searchBy = [];
150
151 2
        if ($search === null) {
152
            return $searchBy;
153
        }
154
155 2
        foreach ($search as $key => $data) {
156 2
            if (empty($data) || !is_array($data) || !isset($data['field']) || !in_array($data['field'], $allowedCols, true)) {
157
                continue;
158
            }
159
160 2
            $searchBy[$key] = $this->getFilter($data['field'], $data);
161
162 2
            if (strpos($data['field'], 'GroupConcat') !== false) {
163 2
                $searchBy[$key]->setHaving(true);
164
            }
165
        }
166
167 2
        return $searchBy;
168
    }
169
170
    /**
171
     * @param Filter[] $searchBy
172
     * @param Alias[] $aliases
173
     */
174 3
    private function replaceSearchByAliases(array $searchBy, array $aliases)
175
    {
176 3
        foreach ($searchBy as $filter) {
177 3
            if (array_key_exists($filter->getField(), $aliases)) {
178 3
                $filter->setField($aliases[$filter->getField()]->getExpr());
179
            }
180
        }
181 3
    }
182
183
    /**
184
     * Get searchby data prepared for query builder
185
     *
186
     * If simple, $search must be set to:
187
     * <code>
188
     *     $this->searchData = array(
189
     *         'column_name1' => 'search_value1',
190
     *         'column_name2' => 'search_value2',
191
     *     );
192
     * </code>
193
     * All comparisons will be treated as "like" here.
194
     *
195
     * If not simple, $search must be set to:
196
     * <code>
197
     *     $this->searchData = array(
198
     *         array('field' => 'column_name1', 'type' => 'like', 'val' => 'search_value1'),
199
     *         array('field' => 'column_name2', 'type' => 'eq', 'val' => 'search_value2'),
200
     *     );
201
     * </code>
202
     *
203
     * For both cases GroupConcat columns the result will receive extra $searchBy["column_name1"]["having"] = true
204
     *
205
     * @param ConfigInterface $config
206
     * @return array
207
     */
208 3
    private function getSearchBy(ConfigInterface $config): array
209
    {
210
        // Get basic search by
211 3
        $searchBy = $config->getRequest()->isSimple()
212 1
            ? $this->getSimpleSearchBy($config->getSearchAllowedCols(), $config->getRequest()->getQuery())
213 3
            : $this->getFullSearchBy($config->getSearchAllowedCols(), $config->getRequest()->getQuery());
214
215
        // Set search aliases to more complicated expressions
216 3
        $this->replaceSearchByAliases($searchBy, $config->getSearchByAliases());
217
218
        // Set search extra filters (can be used to display entries for one particular entity,
219
        // or to add some extra conditions/filterings)
220 3
        $searchBy = array_merge($searchBy, $config->getSearchByExtra());
221
222 3
        return $searchBy;
223
    }
224
225
    /**
226
     * @param ConfigInterface $config
227
     * @return QueryFilterArgs
228
     */
229 3
    private function getQueryFilterArgs(ConfigInterface $config): QueryFilterArgs
230
    {
231 3
        $searchBy = $this->getSearchBy($config);
232 3
        $currentPage = $this->getCurrentPage($config);
233 3
        $sortData = $this->getSortData($config);
234
235 3
        $limit = $config->getRequest()->getLimit();
236 3
        $allowedLimits = $config->getAllowedLimits();
237 3
        if ($limit === -1 || (!empty($allowedLimits) && !in_array($limit, $config->getAllowedLimits(), true))) {
238
            $limit = $config->getDefaultLimit();
239
        }
240
241 3
        $args = (new QueryFilterArgs())
242 3
            ->setSearchBy($searchBy)
243 3
            ->setSortBy($sortData)
244 3
            ->setLimit($limit)
245 3
            ->setOffset(($currentPage - 1) * $limit);
246
247 3
        return $args;
248
    }
249
250
    /**
251
     * @param ConfigInterface $config
252
     * @param QueryFilterArgs $args
253
     * @return QueryResult
254
     */
255 3
    private function getFilterData(ConfigInterface $config, QueryFilterArgs $args): QueryResult
256
    {
257
        // Query database to obtain corresponding entities
258 3
        $repositoryCallback = $config->getRepositoryCallback();
259
260
        // $repositoryCallback can be an array, but since PHP 7.0 it's possible to use it as a function directly
261
        // i.e. without using call_user_func[_array]().
262
        // For the reference: https://trowski.com/2015/06/20/php-callable-paradox/
263 3
        if (!\is_callable($repositoryCallback)) {
264
            throw new InvalidArgumentException('Repository callback is not callable');
265
        }
266
267 3
        $filterData = $repositoryCallback($args);
268
269 3
        return $filterData;
270
    }
271
272
    /**
273
     * Gets filtered data
274
     *
275
     * @param ConfigInterface $config
276
     * @return ResponseInterface
277
     */
278 3
    public function getData(ConfigInterface $config): ResponseInterface
279
    {
280 3
        $args = $this->getQueryFilterArgs($config);
281
282 3
        $startTime = microtime(true);
283 3
        $filterData = $this->getFilterData($config, $args);
284 3
        $duration = microtime(true) - $startTime;
285
286
        /** @var ResponseInterface $response */
287 3
        $response = new $this->responseClassName;
288 3
        $response->setData($filterData->getResult());
289 3
        $response->addMeta('total_records', $filterData->getTotalRows());
290 3
        $response->addMeta('metrics', array(
291 3
            'query_and_transformation' => $duration,
292
        ));
293
294 3
        return $response;
295
    }
296
}
297