Failed Conditions
Push — master ( e7a0e8...cff73f )
by Mark
36s queued 31s
created

StructuredReference::getTableByName()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 4
c 0
b 0
f 0
dl 0
loc 9
ccs 0
cts 0
cp 0
rs 10
cc 2
nc 2
nop 1
crap 6
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Calculation\Engine\Operands;
4
5
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
6
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
7
use PhpOffice\PhpSpreadsheet\Cell\Cell;
8
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
9
use PhpOffice\PhpSpreadsheet\Worksheet\Table;
10
11
final class StructuredReference implements Operand
12
{
13
    public const NAME = 'Structured Reference';
14
15
    private const OPEN_BRACE = '[';
16 11
    private const CLOSE_BRACE = ']';
17
18 11
    private const ITEM_SPECIFIER_ALL = '#All';
19
    private const ITEM_SPECIFIER_HEADERS = '#Headers';
20
    private const ITEM_SPECIFIER_DATA = '#Data';
21 11
    private const ITEM_SPECIFIER_TOTALS = '#Totals';
22
    private const ITEM_SPECIFIER_THIS_ROW = '#This Row';
23 11
24
    private const ITEM_SPECIFIER_ROWS_SET = [
25 11
        self::ITEM_SPECIFIER_ALL,
26 11
        self::ITEM_SPECIFIER_HEADERS,
27 11
        self::ITEM_SPECIFIER_DATA,
28 11
        self::ITEM_SPECIFIER_TOTALS,
29 11
    ];
30 11
31 11
    private const TABLE_REFERENCE = '/([\p{L}_\\\\][\p{L}\p{N}\._]+)?(\[(?:[^\]\[]+|(?R))*+\])/miu';
32
33
    private string $value;
34 11
35 11
    private string $tableName;
36 11
37 7
    private string $reference;
38
39 11
    private ?int $headersRow;
40
41
    private int $firstDataRow;
42 11
43
    private int $lastDataRow;
44
45 11
    private ?int $totalsRow;
46
47 11
    private array $columns;
48
49
    public function __construct(string $structuredReference)
50
    {
51
        $this->value = $structuredReference;
52
    }
53
54
    public static function fromParser(string $formula, int $index, array $matches): self
55
    {
56
        $val = $matches[0];
57
58
        $srCount = substr_count($val, self::OPEN_BRACE)
59
            - substr_count($val, self::CLOSE_BRACE);
60
        while ($srCount > 0) {
61
            $srIndex = strlen($val);
62
            $srStringRemainder = substr($formula, $index + $srIndex);
63
            $closingPos = strpos($srStringRemainder, self::CLOSE_BRACE);
64
            if ($closingPos === false) {
65
                throw new Exception("Formula Error: No closing ']' to match opening '['");
66
            }
67
            $srStringRemainder = substr($srStringRemainder, 0, $closingPos + 1);
68
            --$srCount;
69
            if (strpos($srStringRemainder, self::OPEN_BRACE) !== false) {
70
                ++$srCount;
71
            }
72
            $val .= $srStringRemainder;
73
        }
74
75
        return new self($val);
76
    }
77
78
    /**
79
     * @throws Exception
80
     * @throws \PhpOffice\PhpSpreadsheet\Exception
81
     */
82
    public function parse(Cell $cell): string
83
    {
84
        $this->getTableStructure($cell);
85
        $cellRange = ($this->isRowReference()) ? $this->getRowReference($cell) : $this->getColumnReference();
86
87
        return $cellRange;
88
    }
89
90
    private function isRowReference(): bool
91
    {
92
        return strpos($this->value, '[@') !== false
93
            || strpos($this->value, '[' . self::ITEM_SPECIFIER_THIS_ROW . ']') !== false;
94
    }
95
96
    /**
97
     * @throws Exception
98
     * @throws \PhpOffice\PhpSpreadsheet\Exception
99
     */
100
    private function getTableStructure(Cell $cell): void
101
    {
102
        preg_match(self::TABLE_REFERENCE, $this->value, $matches);
103
104
        $this->tableName = $matches[1];
105
        $table = ($this->tableName === '')
106
            ? $this->getTableForCell($cell)
107
            : $this->getTableByName($cell);
108
        $this->reference = $matches[2];
109
        $tableRange = Coordinate::getRangeBoundaries($table->getRange());
110
111
        $this->headersRow = ($table->getShowHeaderRow()) ? (int) $tableRange[0][1] : null;
112
        $this->firstDataRow = ($table->getShowHeaderRow()) ? (int) $tableRange[0][1] + 1 : $tableRange[0][1];
113
        $this->totalsRow = ($table->getShowTotalsRow()) ? (int) $tableRange[1][1] : null;
114
        $this->lastDataRow = ($table->getShowTotalsRow()) ? (int) $tableRange[1][1] - 1 : $tableRange[1][1];
115
116
        $this->columns = $this->getColumns($cell, $tableRange);
117
    }
118
119
    /**
120
     * @throws Exception
121
     * @throws \PhpOffice\PhpSpreadsheet\Exception
122
     */
123
    private function getTableForCell(Cell $cell): Table
124
    {
125
        $tables = $cell->getWorksheet()->getTableCollection();
126
        foreach ($tables as $table) {
127
            /** @var Table $table */
128
            $range = $table->getRange();
129
            if ($cell->isInRange($range) === true) {
130
                $this->tableName = $table->getName();
131
132
                return $table;
133
            }
134
        }
135
136
        throw new Exception('Table for Structured Reference cannot be identified');
137
    }
138
139
    /**
140
     * @throws Exception
141
     * @throws \PhpOffice\PhpSpreadsheet\Exception
142
     */
143
    private function getTableByName(Cell $cell): Table
144
    {
145
        $table = $cell->getWorksheet()->getTableByName($this->tableName);
1 ignored issue
show
Bug introduced by
Are you sure the assignment to $table is correct as $cell->getWorksheet()->g...yName($this->tableName) targeting PhpOffice\PhpSpreadsheet...sheet::getTableByName() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
146
147
        if ($table === null) {
0 ignored issues
show
introduced by
The condition $table === null is always true.
Loading history...
148
            throw new Exception("Table {$this->tableName} for Structured Reference cannot be located");
149
        }
150
151
        return $table;
152
    }
153
154
    private function getColumns(Cell $cell, array $tableRange): array
155
    {
156
        $worksheet = $cell->getWorksheet();
157
        $cellReference = $cell->getCoordinate();
158
159
        $columns = [];
160
        $lastColumn = ++$tableRange[1][0];
161
        for ($column = $tableRange[0][0]; $column !== $lastColumn; ++$column) {
162
            $columns[$column] = $worksheet
163
                ->getCell($column . $this->headersRow)
164
                ->getCalculatedValue();
165
        }
166
167
        $cell = $worksheet->getCell($cellReference);
0 ignored issues
show
Unused Code introduced by
The assignment to $cell is dead and can be removed.
Loading history...
168
169
        return $columns;
170
    }
171
172
    private function getRowReference(Cell $cell): string
173
    {
174
        $reference = str_replace("\u{a0}", ' ', $this->reference);
175
        /** @var string $reference */
176
        $reference = str_replace('[' . self::ITEM_SPECIFIER_THIS_ROW . '],', '', $reference);
177
178
        foreach ($this->columns as $columnId => $columnName) {
179
            $columnName = str_replace("\u{a0}", ' ', $columnName);
180
            $cellReference = $columnId . $cell->getRow();
181
            $pattern1 = '/\[' . preg_quote($columnName) . '\]/miu';
182
            $pattern2 = '/@' . preg_quote($columnName) . '/miu';
183
            /** @var string $reference */
184
            if (preg_match($pattern1, $reference) === 1) {
185
                $reference = preg_replace($pattern1, $cellReference, $reference);
186
            } elseif (preg_match($pattern2, $reference) === 1) {
187
                $reference = preg_replace($pattern2, $cellReference, $reference);
188
            }
189
        }
190
191
        /** @var string $reference */
192
        return $this->validateParsedReference(trim($reference, '[]@, '));
193
    }
194
195
    /**
196
     * @throws Exception
197
     * @throws \PhpOffice\PhpSpreadsheet\Exception
198
     */
199
    private function getColumnReference(): string
200
    {
201
        $reference = str_replace("\u{a0}", ' ', $this->reference);
202
        $startRow = ($this->totalsRow === null) ? $this->lastDataRow : $this->totalsRow;
203
        $endRow = ($this->headersRow === null) ? $this->firstDataRow : $this->headersRow;
204
205
        [$startRow, $endRow] = $this->getRowsForColumnReference($reference, $startRow, $endRow);
206
        $reference = $this->getColumnsForColumnReference($reference, $startRow, $endRow);
207
208
        $reference = trim($reference, '[]@, ');
209
        if (substr_count($reference, ':') > 1) {
210
            $cells = explode(':', $reference);
211
            $firstCell = array_shift($cells);
212
            $lastCell = array_pop($cells);
213
            $reference = "{$firstCell}:{$lastCell}";
214
        }
215
216
        return $this->validateParsedReference($reference);
217
    }
218
219
    /**
220
     * @throws Exception
221
     * @throws \PhpOffice\PhpSpreadsheet\Exception
222
     */
223
    private function validateParsedReference(string $reference): string
224
    {
225
        if (preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . ':' . Calculation::CALCULATION_REGEXP_CELLREF . '$/miu', $reference) !== 1) {
226
            if (preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/miu', $reference) !== 1) {
227
                throw new Exception("Invalid Structured Reference {$this->reference} {$reference}");
228
            }
229
        }
230
231
        return $reference;
232
    }
