Failed Conditions
Push — master ( e7b6d0...f491f4 )
by Denis
03:14
created

QueryFilter::getFilter()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 2

Importance

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