Completed
Push — master ( 2f5812...089fcb )
by Denis
03:11
created

QueryFilter::getData()   D

Complexity

Conditions 16
Paths 160

Size

Total Lines 83
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 35
CRAP Score 18.8084

Importance

Changes 0
Metric Value
cc 16
eloc 45
nc 160
nop 1
dl 0
loc 83
ccs 35
cts 45
cp 0.7778
crap 18.8084
rs 4.5839
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
     * Prepares data to use for sorting and paging
51
     *
52
     * Resulting 'sortdata' array element contains associated array where keys are column names and values are order
53
     * directions (only one array item is supported now, others are ignored). If no sorting provided, default sorting data is used.
54
     *
55
     * @return array Resulting array contains two elements 'curpage' and 'sortdata' as keys with corresponding values
56
     */
57 2
    private function getPageData(ConfigInterface $config): array
58
    {
59
        $sort = [
60 2
            'field' => $config->getRequest()->getSortBy(),
61 2
            'type' => $config->getRequest()->getSortDir(),
62
        ];
63 2
        $curPage = $config->getRequest()->getPageNum();
64
65 2
        if ($curPage < 1) {
66
            $curPage = 1;
67
        }
68
69 2
        $sortdata = $config->getSortColsDefault();
70 2
        if (isset($sort['field'], $sort['type'])) {
71 2
            if (in_array($sort['type'], array('asc', 'desc'), true) && in_array($sort['field'], $config->getSortCols(), true)) {
72 2
                $sortdata = array($sort['field'] => $sort['type']);
73
            }
74
        }
75
76
        return array(
77 2
            'curpage' => $curPage,
78 2
            'sortdata' => $sortdata,
79
        );
80
    }
81
82 1
    private function getSimpleSearchBy(array $allowedCols, ?array $search): array
83
    {
84 1
        $searchBy = [];
85
86 1
        if ($search === null) {
87
            return $searchBy;
88
        }
89
90 1
        foreach ($search as $key => $val) {
91 1
            if (in_array($key, $allowedCols, true) && $val !== null) {
92 1
                $searchBy[$key] = array(
93 1
                    'type' => 'like',
94 1
                    'val' => $val,
95
                );
96 1
                if (strpos($key, 'GroupConcat') !== false) {
97 1
                    $searchBy[$key]['having'] = true;
98
                }
99
            }
100
        }
101
102 1
        return $searchBy;
103
    }
104
105 1
    private function getFullSearchBy(array $allowedCols, ?array $search): array
106
    {
107 1
        $searchBy = [];
108
109 1
        if ($search === null) {
110
            return $searchBy;
111
        }
112
113 1
        foreach ($search as $data) {
114 1
            if (!empty($data) && is_array($data) && isset($data['field']) && in_array($data['field'], $allowedCols, true)) {
115 1
                $field = $data['field'];
116 1
                unset($data['field']);
117 1
                $searchBy[$field] = $data;
118 1
                if (strpos($field, 'GroupConcat') !== false) {
119 1
                    $searchBy[$field]['having'] = true;
120
                }
121
            }
122
        }
123
124 1
        return $searchBy;
125
    }
126
127
    /**
128
     * Get searchby data prepared for query builder
129
     *
130
     * If simple, $search must be set to:
131
     * <code>
132
     *     $this->searchData = array(
133
     *         'column_name1' => 'search_value1',
134
     *         'column_name2' => 'search_value2',
135
     *     );
136
     * </code>
137
     * All comparisons will be treated as "like" here.
138
     *
139
     * If not simple, $search must be set to:
140
     * <code>
141
     *     $this->searchData = array(
142
     *         array('field' => 'column_name1', 'type' => 'like', 'val' => 'search_value1'),
143
     *         array('field' => 'column_name2', 'type' => 'eq', 'val' => 'search_value2'),
144
     *     );
145
     * </code>
146
     *
147
     * For both cases GroupConcat columns the result will receive extra $searchBy["column_name1"]["having"] = true
148
     *
149
     * @param array $allowedCols
150
     * @param array|null $search
151
     * @param bool $simple
152
     * @return array
153
     */
154 2
    private function getSearchBy(array $allowedCols, ?array $search, bool $simple): array
155
    {
156 2
        return $simple ? $this->getSimpleSearchBy($allowedCols, $search) : $this->getFullSearchBy($allowedCols, $search);
157
    }
