Passed
Push — master ( 729744...d41911 )
by Denis
02:41
created

QueryFilter::getData()   C

Complexity

Conditions 13
Paths 96

Size

Total Lines 79
Code Lines 42

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 33
CRAP Score 14.6632

Importance

Changes 0
Metric Value
cc 13
eloc 42
nc 96
nop 1
dl 0
loc 79
ccs 33
cts 42
cp 0.7856
crap 14.6632
rs 5.1136
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
2
3
declare(strict_types=1);
4
5
namespace Artprima\QueryFilterBundle\QueryFilter;
6
7
use Artprima\QueryFilterBundle\Exception\InvalidArgumentException;
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
final 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 4
    public function __construct(string $responseClassName)
30
    {
31 4
        $refClass = new \ReflectionClass($responseClassName);
32 4
        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 3
        $constructor = $refClass->getConstructor();
41 3
        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 2
        $this->responseClassName = $responseClassName;
49 2
    }
50
51
    /**
52
     * Prepares data to use for sorting and paging
53
     *
54
     * Resulting 'sortdata' array element contains associated array where keys are column names and values are order
55
     * directions (only one array item is supported now, others are ignored). If no sorting provided, default sorting data is used.
56
     *
57
     * @return array Resulting array contains two elements 'curpage' and 'sortdata' as keys with corresponding values
58
     */
59 2
    private function getPageData(ConfigInterface $config): array
60
    {
61
        $sort = [
62 2
            'field' => $config->getRequest()->getSortBy(),
63 2
            'type' => $config->getRequest()->getSortDir(),
64
        ];
65 2
        $curPage = $config->getRequest()->getPageNum();
66
67 2
        if ($curPage < 1) {
68
            $curPage = 1;
69
        }
70
71 2
        $sortdata = $config->getSortColsDefault();
72 2
        if (isset($sort['field'], $sort['type'])) {
73 2
            if (in_array($sort['type'], array('asc', 'desc'), true) && in_array($sort['field'], $config->getSortCols(), true)) {
74 2
                $sortdata = array($sort['field'] => $sort['type']);
75
            }
76
        }
77
78
        return array(
79 2
            'curpage' => $curPage,
80 2
            'sortdata' => $sortdata,
81
        );
82
    }
83
84 1
    private function getSimpleSearchBy(array $allowedCols, ?array $search): array
85
    {
86 1
        $searchBy = [];
87
88 1
        if ($search === null) {
89
            return $searchBy;
90
        }
91
92 1
        foreach ($search as $key => $val) {
93 1
            if (in_array($key, $allowedCols, true) && $val !== null) {
94 1
                $searchBy[$key] = array(
95 1
                    'type' => 'like',
96 1
                    'val' => $val,
97
                );
98 1
                if (strpos($key, 'GroupConcat') !== false) {
99 1
                    $searchBy[$key]['having'] = true;
100
                }
101
            }
102
        }
103
104 1
        return $searchBy;
105
    }
106
107 1
    private function getFullSearchBy(array $allowedCols, ?array $search): array
108
    {
109 1
        $searchBy = [];
110
111 1
        foreach ($search as $data) {
112 1
            if (!empty($data) && is_array($data) && isset($data['field']) && in_array($data['field'], $allowedCols, true)) {
113 1
                $field = $data['field'];
114 1
                unset($data['field']);
115 1
                $searchBy[$field] = $data;
116 1
                if (strpos($field, 'GroupConcat') !== false) {
117 1
                    $searchBy[$field]['having'] = true;
118
                }
119
            }
120
        }
121
122 1
        return $searchBy;
123
    }
124
125
    /**
126
     * Get searchby data prepared for query builder
127
     *
128
     * If simple, $search must be set to:
129
     * <code>
130
     *     $this->searchData = array(
131
     *         'column_name1' => 'search_value1',
132
     *         'column_name2' => 'search_value2',
133
     *     );
134
     * </code>
135
     * All comparisons will be treated as "like" here.
136
     *
137
     * If not simple, $search must be set to:
138
     * <code>
139
     *     $this->searchData = array(
140
     *         array('field' => 'column_name1', 'type' => 'like', 'val' => 'search_value1'),
141
     *         array('field' => 'column_name2', 'type' => 'eq', 'val' => 'search_value2'),
142
     *     );
143
     * </code>
144
     *
145
     * For both cases GroupConcat columns the result will receive extra $searchBy["column_name1"]["having"] = true
146
     *
147
     * @param array $allowedCols
148
     * @param array|null $search
149
     * @param bool $simple
150
     * @return array
151
     */
152 2
    private function getSearchBy(array $allowedCols, ?array $search, bool $simple): array
153
    {
154 2
        return $simple ? $this->getSimpleSearchBy($allowedCols, $search) : $this->getFullSearchBy($allowedCols, $search);
155
    }