233
234
    private function fullData(int $startRow, int $endRow): string
235
    {
236
        $columns = array_keys($this->columns);
237
        $firstColumn = array_shift($columns);
238
        $lastColumn = (empty($columns)) ? $firstColumn : array_pop($columns);
239
240
        return "{$firstColumn}{$startRow}:{$lastColumn}{$endRow}";
241
    }
242
243
    private function getMinimumRow(string $reference): int
244
    {
245
        switch ($reference) {
246
            case self::ITEM_SPECIFIER_ALL:
247
            case self::ITEM_SPECIFIER_HEADERS:
248
                return $this->headersRow ?? $this->firstDataRow;
249
            case self::ITEM_SPECIFIER_DATA:
250
                return $this->firstDataRow;
251
            case self::ITEM_SPECIFIER_TOTALS:
252
                return $this->totalsRow ?? $this->lastDataRow;
253
        }
254
255
        return $this->headersRow ?? $this->firstDataRow;
256
    }
257
258
    private function getMaximumRow(string $reference): int
259
    {
260
        switch ($reference) {
261
            case self::ITEM_SPECIFIER_HEADERS:
262
                return $this->headersRow ?? $this->firstDataRow;
263
            case self::ITEM_SPECIFIER_DATA:
264
                return $this->lastDataRow;
265
            case self::ITEM_SPECIFIER_ALL:
266
            case self::ITEM_SPECIFIER_TOTALS:
267
                return $this->totalsRow ?? $this->lastDataRow;
268
        }
269
270
        return $this->totalsRow ?? $this->lastDataRow;
271
    }
