Completed
Push — master ( feabd5...fe039e )
by Antonio
12:24 queued 11:06
created

src/behaviors/GroupColumnsBehavior.php (1 issue)

parameters are used.

Unused Code Minor

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
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