Completed
Push — master ( 198be3...b06bc8 )
by Vitaliy
04:10
created

PageTotalsRow::__construct()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 22
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 22
rs 8.6737
cc 5
eloc 14
nc 3
nop 2
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\Rendering\ViewTrait;
15
16
/**
17
 * Class PageTotalsRow
18
 */
19
class PageTotalsRow implements PartInterface, ViewComponentInterface
20
{
21
    use PartTrait {
22
        PartTrait::attachToCompound as attachToCompoundInternal;
23
    }
24
    use ChildNodeTrait;
25
    use ViewTrait;
26
27
    const OPERATION_SUM = 'sum';
28
    const OPERATION_AVG = 'avg';
29
    const OPERATION_COUNT = 'count';
30
    const OPERATION_IGNORE = null;
31
32
    protected $valuePrefixes = [
33
        self::OPERATION_SUM => 'Σ',
34
        self::OPERATION_AVG => 'Avg.',
35
        self::OPERATION_COUNT => 'Count:'
36
    ];
37
38
    protected $operations;
39
40
    protected $totalData;
41
42
    protected $cellObserver;
43
44
    protected $rowsProcessed = 0;
45
46
    private $dataCollectingCallback;
47
48
    private $stopDataCollecting = false;
49
50
    /**
51
     * @var string
52
     */
53
    private $defaultOperation;
54
55
    /**
56
     * PageTotalsRow constructor.
57
     *
58
     * Operations passed to first argument ($operations) can contain values
59
     * of PageTotalsRow::OPERATIN_* constants or Closure or null.
60
     * If $operations has no key for column, default operation will be used.
61
     *
62
     * @param array $operations keys are field names and values are operations
63
     * @param string $defaultOperation
64
     */
65
    public function __construct(array $operations = [], $defaultOperation = null)
66
    {
67
        $this->id = 'page_totals_row';
68
        $this->destinationParentId = 'list_container';
69
        $this->operations = $operations;
70
        $this->dataCollectingCallback = function () {
71
            if ($this->stopDataCollecting) {
72
                return;
73
            }
74
            $this->rowsProcessed++;
75
            /** @var Grid $grid */
76
            $grid = $this->root;
77
            foreach ($grid->getColumns() as $column) {
78
                $this->pushData($column->getId(), $column->getCurrentValue());
79
            }
80
        };
81
82
        if ($defaultOperation === null) {
83
            $defaultOperation = empty($operations) ? self::OPERATION_SUM : self::OPERATION_IGNORE;
84
        }
85
        $this->defaultOperation = $defaultOperation;
86
    }
87
88
    public function attachToCompound(Compound $root, $prepend = false)
89
    {
90
        $this->attachToCompoundInternal($root, $prepend);
91
        /** @var CollectionView $collectionView */
92
        $collectionView = $root->getComponent('collection_view');
93
        $collectionView->setDataInjector(function ($dataRow, $collectionView) use ($root) {
94
            call_user_func([$root, 'setCurrentRow'], $dataRow, $collectionView);
95
            call_user_func($this->dataCollectingCallback);
96
        });
97
    }
98
99
    /**
100
     * @return array
101
     */
102
    public function getValuePrefixes()
103
    {
104
        return $this->valuePrefixes;
105
    }
106
107
    public function setValuePrefixes(array $valuePrefixes)
108
    {
109
        $this->valuePrefixes = $valuePrefixes;
110
        return $this;
111
    }
112
113
    public function render()
114
    {
115
        /** @var Grid $grid */
116
        $grid = $this->root;
117
        $this->stopDataCollecting = true;
118
        $tr = $grid->getRecordView();
119
120
        // set total_row as current grid row
121
        $lastRow = $grid->getCurrentRow();
122
        $grid->setCurrentRow($this->totalData);
123
124
        // modify columns
125
        $valueCalculators = [];
126
        $valueFormatters = [];
127
        foreach ($grid->getColumns() as $column) {
128
            $valueCalculators[$column->getId()] = $column->getValueCalculator();
129
            $valueFormatters[$column->getId()] = $prevFormatter = $column->getValueFormatter();
130
            $column->setValueCalculator(null);
131
            $column->setValueFormatter(function ($value) use ($prevFormatter, $column) {
132
                if ($prevFormatter) {
133
                    $value = call_user_func($prevFormatter, $value);
134
                }
135
                $operation = $this->getOperation($column->getId());
136
                if ($value !== null && is_string($operation) && array_key_exists($operation, $this->valuePrefixes)) {
137
                    $value = $this->valuePrefixes[$operation] . '&nbsp;' . $value;
138
                }
139
                return $value;
140
            });
141
        }
142
143
        $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...
144
145
        // restore column value calculators & formatters
146
        foreach ($grid->getColumns() as $column) {
147
            $column->setValueCalculator($valueCalculators[$column->getId()]);
148
            $column->setValueFormatter($valueFormatters[$column->getId()]);
149
        }
150
        // restore last data row
151
        $grid->setCurrentRow($lastRow);
152
        return $output;
153
    }
154
155
    protected function pushData($field, $value)
156
    {
157
        if ($this->totalData === null) {
158
            $this->totalData = new \stdClass();
159
        }
160
        if (!is_numeric($value)) {
161
            return;
162
        }
163
        if (!property_exists($this->totalData, $field)) {
164
            $this->totalData->$field = 0;
165
        }
166
        $operation = $this->getOperation($field);
167
        switch ($operation) {
168
            case self::OPERATION_SUM:
169
                $this->totalData->$field += $value;
170
                break;
171
            case self::OPERATION_COUNT:
172
                $this->totalData->$field = $this->rowsProcessed;
173
                break;
174
            case self::OPERATION_AVG:
175
                $sumFiled = "{$field}_sum_for_totals";
176
                if (!property_exists($this->totalData, $sumFiled)) {
177
                    $this->totalData->$sumFiled = 0;
178
                }
179
                $this->totalData->$sumFiled += $value;
180
                $this->totalData->$field = round(
181
                    $this->totalData->$sumFiled / $this->rowsProcessed,
182
                    2
183
                );
184
                break;
185
            case self::OPERATION_IGNORE:
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $operation of type string|Closure|null against self::OPERATION_IGNORE of type null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
186
                $this->totalData->$field = null;
187
                break;
188
            default:
189
                if ($operation instanceof Closure) {
190
                    $this->totalData->$field = $operation($this->totalData->$field, $value);
191
                    break;
192
                }
193
                throw new LogicException(
194
                    'PageTotalsRow: Unknown aggregation operation.'
195
                );
196
        }
197
    }
198
199
    /**
200
     * @param string $columnName
201
     * @return string|Closure|null
202
     */
203
    protected function getOperation($columnName)
204
    {
205
        return array_key_exists($columnName, $this->operations)
206
            ? $this->operations[$columnName]
207
            : $this->defaultOperation;
208
    }
209
}
210