Completed
Push — master ( a8e426...cdcd92 )
by Simonas
62:46
created

DynamicAggregate   C

Complexity

Total Complexity 50

Size/Duplication

Total Lines 348
Duplicated Lines 8.05 %

Coupling/Cohesion

Components 1
Dependencies 17

Importance

Changes 9
Bugs 2 Features 4
Metric Value
wmc 50
lcom 1
cbo 17
dl 28
loc 348
rs 5.511
c 9
b 2
f 4

15 Methods

Rating   Name   Duplication   Size   Complexity  
A getNameField() 0 4 1
A getShowZeroChoices() 0 4 1
A getState() 0 13 3
A modifySearch() 0 20 4
C preProcessSearch() 0 43 7
A createViewData() 0 4 1
A isRelated() 0 4 1
A fetchAggregation() 0 14 3
A addSubFilterAggregation() 6 21 3
A getOptionUrlParameters() 10 20 3
C getViewData() 0 41 13
A formInitialUnsortedChoices() 0 20 3
A isChoiceActive() 12 12 4
A createChoice() 0 16 2
A addViewDataItem() 0 9 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like DynamicAggregate often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DynamicAggregate, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of the ONGR package.
5
 *
6
 * (c) NFQ Technologies UAB <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace ONGR\FilterManagerBundle\Filter\Widget\Dynamic;
13
14
use ONGR\ElasticsearchBundle\Result\Aggregation\AggregationValue;
15
use ONGR\ElasticsearchDSL\Aggregation\Bucketing\FilterAggregation;
16
use ONGR\ElasticsearchDSL\Aggregation\Bucketing\NestedAggregation;
17
use ONGR\ElasticsearchDSL\Aggregation\Bucketing\TermsAggregation;
18
use ONGR\ElasticsearchDSL\BuilderInterface;
19
use ONGR\ElasticsearchDSL\Query\Compound\BoolQuery;
20
use ONGR\ElasticsearchDSL\Query\MatchAllQuery;
21
use ONGR\ElasticsearchDSL\Query\Joining\NestedQuery;
22
use ONGR\ElasticsearchDSL\Query\TermLevel\TermQuery;
23
use ONGR\ElasticsearchDSL\Search;
24
use ONGR\ElasticsearchBundle\Result\DocumentIterator;
25
use ONGR\FilterManagerBundle\Filter\FilterState;
26
use ONGR\FilterManagerBundle\Filter\Helper\SortAwareTrait;
27
use ONGR\FilterManagerBundle\Filter\Helper\ViewDataFactoryInterface;
28
use ONGR\FilterManagerBundle\Filter\ViewData\AggregateViewData;
29
use ONGR\FilterManagerBundle\Filter\ViewData;
30
use ONGR\FilterManagerBundle\Filter\Widget\AbstractFilter;
31
use ONGR\FilterManagerBundle\Search\SearchRequest;
32
use Symfony\Component\HttpFoundation\Request;
33
34
/**
35
 * This class provides single terms choice.
36
 */