158
159
    /**
160
     * Gets data for use in twig templates
161
     *
162
     * Consists of 8 steps:
163
     *
164
     * 1. Builds searchBy (see more: {@link QueryFilter::getSearchBy()}, {@link ConfigInterface::getAllowedCols()},
165
     *   {@link ConfigInterface::getSearchData()}, {@link ConfigInterface::isSimpleSearch()})
166
     *
167
     * 2. Modifies searchBy array with the shortcut expanders (see more: {@link ConfigInterface::getShortcutExpanders()})
168
     *
169
     * 3. Adds searchByExtra (if any) to the initial searchBy (see more: {@link ConfigInterface::getSearchByExtra()})
170
     *
171
     * 4. Obtains paging and sorting data (see more: {@link QueryFilter::getSortPageData()}, {@link ConfigInterface::getSortCols()},
172
     *    {@link ConfigInterface::getCurPage()}, {@link ConfigInterface::getSortBy()},
173
     *    {@link ConfigInterface::getSortColsDefault()})
174
     *
175
     * 5. Obtains data to according to the conditions defined in steps 1-4 (see more:
176
     *    {@link ConfigInterface::getRepositoryCallback()}, {@link ConfigInterface::getItemsPerPage})
177
     *
178
     * 6. Prepares and retuns the response
179
     *
180
     * @todo: refactor to smaller parts
181
     *
182
     * @return ResponseInterface
183
     * @throws InvalidArgumentException
184
     */
185 2
    public function getData(ConfigInterface $config): ResponseInterface
186
    {
187
        // 1. Get  search by from query params limited by specified search by keys
188 2
        $searchBy = $this->getSearchBy(
189 2
            $config->getSearchAllowedCols(),
190 2
            $config->getRequest()->getQuery(),
191 2
            $config->getRequest()->isSimple()
192
        );
193
194
        // (optional) 2. replace search by shortcut(s) with more complicated expressions  // **
195 2
        $aliases = $config->getSearchByAliases();
196 2
        foreach ($aliases as $alias => $value) {
197
            if (!empty($searchBy[$alias])) {
198
                if (!empty($value['data'])) {
199
                    $searchBy[$value['name']] = $value['data'];
200
                    $searchBy[$value['name']]['val'] = $searchBy[$alias]['val'];
201
                } else {
202
                    $searchBy[$value['name']] = $searchBy[$alias];
203
                }
204
                unset($searchBy[$alias]);
205
            }
206
        }
207
208
        // (optional) 3. Set search extra filters (can be used to display entries for one particular entity,
209
        //               or to add some extra conditions/filterings)
210 2
        $searchByExtra = $config->getSearchByExtra();
211 2
        if (!empty($searchByExtra)) {
212
            // @todo: possible further extension
213
            // if (is_object($searchByExtra) && ($searchByExtra instanceof \Closure)) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
214
            //     $searchByExtra = $searchByExtra($searchBy);
0 ignored issues
show
Unused Code Comprehensibility introduced by
60% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
215
            // }
216
            $searchBy = array_merge($searchBy, $searchByExtra);
217
        }
218
219
        // 4. Obtain paging and sorting data
220 2
        $pageData = $this->getPageData($config);
221
222
        // 4.5 Replace spaces by %
223 2
        foreach ($searchBy as &$item) {
224 2
            if (is_array($item) && isset($item['type'], $item['val']) && ($item['type'] === 'like') && preg_match('/[\s\.,]+/', $item['val'])) {
225 2
                $words = preg_split('/[\s\.,]+/', $item['val']);
226 2
                $item['val'] = $words ? implode('%', $words) : $item['val'];
227
            }
228
        }
229 2
        unset($item);
230
231
        // 5. Query database to obtain corresponding entities
232 2
        $repositoryCallback = $config->getRepositoryCallback();
233 2
        if (!\is_callable($repositoryCallback)) {
234
            throw new InvalidArgumentException('Repository callback is not callable');
235
        }
236 2
        $limit = $config->getRequest()->getLimit();
237 2
        $allowedLimits = $config->getAllowedLimits();
238 2
        if ($limit === -1 || (!empty($allowedLimits) && !in_array($limit, $config->getAllowedLimits(), true))) {
239
            $limit = $config->getDefaultLimit();
240
        }
241 2
        $args = (new QueryFilterArgs())
242 2
            ->setSearchBy($searchBy)
243 2
            ->setSortBy($pageData['sortdata'])
244 2
            ->setLimit($limit)
245 2
            ->setOffset(($pageData['curpage'] - 1) * $limit);
246
247
        // $repositoryCallback can be an array, but since PHP 7.0 it's possible to use it as a function directly
248
        // i.e. without using call_user_func[_array]().
249
        // For the reference: https://trowski.com/2015/06/20/php-callable-paradox/
250 2
        $startTime = microtime(true);
251 2
        $filterData = $repositoryCallback($args);
252 2
        $duration = microtime(true) - $startTime;
253 2
        if (!$filterData instanceof QueryResult) {
254
            throw new InvalidArgumentException('Repository callback must return an instance of QueryResult');
255
        }
256
257
        // 6. Prepare the data
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
        // 7. Return the data
267 2
        return $response;
268
    }
269
}
270