Failed Conditions
Push — master ( 9af1c5...729744 )
by Denis
02:57 queued 13s
created

QueryFilter::getData()   D

Complexity

Conditions 15
Paths 144

Size

Total Lines 78
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 33
CRAP Score 18.5156

Importance

Changes 0
Metric Value
cc 15
eloc 44
nc 144
nop 1
dl 0
loc 78
ccs 33
cts 44
cp 0.75
crap 18.5156
rs 4.7945
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
            if (is_object($searchByExtra) && ($searchByExtra instanceof \Closure)) {
0 ignored issues
show
introduced by
The condition is_object($searchByExtra...xtra instanceof Closure can never be true.
Loading history...
211
                $searchByExtra = $searchByExtra($searchBy);
212
            }
213
            $searchBy = array_merge($searchBy, $searchByExtra);
214
        }
215
216
        // 4. Obtain paging and sorting data
217 2
        $pageData = $this->getPageData($config);
218
219
        // 4.5 Replace spaces by %
220 2
        foreach ($searchBy as &$item) {
221 2
            if (is_array($item) && isset($item['type'], $item['val']) && ($item['type'] === 'like') && preg_match('/[\s\.,]+/', $item['val'])) {
222 2
                $words = preg_split('/[\s\.,]+/', $item['val']);
223 2
                $item['val'] = $words ? implode('%', $words) : $item['val'];
224
            }
225
        }
226 2
        unset($item);
227
228
        // 5. Query database to obtain corresponding entities
229 2
        $repositoryCallback = $config->getRepositoryCallback();
230 2
        if (!\is_callable($repositoryCallback)) {
231
            throw new InvalidArgumentException('Repository callback is not callable');
232
        }
233 2
        $itemsPerPage = $config->getRequest()->getLimit();
234 2
        $args = (new QueryFilterArgs())
235 2
            ->setSearchBy($searchBy)
236 2
            ->setSortBy($pageData['sortdata'])
237 2
            ->setLimit($itemsPerPage)
238 2
            ->setOffset(($pageData['curpage'] - 1) * $itemsPerPage);
239
240
        // $repositoryCallback can be an array, but since PHP 7.0 it's possible to use it as a function directly
241
        // i.e. without using call_user_func[_array]().
242
        // For the reference: https://trowski.com/2015/06/20/php-callable-paradox/
243 2
        $startTime = microtime(true);
244 2
        $filterData = $repositoryCallback($args);
245 2
        $duration = microtime(true) - $startTime;
246 2
        if (!$filterData instanceof QueryResult) {
247
            throw new InvalidArgumentException('Repository callback must return an instance of QueryResult');
248
        }
249
250
        // 6. Prepare the data
251
        /** @var ResponseInterface $response */
252 2
        $response = new $this->responseClassName;
253 2
        $response->setData($filterData->getResult());
254 2
        $response->addMeta('total_records', $filterData->getTotalRows());
255 2
        $response->addMeta('metrics', array(
256 2
            'query_and_transformation' => $duration,
257
        ));
258
259
        // 7. Return the data
260 2
        return $response;
261
    }
262
}
263