272
273
    public function value(): string
274
    {
275
        return $this->value;
276
    }
277
278
    /**
279
     * @return array<int, int>
280
     */
281
    private function getRowsForColumnReference(string &$reference, int $startRow, int $endRow): array
282
    {
283
        $rowsSelected = false;
284
        foreach (self::ITEM_SPECIFIER_ROWS_SET as $rowReference) {
285
            $pattern = '/\[' . $rowReference . '\]/mui';
286
            /** @var string $reference */
287
            if (preg_match($pattern, $reference) === 1) {
288
                $rowsSelected = true;
289
                $startRow = min($startRow, $this->getMinimumRow($rowReference));
290
                $endRow = max($endRow, $this->getMaximumRow($rowReference));
291
                $reference = preg_replace($pattern, '', $reference);
292
            }
293
        }
294
        if ($rowsSelected === false) {
295
            // If there isn't any Special Item Identifier specified, then the selection defaults to data rows only.
296
            $startRow = $this->firstDataRow;
297
            $endRow = $this->lastDataRow;
298
        }
299
300
        return [$startRow, $endRow];
301
    }
302
303
    private function getColumnsForColumnReference(string $reference, int $startRow, int $endRow): string
304
    {
305
        $columnsSelected = false;
306
        foreach ($this->columns as $columnId => $columnName) {
307
            $columnName = str_replace("\u{a0}", ' ', $columnName);
308
            $cellFrom = "{$columnId}{$startRow}";
309
            $cellTo = "{$columnId}{$endRow}";
310
            $cellReference = ($cellFrom === $cellTo) ? $cellFrom : "{$cellFrom}:{$cellTo}";
311
            $pattern = '/\[' . preg_quote($columnName) . '\]/mui';
312
            if (preg_match($pattern, $reference) === 1) {
313
                $columnsSelected = true;
314
                $reference = preg_replace($pattern, $cellReference, $reference);
315
            }
316
            /** @var string $reference */
317
        }
318
        if ($columnsSelected === false) {
319
            return $this->fullData($startRow, $endRow);
320
        }
321
322
        return $reference;
323
    }
324
}
325