37
class DynamicAggregate extends AbstractFilter implements ViewDataFactoryInterface
38
{
39
    use SortAwareTrait;
40
41
    /**
42
     * @return string
43
     */
44
    public function getNameField()
45
    {
46
        return $this->getOption('name_field', false);
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a string|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
47
    }
48
49
    /**
50
     * @return bool
51
     */
52
    public function getShowZeroChoices()
53
    {
54
        return $this->getOption('show_zero_choices', false);
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a string|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
55
    }
56
57
    /**
58
     * {@inheritdoc}
59
     */
60
    public function getState(Request $request)
61
    {
62
        $state = new FilterState();
63
        $value = $request->get($this->getRequestField());
64
65
        if (isset($value) && is_array($value)) {
66
            $state->setActive(true);
67
            $state->setValue($value);
68
            $state->setUrlParameters([$this->getRequestField() => $value]);
69
        }
70
71
        return $state;
72
    }
73
74
    /**
75
     * {@inheritdoc}
76
     */
77
    public function modifySearch(Search $search, FilterState $state = null, SearchRequest $request = null)
78
    {
79
        list($path, $field) = explode('>', $this->getDocumentField());
80
81
        if ($state && $state->isActive()) {
82
            $boolQuery = new BoolQuery();
83
            foreach ($state->getValue() as $groupName => $value) {
84
                $innerBoolQuery = new BoolQuery();
85
                $nestedQuery = new NestedQuery($path, $innerBoolQuery);
86
                $innerBoolQuery->add(
87
                    new TermQuery($field, $value)
88
                );
89
                $innerBoolQuery->add(
90
                    new TermQuery($this->getNameField(), $groupName)
91
                );
92
                $boolQuery->add($nestedQuery);
93
            }
94
            $search->addPostFilter($boolQuery);
95
        }
96
    }
97
98
    /**
99
     * {@inheritdoc}
100
     */
101
    public function preProcessSearch(Search $search, Search $relatedSearch, FilterState $state = null)
102
    {
103
        list($path, $field) = explode('>', $this->getDocumentField());
104
        $filter = !empty($filter = $relatedSearch->getPostFilters()) ? $filter : new MatchAllQuery();
105
        $aggregation = new NestedAggregation($state->getName(), $path);
0 ignored issues
show
Bug introduced by
It seems like $state is not always an object, but can also be of type null. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
106
        $nameAggregation = new TermsAggregation('name', $this->getNameField());
107
        $valueAggregation = new TermsAggregation('value', $field);
108
        $filterAggregation = new FilterAggregation($state->getName() . '-filter');
109
        $nameAggregation->addAggregation($valueAggregation);
110
        $aggregation->addAggregation($nameAggregation);
111
        $filterAggregation->setFilter($filter);
112
113
        if ($this->getSortType()) {
114
            $valueAggregation->addParameter('order', [$this->getSortType() => $this->getSortOrder()]);
115
        }
116
117
        if ($state->isActive()) {
118
            foreach ($state->getValue() as $key => $term) {
119
                $terms = $state->getValue();
120
                unset($terms[$key]);
121
122
                $this->addSubFilterAggregation(
123
                    $filterAggregation,
124
                    $aggregation,
125
                    $terms,
126
                    $key
127
                );
128
            }
129
        }
130
131
        $this->addSubFilterAggregation(
132
            $filterAggregation,
133
            $aggregation,
134
            $state->getValue() ? $state->getValue() : [],
135
            'all-selected'
136
        );
137
138
        $search->addAggregation($filterAggregation);
139
140
        if ($this->getShowZeroChoices()) {
141
            $search->addAggregation($aggregation);
142
        }
143
    }
144
145
    /**
146
     * {@inheritdoc}
147
     */
148
    public function createViewData()
149
    {
150
        return new AggregateViewData();
151
    }
152
153
    /**
154
     * {@inheritdoc}
155
     */
156
    public function getViewData(DocumentIterator $result, ViewData $data)
157
    {
158
        $unsortedChoices = [];
159
        $activeNames = $data->getState()->isActive() ? array_keys($data->getState()->getValue()) : [];
160
        $filterAggregations = $this->fetchAggregation($result, $data->getName(), $data->getState()->getValue());
161
162
        if ($this->getShowZeroChoices()) {
163
            $unsortedChoices = $this->formInitialUnsortedChoices($result, $data);
164
        }
165
166
        /** @var AggregationValue $bucket */
167
        foreach ($filterAggregations as $activeName => $filterAggregation) {
168
            foreach ($filterAggregation as $nameAggregation) {
169
                $name = $nameAggregation['key'];
170
171
                if (($name != $activeName && $activeName != 'all-selected') ||
172
                    ($activeName == 'all-selected' && in_array($name, $activeNames))) {
173
                    continue;
174
                }
175
176
                foreach ($nameAggregation['value']['buckets'] as $bucket) {
177
                    $choice = $this->createChoice($data, $name, $activeName, $bucket);
0 ignored issues
show
Compatibility introduced by
$data of type object<ONGR\FilterManagerBundle\Filter\ViewData> is not a sub-type of object<ONGR\FilterManage...Data\AggregateViewData>. It seems like you assume a child class of the class ONGR\FilterManagerBundle\Filter\ViewData to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
178
                    $unsortedChoices[$name][$bucket['key']] = $choice;
179
                }
180
181
                $this->addViewDataItem($data, $name, $unsortedChoices[$name]);
0 ignored issues
show
Compatibility introduced by
$data of type object<ONGR\FilterManagerBundle\Filter\ViewData> is not a sub-type of object<ONGR\FilterManage...Data\AggregateViewData>. It seems like you assume a child class of the class ONGR\FilterManagerBundle\Filter\ViewData to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
182
                unset($unsortedChoices[$name]);
183
            }
184
        }
185
186
        if ($this->getShowZeroChoices() && !empty($unsortedChoices)) {
187
            foreach ($unsortedChoices as $name => $choices) {
188
                $this->addViewDataItem($data, $name, $unsortedChoices[$name]);
0 ignored issues
show
Compatibility introduced by
$data of type object<ONGR\FilterManagerBundle\Filter\ViewData> is not a sub-type of object<ONGR\FilterManage...Data\AggregateViewData>. It seems like you assume a child class of the class ONGR\FilterManagerBundle\Filter\ViewData to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
189
            }
190
        }
191
192
        /** @var AggregateViewData $data */
193
        $data->sortItems();
194
195
        return $data;
196
    }
197
198
    /**
199
     * {@inheritdoc}
200
     */
201
    public function isRelated()
202
    {
203
        return true;
204
    }
205
206
    /**
207
     * Fetches buckets from search results.
208
     *
209
     * @param DocumentIterator $result     Search results.
210
     * @param string           $filterName Filter name.
211
     * @param array            $values     Values from the state object
212
     *
213
     * @return array Buckets.
214
     */
215
    protected function fetchAggregation(DocumentIterator $result, $filterName, $values)
216
    {
217
        $data = [];
218
        $values = empty($values) ? [] : $values;
219
        $aggregation = $result->getAggregation(sprintf('%s-filter', $filterName));
220
221
        foreach ($values as $name => $value) {
222
            $data[$name] = $aggregation->find(sprintf('%s.%s.name', $name, $filterName));
223
        }
224
225
        $data['all-selected'] = $aggregation->find(sprintf('all-selected.%s.name', $filterName));
226
227
        return $data;
228
    }
229
230
    /**
231
     * A method used to add an additional filter to the aggregations
232
     * in preProcessSearch
233
     *
234
     * @param FilterAggregation $filterAggregation
235
     * @param NestedAggregation $deepLevelAggregation
236
     * @param array             $terms Terms of additional filter
237
     * @param string            $aggName
238
     *
239
     * @return BuilderInterface
240
     */
241
    protected function addSubFilterAggregation(
242
        $filterAggregation,
243
        &$deepLevelAggregation,
244
        $terms,
245
        $aggName
246
    ) {
247
        list($path, $field) = explode('>', $this->getDocumentField());
248
        $boolQuery = new BoolQuery();
249
250 View Code Duplication
        foreach ($terms as $groupName => $term) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
251
            $nestedBoolQuery = new BoolQuery();
252
            $nestedBoolQuery->add(new TermQuery($field, $term));
253
            $nestedBoolQuery->add(new TermQuery($this->getNameField(), $groupName));
254
            $boolQuery->add(new NestedQuery($path, $nestedBoolQuery));
255
        }
256
257
        $boolQuery = !empty($boolQuery->getQueries()) ? $boolQuery : new MatchAllQuery();
258
        $innerFilterAggregation = new FilterAggregation($aggName, $boolQuery);
259
        $innerFilterAggregation->addAggregation($deepLevelAggregation);
260
        $filterAggregation->addAggregation($innerFilterAggregation);
261
    }
