Completed
Push — master ( 7e59a6...918d55 )
by Vitaliy
06:09 queued 02:22
created

PageTotalsRow::render()   D

Complexity

Conditions 9
Paths 4

Size

Total Lines 41
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 90

Importance

Changes 0
Metric Value
dl 0
loc 41
ccs 0
cts 29
cp 0
rs 4.909
c 0
b 0
f 0
cc 9
eloc 25
nc 4
nop 0
crap 90
1
<?php
2
3
namespace ViewComponents\Grids\Component;
4
5
use Closure;
6
use LogicException;
7
use Nayjest\Tree\ChildNodeTrait;
8
use ViewComponents\ViewComponents\Base\Compound\PartInterface;
9
use ViewComponents\ViewComponents\Base\Compound\PartTrait;
10
use ViewComponents\ViewComponents\Base\ViewComponentInterface;
11
use ViewComponents\ViewComponents\Component\CollectionView;
12
use ViewComponents\ViewComponents\Component\Compound;
13
use ViewComponents\Grids\Grid;
14
use ViewComponents\ViewComponents\Component\ManagedList;
15
use ViewComponents\ViewComponents\Rendering\ViewTrait;
16
17
/**
18
 * Class PageTotalsRow
19
 */
20
class PageTotalsRow implements PartInterface, ViewComponentInterface
21
{
22
    use PartTrait {
23
        PartTrait::attachToCompound as attachToCompoundInternal;
24
    }
25
    use ChildNodeTrait;
26
    use ViewTrait;
27
28
    const ID = 'page_totals_row';
29
30
    const OPERATION_SUM = 'sum';
31
    const OPERATION_AVG = 'avg';
32
    const OPERATION_COUNT = 'count';
33
    const OPERATION_IGNORE = 'ignore';
34
35
    protected $valuePrefixes = [
36
        self::OPERATION_SUM => 'Σ',
37
        self::OPERATION_AVG => 'Avg.',
38
        self::OPERATION_COUNT => 'Count:'
39
    ];
40
41
    /**
42
     * Keys are column id's and values are operations (PageTotalsRow::OPERATION_* constants  or closures).
43
     *
44
     * @var string[]|array
45
     */
46
    protected $operations;
47
48
    protected $totalData;
49
50
    protected $cellObserver;
51
52
    protected $rowsProcessed = 0;
53
54
    private $isTotalsCalculationFinished = false;
55
56
    /**
57
     * @var string
58
     */
59
    private $defaultOperation;
60
61
    /**
62
     * PageTotalsRow constructor.
63
     *
64
     * Operations passed to first argument ($operations) may contain values
65
     * of PageTotalsRow::OPERATIN_* constants or Closure or null. Keys must be equal to target column id's
66
     * If $operations has no value for column, default operation will be used for that column.
67
     *
68
     * Closure passed to operations can accept
69
     * accumulated value in first argument and current row value in second argument.
70
     *
71
     * @param array|string[] $operations (optional) keys are column id's and values are operations
72
     *                                   (see PageTotalsRow::OPERATION_* constants) or closures.
73
     * @param string|null $defaultOperation operation that will be used for column
74
     *                                      if operation isn't specified for this column in first argument.
75
     */
76
    public function __construct(array $operations = [], $defaultOperation = null)
77
    {
78
        $this->id = static::ID;
79
        $this->destinationParentId = ManagedList::LIST_CONTAINER_ID;
80
        $this->operations = $operations;
81
        if ($defaultOperation === null) {
82
            $defaultOperation = empty($operations) ? self::OPERATION_SUM : self::OPERATION_IGNORE;
83
        }
84
        $this->defaultOperation = $defaultOperation;
85
    }
86
87
    /**
88
     * @param Compound|Grid $root
89
     * @param bool $prepend
90
     */
91
    public function attachToCompound(Compound $root, $prepend = false)
92
    {
93
        $this->attachToCompoundInternal($root, $prepend);
94
        $this->replaceGridDataInjector($root);
0 ignored issues
show
Compatibility introduced by
$root of type object<ViewComponents\Vi...nts\Component\Compound> is not a sub-type of object<ViewComponents\Grids\Grid>. It seems like you assume a child class of the class ViewComponents\ViewComponents\Component\Compound 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...
95
    }
96
97
    /**
98
     * Returns string prefixes for data printed in totals row for different operations.
99
     *
100
     * Keys are operations and values are prefixes.
101
     *
102
     * @return string[]
103
     */
104
    public function getValuePrefixes()
105
    {
106
        return $this->valuePrefixes;
107
    }
108
109
    /**
110
     * Sets string prefixes for data printed in totals row for different operations.
111
     *
112
     * @param string[] $valuePrefixes keys are operations and values are prefixes.
113
     * @return $this
114
     */
115
    public function setValuePrefixes(array $valuePrefixes)
116
    {
117
        $this->valuePrefixes = $valuePrefixes;
118
        return $this;
119
    }
120
121
    /**
122
     * Renders tag and returns output.
123
     *
124
     * @return string
125
     */
126
    public function render()
127
    {
128
        /** @var Grid $grid */
129
        $grid = $this->root;
130
        $this->isTotalsCalculationFinished = true;
131
        $tr = $grid->getRecordView();
132
        // set total_row as current grid row
133
        $lastRow = $grid->getCurrentRow();
134
        $grid->setCurrentRow($this->totalData);
135
136
        // modify columns, prepare it for rendering totals row
137
        $valueCalculators = [];
138
        $valueFormatters = [];
139
        foreach ($grid->getColumns() as $column) {
140
            $valueCalculators[$column->getId()] = $column->getValueCalculator();
141
            $valueFormatters[$column->getId()] = $prevFormatter = $column->getValueFormatter();
142
            $column->setValueCalculator(null);
143
            $column->setValueFormatter(function ($value) use ($prevFormatter, $column) {
144
                $operation = $this->getOperation($column->getId());
145
                if ($prevFormatter && !($operation === static::OPERATION_IGNORE || $operation instanceof Closure)) {
146
                    $value = call_user_func($prevFormatter, $value);
147
                }
148
                // Add value prefix if specified for operation
149
                if ($value !== null && is_string($operation) && array_key_exists($operation, $this->valuePrefixes)) {
150
                    $value = $this->valuePrefixes[$operation] . '&nbsp;' . $value;
151
                }
152
                return $value;
153
            });
154
        }
155
156
        $output = $tr->render();
0 ignored issues
show
Bug introduced by
The method render does only exist in ViewComponents\ViewCompo...\ViewComponentInterface, but not in ViewComponents\ViewCompo...\Compound\PartInterface.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
157
158
        // restore column value calculators & formatters
159
        foreach ($grid->getColumns() as $column) {
160
            $column->setValueCalculator($valueCalculators[$column->getId()]);
161
            $column->setValueFormatter($valueFormatters[$column->getId()]);
162
        }
163
        // restore last data row
164
        $grid->setCurrentRow($lastRow);
165
        return $output;
166
    }
167
168
    protected function pushData($field, $value)
169
    {
170
        if ($this->totalData === null) {
171
            $this->totalData = new \stdClass();
172
        }
173
        if (!property_exists($this->totalData, $field)) {
174
            $this->totalData->$field = 0;
175
        }
176
        $operation = $this->getOperation($field);
177
        switch ($operation) {
178
            case self::OPERATION_SUM:
179
                if (!is_numeric($value)) {
180
                    return;
181
                }
182
                $this->totalData->$field += $value;
183
                break;
184
            case self::OPERATION_COUNT:
185
                $this->totalData->$field = $this->rowsProcessed;
186
                break;
187
            case self::OPERATION_AVG:
188
                $sumField = "{$field}_sum_for_totals";
189
                if (!property_exists($this->totalData, $sumField)) {
190
                    $this->totalData->$sumField = 0;
191
                }
192
                if (is_numeric($value)) {
193
                    $this->totalData->$sumField += $value;
194
                }
195
                $this->totalData->$field = round(
196
                    $this->totalData->$sumField / $this->rowsProcessed,
197
                    2
198
                );
199
                break;
200
            case self::OPERATION_IGNORE:
201
                $this->totalData->$field = null;
202
                break;
203
            default:
204
                if ($operation instanceof Closure) {
205
                    if (!property_exists($this->totalData, $field)) {
206
                        $this->totalData->$field = 0;
207
                    }
208
                    $this->totalData->$field = $operation($this->totalData->$field, $value);
209
                    break;
210
                }
211
                throw new LogicException(
212
                    'PageTotalsRow: Unknown aggregation operation.'
213
                );
214
        }
215
    }
216
217
    /**
218
     * @param string $columnName
219
     * @return string|Closure|null
220
     */
221
    protected function getOperation($columnName)
222
    {
223
        return array_key_exists($columnName, $this->operations)
224
            ? $this->operations[$columnName]
225
            : $this->defaultOperation;
226
    }
227
228
    protected function processCurrentRow()
229
    {
230
        if ($this->isTotalsCalculationFinished) {
231
            return;
232
        }
233
        $this->rowsProcessed++;
234
        /** @var Grid $grid */
235
        $grid = $this->root;
236
        foreach ($grid->getColumns() as $column) {
237
            $this->pushData($column->getId(), $column->getCurrentValue());
238
        }
239
    }
240
241
    protected function replaceGridDataInjector(Grid $grid)
242
    {
243
        /** @var CollectionView $collectionView */
244
        $grid->getCollectionView()->setDataInjector(function ($dataRow) use ($grid) {
245
            $grid->setCurrentRow($dataRow);
246
            $this->processCurrentRow();
247
        });
248
    }
249
}
250