GroupColumnsBehavior   F
last analyzed

Complexity

Total Complexity 70

Size/Duplication

Total Lines 457
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 70
lcom 1
cbo 6
dl 0
loc 457
ccs 0
cts 241
cp 0
rs 2.8
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
A renderItems() 0 21 3
B renderTableBody() 0 36 10
F groupColumns() 0 102 21
F renderTableRow() 0 56 16
A renderExtraRow() 0 22 3
A isGroupEdge() 0 19 5
A getExtraRowTotals() 0 6 2
A getRowValues() 0 15 3
A getColumnDataCellContent() 0 16 3
A getColumnDataCellValue() 0 14 4

How to fix   Complexity   

Complex Class

Complex classes like GroupColumnsBehavior 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 GroupColumnsBehavior, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of the 2amigos/yii2-grid-view-library project.
5
 * (c) 2amigOS! <http://2amigos.us/>
6
 * For the full copyright and license information, please view
7
 * the LICENSE file that was distributed with this source code.
8
 */
9
10
namespace dosamigos\grid\behaviors;
11
12
use Closure;
13
use yii\base\Behavior;
14
use yii\grid\DataColumn;
15
use yii\grid\GridView;
16
use yii\helpers\ArrayHelper;
17
use yii\helpers\Html;
18
19
class GroupColumnsBehavior extends Behavior
20
{
21
    const MERGE_SIMPLE = 'simple';
22
    const MERGE_NESTED = 'nested';
23
    const MERGE_FIRST_ROW = 'firstrow';
24
    const POS_ABOVE = 'above';
25
    const POS_BELOW = 'below';
26
27
    /**
28
     * @var array the column names to merge
29
     */
30
    public $mergeColumns = [];
31
    /**
32
     * @var string the way to merge the columns. Possible values are:
33
     *
34
     * - [[GroupGridView::MERGE_SIMPLE]] Column values are merged independently
35
     * - [[GroupGridView::MERGE_NESTED]] Column values are merged if at least one value of nested columns changes
36
     *   (makes sense when several columns in $mergeColumns option)
37
     * - [[GroupGridView::MERGE_FIRST_ROW]] Column values are merged independently, but value is shown in first row of
38
     *   group and below cells just cleared (instead of `rowspan`)
39
     *
40
     */
41
    public $type = self::MERGE_SIMPLE;
42
    /**
43
     * Need to merge null values in columns? Default - merge on.
44
     * @var boolean
45
     */
46
    public $doNotMergeEmptyValue = false;
47
    /**
48
     * Exclude column for the rule if [[GroupGridView::doNotMergeEmptyValue]] is true.
49
     * @var array
50
     */
51
    public $mergeEmptyColumns = [];
52
    /**
53
     * @var string the CSS class to use for the merged cells
54
     */
55
    public $mergeCellClass = 'group-view-merge-cells';
56
    /**
57
     * @var array the list of columns on which change extra row will be triggered
58
     */
59
    public $extraRowColumns = [];
60
    /**
61
     * @var string|\Closure an anonymous function that returns the value to be displayed for every extra row.
62
     *                      The signature of this function is `function ($model, $key, $index, $totals)`.
63
     *                      If this is not set, `$model[$attribute]` will be used to obtain the value.
64
     *
65
     * You may also set this property to a string representing the attribute name to be displayed in this column.
66
     * This can be used when the attribute to be displayed is different from the [[attribute]] that is used for
67
     * sorting and filtering.
68
     */
69
    public $extraRowValue;
70
    /**
71
     * @var string the position of the extra row. Possible values are [[GroupGridView::POS_ABOVE]] or
72
     *             [[GroupGridView::POS_BELOW]]
73
     */
74
    public $extraRowPosition = self::POS_ABOVE;
75
    /**
76
     * @var \Closure an anonymous function that returns a calculated value of the totals. Its signature is
77
     *               `function($model, $index, $totals)`
78
     */
79
    public $extraRowTotalsValue;
80
    /**
81
     * @var string the CSS class to add on the extra row TD tag
82
     */
83
    public $extraRowClass = 'group-view-extra-row';
84
    /**
85
     * @var array stores the groups
86
     */
87
    private $_groups = [];
88
89
    /**
90
     * Renders the data models for the grid view.
91
     */
92
    public function renderItems()
93
    {
94
        /** @var GridView $grid */
95
        $grid = $this->owner;
96
        $caption = $grid->renderCaption();
97
        $columnGroup = $grid->renderColumnGroup();
98
        $tableHeader = $grid->showHeader ? $grid->renderTableHeader() : false;
99
        $tableBody = $this->renderTableBody();
100
        $tableFooter = $grid->showFooter ? $grid->renderTableFooter() : false;
101
        $content = array_filter(
102
            [
103
                $caption,
104
                $columnGroup,
105
                $tableHeader,
106
                $tableFooter,
107
                $tableBody,
108
            ]
109
        );
110
111
        return Html::tag('table', implode("\n", $content), $grid->tableOptions);
112
    }
113
114
    /**
115
     * Renders the table body.
116
     * @return string the rendering result.
117
     */
118
    public function renderTableBody()
119
    {
120
        if (!empty($this->mergeColumns) || !empty($this->extraRowColumns)) {
121
            $this->groupColumns();
122
        }
123
        /** @var GridView $grid */
124
        $grid = $this->owner;
125
        $models = array_values($grid->dataProvider->getModels());
126
        $keys = $grid->dataProvider->getKeys();
127
        $rows = [];
128
        foreach ($models as $index => $model) {
129
            $key = $keys[$index];
130
            if ($grid->beforeRow !== null) {
131
                $row = call_user_func($grid->beforeRow, $model, $key, $index, $grid);
132
                if (!empty($row)) {
133
                    $rows[] = $row;
134
                }
135
            }
136
137
            $rows[] = $this->renderTableRow($model, $key, $index);
138
139
            if ($grid->afterRow !== null) {
140
                $row = call_user_func($grid->afterRow, $model, $key, $index, $grid);
141
                if (!empty($row)) {
142
                    $rows[] = $row;
143
                }
144
            }
145
        }
146
147
        if (empty($rows) && $grid->emptyText !== false) {
148
            $colspan = count($grid->columns);
149
150
            return "<tbody>\n<tr><td colspan=\"$colspan\">" . $grid->renderEmpty() . "</td></tr>\n</tbody>";
151
        }
152
        return "<tbody>\n" . implode("\n", $rows) . "\n</tbody>";
153
    }
154
155
    /**
156
     * Finds and stores changes of grouped columns
157
     */
158
    public function groupColumns()
159
    {
160
        /** @var GridView $grid */
161
        $grid = $this->owner;
162
        $models = $grid->dataProvider->getModels();
163
164
        if (count($models) == 0) {
165
            return;
166
        }
167
168
        $this->mergeColumns = (array)$this->mergeColumns;
169
        $this->extraRowColumns = (array)$this->extraRowColumns;
170
171
        // store columns for group.
172
        $columns = array_unique(ArrayHelper::merge($this->mergeColumns, $this->extraRowColumns));
173
        foreach ($columns as $key => $name) {
174
            foreach ($grid->columns as $column) {
175
                if (property_exists($column, 'attribute') && ArrayHelper::getValue($column, 'attribute') == $name) {
176
                    $columns[$key] = $column;
177
                } elseif (in_array($name, $this->extraRowColumns)) {
178
                    $columns[$key] = new DataColumn(['attribute' => $name]);
179
                }
180
            }
181
        }
182
183
        $groups = [];
184
185
        // values for the first row
186
        $values = $this->getRowValues($columns, $models[0]);
187
188
        foreach ($values as $key => $value) {
189
            $groups[$key][] = [
190
                'value' => $value,
191
                'column' => $key,
192
                'start' => 0
193
            ];
194
        }
195
196
        // calculate totals for the first row
197
        $totals = [];
198
199
        // iterate through models
200
        foreach ($models as $index => $model) {
201
            // save row values in array
202
            $rowValues = $this->getRowValues($columns, $model, $index);
203
204
            // define if any change occurred. Need this extra foreach for correctly process extraRows
205
            $changedColumns = [];
206
            foreach ($rowValues as $name => $value) {
207
                $previous = end($groups[$name]);
208
                if ($this->doNotMergeEmptyValue && empty($value) && !in_array($name, $this->mergeEmptyColumns, true)) {
209
                    $changedColumns[] = $name;
210
                } elseif ($value != $previous['value']) {
211
                    $changedColumns[] = $name;
212
                }
213
            }
214
215
            // if this flag is true we will write change for all grouped columns. Its required when a change of any
216
            // column from extraRowColumns occurs
217
            $extraRowColumnChanged = (count(array_intersect($changedColumns, $this->extraRowColumns)) > 0);
218
219
            // this changeOccured related to foreach below. It is required only for mergeType == self::MERGE_NESTED,
220
            // to write change for all nested columns when change of previous column occurred
221
            $changeOccurred = false;
222
            foreach ($rowValues as $columnName => $columnValue) {
223
                // value changed
224
                $valueChanged = in_array($columnName, $changedColumns);
225
                //change already occured in this loop and mergeType set to MERGE_NESTED
226
                $saveChange = $valueChanged || ($changeOccurred && $this->type == self::MERGE_NESTED);
227
228
                if ($extraRowColumnChanged || $saveChange) {
229
                    $changeOccurred = true;
230
                    $lastIndex = count($groups[$columnName]) - 1;
231
232
                    //finalize prev group
233
                    $groups[$columnName][$lastIndex]['end'] = $index - 1;
234
                    $groups[$columnName][$lastIndex]['totals'] = $totals;
235
236
                    //begin new group
237
                    $groups[$columnName][] = [
238
                        'start' => $index,
239
                        'column' => $columnName,
240
                        'value' => $columnValue,
241
                    ];
242
                }
243
            }
244
245
            if ($extraRowColumnChanged) {
246
                $totals = [];
247
            }
248
            $totals = $this->getExtraRowTotals($model, $index, $totals);
249
        }
250
251
        // finalize group for last row
252
        foreach ($groups as $name => $value) {
253
            $lastIndex = count($groups[$name]) - 1;
254
            $groups[$name][$lastIndex]['end'] = count($models) - 1;
255
            $groups[$name][$lastIndex]['totals'] = $totals;
256
        }
257
258
        $this->_groups = $groups;
259
    }
260
261
    /**
262
     * @inheritdoc
263
     */
264
    public function renderTableRow($model, $key, $index)
265
    {
266
        $rows = [];
267
        /** @var GridView $grid */
268
        $grid = $this->owner;
269
270
        if ($grid->rowOptions instanceof Closure) {
271
            $options = call_user_func($grid->rowOptions, $model, $key, $index, $this);
272
        } else {
273
            $options = $grid->rowOptions;
274
        }
275
        $options['data-key'] = is_array($key) ? json_encode($key) : (string)$key;
276
277
        $cells = [];
278
        /** @var \yii\grid\Column $column */
279
        foreach ($grid->columns as $column) {
280
            $name = property_exists($column, 'attribute') ? ArrayHelper::getValue($column, 'attribute') : '' ;
281
282
            $isGroupColumn = in_array($name, $this->mergeColumns);
283
284
            if (!$isGroupColumn) {
285
                $cells[] = $column->renderDataCell($model, $key, $index);
286
                continue;
287
            }
288
289
            $edge = $this->isGroupEdge($name, $index);
290
291
            switch ($this->type) {
292
                case static::MERGE_SIMPLE:
293
                case static::MERGE_NESTED:
294
                    if (isset($edge['start'])) {
295
                        $column->contentOptions['rowspan'] = $edge['group']['end'] - $edge['group']['start'] + 1;
296
                        Html::addCssClass($column->contentOptions, $this->mergeCellClass);
297
                        $cells[] = $column->renderDataCell($model, $key, $index);
298
                    }
299
                    break;
300
                case static::MERGE_FIRST_ROW:
301
                    $cells[] = isset($edge['start']) ? $column->renderDataCell($model, $key, $index) : '<td></td>';
302
            }
303
        }
304
305
        $rows[] = Html::tag('tr', implode('', $cells), $options);
306
307
        if (count($this->extraRowColumns)) {
308
            $extraRowEdge = $this->isGroupEdge($this->extraRowColumns[0], $index);
309
            if ($this->extraRowPosition == static::POS_ABOVE && isset($extraRowEdge['start'])) {
310
                array_unshift($rows, $this->renderExtraRow($model, $key, $index, $extraRowEdge['group']['totals']));
311
            }
312
313
            if ($this->extraRowPosition == static::POS_BELOW && isset($extraRowEdge['end'])) {
314
                $rows[] = $this->renderExtraRow($model, $key, $index, $extraRowEdge['group']['totals']);
315
            }
316
        }
317
318
        return implode('', $rows);
319
    }
320
321
    /**
322
     * Renders extra row when required
323
     *
324
     * @param  mixed $model
325
     * @param  mixed $key
326
     * @param  int $index
327
     * @param  number $totals
328
     *
329
     * @return string the extra row
330
     */
331
    protected function renderExtraRow($model, $key, $index, $totals)
332
    {
333
        /** @var GridView $grid */
334
        $grid = $this->owner;
335
336
        if ($this->extraRowValue instanceof Closure) {
337
            $content = call_user_func($this->extraRowValue, $model, $key, $index, $totals);
338
        } else {
339
            $values = [];
340
            foreach ($this->extraRowColumns as $name) {
341
                $values[] = ArrayHelper::getValue($model, $name);
342
            }
343
344
            $content = '<strong>' . implode(' :: ', $values) . '</strong>';
345
        }
346
347
        $colspan = count($grid->columns);
348
349
        $cell = Html::tag('td', $content, ['class' => $this->extraRowClass, 'colspan' => $colspan]);
350
351
        return Html::tag('tr', $cell);
352
    }
353
354
    /**
355
     * Is current row start or end of group in particular column
356
     *
357
     * @param  string $name the column name
358
     * @param  int $row the row index
359
     *
360
     * @return array
361
     */
362
    protected function isGroupEdge($name, $row)
363
    {
364
        $result = [];
365
        foreach ($this->_groups[$name] as $column) {
366
            if ($column['start'] == $row) {
367
                $result['start'] = $row;
368
                $result['group'] = $column;
369
            }
370
            if ($column['end'] == $row) {
371
                $result['end'] = $row;
372
                $result['group'] = $column;
373
            }
374
            if (count($result)) {
375
                break;
376
            }
377
        }
378
379
        return $result;
380
    }
381
382
    /**
383
     * If there is a Closure will return the newly calculated totals on the Closure according to the code specified by
384
     * the user.
385
     *
386
     * @param  mixed $model the data model being rendered
387
     * @param  int $index the zero-based index of the data model among the models array
388
     * @param  array $totals the calculated totals by the Closure
389
     *
390
     * @return array|mixed
391
     */
392
    protected function getExtraRowTotals($model, $index, $totals)
393
    {
394
        return $this->extraRowTotalsValue instanceof Closure
395
            ? call_user_func($this->extraRowTotalsValue, $model, $index, $totals)
396
            : [];
397
    }
398
399
    /**
400
     * Returns the row values of the column
401
     *
402
     * @param  array $columns the columns
403
     * @param  mixed $model the model
404
     * @param  int $index the zero-base index of the model among the models array
405
     *
406
     * @return array
407
     */
408
    protected function getRowValues($columns, $model, $index = 0)
409
    {
410
        /** @var GridView $grid */
411
        $grid = $this->owner;
412
        $values = [];
413
        $keys = $grid->dataProvider->getKeys();
414
        foreach ($columns as $column) {
415
            /** @var \yii\grid\DataColumn $column */
416
            if ($column instanceof DataColumn) { // we only work with DataColumn types
417
                $values[$column->attribute] = $this->getColumnDataCellContent($column, $model, $keys[$index], $index);
418
            }
419
        }
420
421
        return $values;
422
    }
423
424
    /**
425
     * Returns the column data cell content
426
     *
427
     * @param  \yii\grid\DataColumn $column
428
     * @param  mixed $model the data model being rendered
429
     * @param  mixed $key the key associated with the data model
430
     * @param  int $index the zero-based index of the data model among the models array returned by [[GridView::dataProvider]]
431
     *
432
     * @return string               the rendering result
433
     */
434
    protected function getColumnDataCellContent($column, $model, $key, $index)
435
    {
436
        /** @var GridView $grid */
437
        $grid = $this->owner;
438
        if ($column->content === null) {
439
            return $grid->formatter->format(
440
                $this->getColumnDataCellValue($column, $model, $key, $index),
441
                $column->format
442
            );
443
        }
444
        if ($column->content !== null) {
445
            return call_user_func($column->content, $model, $key, $index, $column);
446
        }
447
448
        return $grid->emptyCell;
449
    }
450
451
    /**
452
     * Returns the column data cell value
453
     *
454
     * @param  \yii\grid\DataColumn $column
455
     * @param  mixed $model the data model being rendered
456
     * @param  mixed $key the key associated with the data model
457
     * @param  int $index the zero-based index of the data model among the models array
458
     *
459
     * @return mixed|null           the result
460
     */
461
    protected function getColumnDataCellValue($column, $model, $key, $index)
0 ignored issues
show
Unused Code introduced by
The parameter $key is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
462
    {
463
        if ($column->value !== null) {
464
            if (is_string($column->value)) {
465
                return ArrayHelper::getValue($model, $column->value);
466
            }
467
468
            return call_user_func($column->value, $model, $index, $column);
469
        } elseif ($column->attribute !== null) {
470
            return ArrayHelper::getValue($model, $column->attribute);
471
        }
472
473
        return null;
474
    }
475
}
476