156
157
    /**
158
     * Gets data for use in twig templates
159
     *
160
     * Consists of 8 steps:
161
     *
162
     * 1. Builds searchBy (see more: {@link QueryFilter::getSearchBy()}, {@link ConfigInterface::getAllowedCols()},
163
     *   {@link ConfigInterface::getSearchData()}, {@link ConfigInterface::isSimpleSearch()})
164
     *
165
     * 2. Modifies searchBy array with the shortcut expanders (see more: {@link ConfigInterface::getShortcutExpanders()})
166
     *
167
     * 3. Adds searchByExtra (if any) to the initial searchBy (see more: {@link ConfigInterface::getSearchByExtra()})
168
     *
169
     * 4. Obtains paging and sorting data (see more: {@link QueryFilter::getSortPageData()}, {@link ConfigInterface::getSortCols()},
170
     *    {@link ConfigInterface::getCurPage()}, {@link ConfigInterface::getSortBy()},
171
     *    {@link ConfigInterface::getSortColsDefault()})
172
     *
173
     * 5. Obtains data to according to the conditions defined in steps 1-4 (see more:
174
     *    {@link ConfigInterface::getRepositoryCallback()}, {@link ConfigInterface::getItemsPerPage})
175
     *
176
     * 6. Prepares and retuns the response
177
     *
178
     * @todo: refactor to smaller parts
179
     *
180
     * @return ResponseInterface
181
     * @throws InvalidArgumentException
182
     */
183 2
    public function getData(ConfigInterface $config): ResponseInterface
184
    {
185
        // 1. Get  search by from query params limited by specified search by keys
186 2
        $searchBy = $this->getSearchBy(
187 2
            $config->getSearchAllowedCols(),
188 2
            $config->getRequest()->getQuery(),
189 2
            $config->getRequest()->isSimple()
190
        );
191
192
        // (optional) 2. replace search by shortcut(s) with more complicated expressions  // **
193 2
        $aliases = $config->getSearchByAliases();
194 2
        foreach ($aliases as $alias => $value) {
195
            if (!empty($searchBy[$alias])) {
196
                if (!empty($value['data'])) {
197
                    $searchBy[$value['name']] = $value['data'];
198
                    $searchBy[$value['name']]['val'] = $searchBy[$alias]['val'];
199
                } else {
200
                    $searchBy[$value['name']] = $searchBy[$alias];
201
                }
202
                unset($searchBy[$alias]);
203
            }
204
        }
205
206
        // (optional) 3. Set search extra filters (can be used to display entries for one particular entity,
207
        //               or to add some extra conditions/filterings)
208 2
        $searchByExtra = $config->getSearchByExtra();
209 2
        if (!empty($searchByExtra)) {
210
            // @todo: possible further extension
211
            // 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...
212
            //     $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...
213
            // }
214
            $searchBy = array_merge($searchBy, $searchByExtra);
215
        }
216
217
        // 4. Obtain paging and sorting data
218 2
        $pageData = $this->getPageData($config);
219
220
        // 4.5 Replace spaces by %
221 2
        foreach ($searchBy as &$item) {
222 2
            if (is_array($item) && isset($item['type'], $item['val']) && ($item['type'] === 'like') && preg_match('/[\s\.,]+/', $item['val'])) {
223 2
                $words = preg_split('/[\s\.,]+/', $item['val']);
224 2
                $item['val'] = $words ? implode('%', $words) : $item['val'];
225
            }
226
        }
227 2
        unset($item);
228
229
        // 5. Query database to obtain corresponding entities
230 2
        $repositoryCallback = $config->getRepositoryCallback();
231 2
        if (!\is_callable($repositoryCallback)) {
232
            throw new InvalidArgumentException('Repository callback is not callable');
233
        }
234 2
        $itemsPerPage = $config->getRequest()->getLimit();
235 2
        $args = (new QueryFilterArgs())
236 2
            ->setSearchBy($searchBy)
237 2
            ->setSortBy($pageData['sortdata'])
238 2
            ->setLimit($itemsPerPage)
239 2
            ->setOffset(($pageData['curpage'] - 1) * $itemsPerPage);
240
241
        // $repositoryCallback can be an array, but since PHP 7.0 it's possible to use it as a function directly
242
        // i.e. without using call_user_func[_array]().
243
        // For the reference: https://trowski.com/2015/06/20/php-callable-paradox/
244 2
        $startTime = microtime(true);
245 2
        $filterData = $repositoryCallback($args);
246 2
        $duration = microtime(true) - $startTime;
247 2
        if (!$filterData instanceof QueryResult) {
248
            throw new InvalidArgumentException('Repository callback must return an instance of QueryResult');
249
        }
250
251
        // 6. Prepare the data
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
        // 7. Return the data
261 2
        return $response;
262
    }
263
}
264