262
263
    /**
264
     * @param string   $key
265
     * @param string   $name
266
     * @param ViewData $data
267
     * @param bool     $active True when the choice is active
268
     *
269
     * @return array
270
     */
271
    protected function getOptionUrlParameters($key, $name, ViewData $data, $active)
272
    {
273
        $value = $data->getState()->getValue();
274
        $parameters = $data->getResetUrlParameters();
275
276 View Code Duplication
        if (!empty($value)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
277
            if ($active) {
278
                unset($value[array_search($key, $value)]);
279
                $parameters[$this->getRequestField()] = $value;
280
281
                return $parameters;
282
            }
283
284
            $parameters[$this->getRequestField()] = $value;
285
        }
286
287
        $parameters[$this->getRequestField()][$name] = $key;
288
289
        return $parameters;
290
    }
291
292
    /**
293
     * Returns whether choice with the specified key is active.
294
     *
295
     * @param string   $key
296
     * @param ViewData $data
297
     * @param string   $activeName
298
     *
299
     * @return bool
300
     */
301 View Code Duplication
    protected function isChoiceActive($key, ViewData $data, $activeName)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
302
    {
303
        if ($data->getState()->isActive()) {
304
            $value = $data->getState()->getValue();
305
306
            if (isset($value[$activeName]) && $key == $value[$activeName]) {
307
                return true;
308
            }
309
        }
310
311
        return false;
312
    }
