Completed
Push — master ( 13833e...80f050 )
by Denis
03:20
created

QueryFilter::getSearchBy()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

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