Passed
Push — master ( 089fcb...bc79fb )
by Denis
02:45
created

QueryFilter::replaceSpacesWithPercents()   B

Complexity

Conditions 7
Paths 4

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 7

Importance

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