313
314
    /**
315
     * Forms $unsortedChoices array with all possible choices.
316
     * 0 is assigned to the document count of the choices.
317
     *
318
     * @param DocumentIterator $result
319
     * @param ViewData         $data
320
     *
321
     * @return array
322
     */
323
    protected function formInitialUnsortedChoices($result, $data)
324
    {
325
        $unsortedChoices = [];
326
        $urlParameters = array_merge(
327
            $data->getResetUrlParameters(),
328
            $data->getState()->getUrlParameters()
329
        );
330
331
        foreach ($result->getAggregation($data->getName())->getAggregation('name') as $nameBucket) {
332
            $groupName = $nameBucket['key'];
333
334
            foreach ($nameBucket->getAggregation('value') as $bucket) {
335
                $bucketArray = ['key' => $bucket['key'], 'doc_count' => 0];
336
                $choice = $this->createChoice($data, $bucket['key'], '', $bucketArray, $urlParameters);
0 ignored issues
show
Compatibility introduced by
$data of type object<ONGR\FilterManagerBundle\Filter\ViewData> is not a sub-type of object<ONGR\FilterManage...Data\AggregateViewData>. It seems like you assume a child class of the class ONGR\FilterManagerBundle\Filter\ViewData to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
Documentation introduced by
$bucketArray is of type array<string,?,{"key":"?","doc_count":"integer"}>, but the function expects a object<ONGR\Elasticsearc...ation\AggregationValue>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
337
                $unsortedChoices[$groupName][$bucket['key']] = $choice;
338
            }
339
        }
340
341
        return $unsortedChoices;
342
    }
343
344
    /**
345
     * @param AggregateViewData $data
346
     * @param string            $name
347
     * @param string            $activeName
348
     * @param AggregationValue  $bucket
349
     * @param array             $urlParameters
350
     *
351
     * @return ViewData\ChoiceAwareViewData
352
     */
353
    protected function createChoice($data, $name, $activeName, $bucket, $urlParameters = null)
354
    {
355
        $active = $this->isChoiceActive($bucket['key'], $data, $activeName);
356
357
        if (empty($urlParameters)) {
358
            $urlParameters = $this->getOptionUrlParameters($bucket['key'], $name, $data, $active);
359
        }
360
361
        $choice = new ViewData\ChoiceAwareViewData();
362
        $choice->setLabel($bucket['key']);
363
        $choice->setCount($bucket['doc_count']);
364
        $choice->setActive($active);
365
        $choice->setUrlParameters($urlParameters);
366
367
        return $choice;
368
    }
369
370
    /**
371
     * @param AggregateViewData $data
372
     * @param string            $name
373
     * @param ViewData\ChoiceAwareViewData[] $choices
374
     */
375
    protected function addViewDataItem($data, $name, $choices)
376
    {
377
        $choiceViewData = new ViewData\ChoicesAwareViewData();
378
        $choiceViewData->setName($name);
379
        $choiceViewData->setChoices($choices);
380
        $choiceViewData->setUrlParameters([]);
381
        $choiceViewData->setResetUrlParameters($data->getResetUrlParameters());
382
        $data->addItem($choiceViewData);
383
    }
384
}
385