1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace Application\Service\Exporter; |
6
|
|
|
|
7
|
|
|
use DateTimeImmutable; |
8
|
|
|
use DateTimeInterface; |
9
|
|
|
use Money\Currencies\ISOCurrencies; |
10
|
|
|
use Money\Formatter\DecimalMoneyFormatter; |
11
|
|
|
use Money\Money; |
12
|
|
|
use PhpOffice\PhpSpreadsheet\Cell\Coordinate; |
13
|
|
|
use PhpOffice\PhpSpreadsheet\Shared\Date; |
14
|
|
|
use PhpOffice\PhpSpreadsheet\Spreadsheet; |
15
|
|
|
use PhpOffice\PhpSpreadsheet\Style\Alignment; |
16
|
|
|
use PhpOffice\PhpSpreadsheet\Style\Border; |
17
|
|
|
use PhpOffice\PhpSpreadsheet\Style\Conditional; |
18
|
|
|
use PhpOffice\PhpSpreadsheet\Style\Fill; |
19
|
|
|
use PhpOffice\PhpSpreadsheet\Style\NumberFormat; |
20
|
|
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; |
21
|
|
|
use PhpOffice\PhpSpreadsheet\Writer\Xlsx; |
22
|
|
|
|
23
|
|
|
abstract class AbstractExcel extends AbstractExporter |
24
|
|
|
{ |
25
|
|
|
/** |
26
|
|
|
* Zebra striping of data rows |
27
|
|
|
*/ |
28
|
|
|
protected bool $zebra = true; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* Enable auto filter on the headers |
32
|
|
|
*/ |
33
|
|
|
protected bool $autoFilter = true; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* Column of current cell we are writing in |
37
|
|
|
*/ |
38
|
|
|
protected int $column = 1; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* Row of current cell we are writing in |
42
|
|
|
*/ |
43
|
|
|
protected int $row = 1; |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* Index of first column containing data |
47
|
|
|
*/ |
48
|
|
|
private int $firstDataColumn = 1; |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* Index of first row containing data |
52
|
|
|
*/ |
53
|
|
|
private int $firstDataRow = 1; |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* Index of last column containing data |
57
|
|
|
*/ |
58
|
|
|
protected int $lastDataColumn = 1; |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* Index of last row containing data |
62
|
|
|
*/ |
63
|
|
|
protected int $lastDataRow = 1; |
64
|
|
|
|
65
|
|
|
private Spreadsheet $workbook; |
66
|
|
|
|
67
|
|
|
protected Worksheet $sheet; |
68
|
|
|
|
69
|
|
|
protected DecimalMoneyFormatter $moneyFormatter; |
70
|
|
|
|
71
|
|
|
protected static array $dateFormat = [ |
72
|
|
|
'numberFormat' => ['formatCode' => NumberFormat::FORMAT_DATE_XLSX14], |
73
|
|
|
]; |
74
|
|
|
|
75
|
|
|
protected static array $defaultFormat = [ |
76
|
|
|
'font' => ['size' => 11], |
77
|
|
|
'alignment' => ['vertical' => Alignment::VERTICAL_CENTER], |
78
|
|
|
]; |
79
|
|
|
|
80
|
|
|
protected static array $titleFormat = [ |
81
|
|
|
'font' => ['size' => 14], |
82
|
|
|
]; |
83
|
|
|
|
84
|
|
|
protected static array $headerFormat = [ |
85
|
|
|
'font' => ['bold' => true, 'color' => ['argb' => 'FFEAEAEA']], |
86
|
|
|
'alignment' => ['wrapText' => true], |
87
|
|
|
'fill' => [ |
88
|
|
|
'fillType' => Fill::FILL_SOLID, |
89
|
|
|
'startColor' => [ |
90
|
|
|
'argb' => 'FF666666', |
91
|
|
|
], |
92
|
|
|
], |
93
|
|
|
]; |
94
|
|
|
|
95
|
|
|
protected static array $zebraFormat = [ |
96
|
|
|
'fill' => [ |
97
|
|
|
'fillType' => Fill::FILL_SOLID, |
98
|
|
|
'startColor' => [ |
99
|
|
|
'argb' => 'FFE6E6E6', |
100
|
|
|
], |
101
|
|
|
'endColor' => [ |
102
|
|
|
'argb' => 'FFE6E6E6', |
103
|
|
|
], |
104
|
|
|
], |
105
|
|
|
]; |
106
|
|
|
|
107
|
|
|
protected static array $totalFormat = [ |
108
|
|
|
'font' => ['bold' => true], |
109
|
|
|
'alignment' => ['wrapText' => true], |
110
|
|
|
'fill' => [ |
111
|
|
|
'fillType' => Fill::FILL_SOLID, |
112
|
|
|
'startColor' => [ |
113
|
|
|
'argb' => 'FFDDDDDD', |
114
|
|
|
], |
115
|
|
|
], |
116
|
|
|
]; |
117
|
|
|
|
118
|
|
|
protected static array $centerFormat = [ |
119
|
|
|
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER], |
120
|
|
|
]; |
121
|
|
|
|
122
|
|
|
protected static array $rightFormat = [ |
123
|
|
|
'alignment' => ['horizontal' => Alignment::HORIZONTAL_RIGHT], |
124
|
|
|
]; |
125
|
|
|
|
126
|
|
|
protected static array $leftFormat = [ |
127
|
|
|
'alignment' => ['horizontal' => Alignment::HORIZONTAL_LEFT], |
128
|
|
|
]; |
129
|
|
|
|
130
|
|
|
protected static array $wrapFormat = [ |
131
|
|
|
'alignment' => ['wrapText' => true], |
132
|
|
|
]; |
133
|
|
|
|
134
|
|
|
protected static array $inactiveFormat = [ |
135
|
|
|
'font' => ['color' => ['argb' => 'FFC0C0C0']], |
136
|
|
|
]; |
137
|
|
|
|
138
|
|
|
/** |
139
|
|
|
* Define border cells inside list of data (very light borders) |
140
|
|
|
*/ |
141
|
|
|
protected static array $bordersInside = [ |
142
|
|
|
'borders' => [ |
143
|
|
|
'inside' => [ |
144
|
|
|
'borderStyle' => Border::BORDER_HAIR, |
145
|
|
|
], |
146
|
|
|
'outline' => [ |
147
|
|
|
'borderStyle' => Border::BORDER_MEDIUM, |
148
|
|
|
], |
149
|
|
|
], |
150
|
|
|
]; |
151
|
|
|
|
152
|
|
|
/** |
153
|
|
|
* Define border cells for total row (thick border) |
154
|
|
|
*/ |
155
|
|
|
protected static array $bordersTotal = [ |
156
|
|
|
'borders' => [ |
157
|
|
|
'outline' => [ |
158
|
|
|
'borderStyle' => Border::BORDER_THICK, |
159
|
|
|
], |
160
|
|
|
], |
161
|
|
|
]; |
162
|
|
|
|
163
|
|
|
protected static array $bordersBottom = [ |
164
|
|
|
'borders' => [ |
165
|
|
|
'bottom' => [ |
166
|
|
|
'borderStyle' => Border::BORDER_MEDIUM, |
167
|
|
|
], |
168
|
|
|
], |
169
|
|
|
]; |
170
|
|
|
|
171
|
|
|
protected static array $bordersBottomLight = [ |
172
|
|
|
'borders' => [ |
173
|
|
|
'bottom' => [ |
174
|
|
|
'borderStyle' => Border::BORDER_HAIR, |
175
|
|
|
], |
176
|
|
|
], |
177
|
|
|
]; |
178
|
|
|
|
179
|
|
|
/** |
180
|
|
|
* Constructor |
181
|
|
|
*/ |
182
|
2 |
|
public function __construct(string $hostname) |
183
|
|
|
{ |
184
|
2 |
|
parent::__construct($hostname); |
185
|
2 |
|
$currencies = new ISOCurrencies(); |
186
|
2 |
|
$this->moneyFormatter = new DecimalMoneyFormatter($currencies); |
187
|
2 |
|
$this->workbook = new Spreadsheet(); |
188
|
2 |
|
$this->sheet = $this->workbook->getActiveSheet(); |
189
|
|
|
|
190
|
2 |
|
$this->sheet->getDefaultRowDimension()->setRowHeight(20); |
191
|
2 |
|
} |
192
|
|
|
|
193
|
1 |
|
protected function writeTitle(): void |
194
|
|
|
{ |
195
|
1 |
|
} |
196
|
|
|
|
197
|
|
|
protected function writeFooter(): void |
198
|
|
|
{ |
199
|
|
|
} |
200
|
|
|
|
201
|
2 |
|
private function writeHeaders(): void |
202
|
|
|
{ |
203
|
|
|
// Headers |
204
|
2 |
|
foreach ($this->getHeaders() as $header) { |
205
|
|
|
// Apply width |
206
|
2 |
|
if (isset($header['width'])) { |
207
|
2 |
|
$colDimension = $this->sheet->getColumnDimensionByColumn($this->column); |
208
|
2 |
|
if ($header['width'] === 'auto') { |
209
|
|
|
$colDimension->setAutoSize(true); |
210
|
|
|
} else { |
211
|
2 |
|
$colDimension->setWidth($header['width']); |
212
|
|
|
} |
213
|
|
|
} |
214
|
|
|
// Apply default format |
215
|
2 |
|
if (!isset($header['formats'])) { |
216
|
|
|
$header['formats'] = [self::$headerFormat]; |
217
|
|
|
} |
218
|
|
|
|
219
|
2 |
|
if (isset($header['colspan']) && $header['colspan'] > 1) { |
220
|
1 |
|
$this->sheet->mergeCellsByColumnAndRow($this->column, $this->row, $this->column + (int) $header['colspan'] - 1, $this->row); |
221
|
|
|
} |
222
|
|
|
|
223
|
2 |
|
$this->write($header['label'], ...$header['formats']); |
224
|
|
|
|
225
|
2 |
|
if (isset($header['colspan']) && $header['colspan'] > 1) { |
226
|
1 |
|
$this->column += (int) $header['colspan'] - 1; |
227
|
|
|
} |
228
|
|
|
} |
229
|
2 |
|
} |
230
|
|
|
|
231
|
|
|
abstract protected function getHeaders(): array; |
232
|
|
|
|
233
|
|
|
/** |
234
|
|
|
* Write the value and style in the cell selected by `column` and `row` variables and move to next column |
235
|
|
|
* |
236
|
|
|
* @param mixed $value |
237
|
|
|
* @param array[] ...$formats optional list of formats to be applied successively |
238
|
|
|
*/ |
239
|
2 |
|
protected function write($value, array ...$formats): void |
240
|
|
|
{ |
241
|
2 |
|
$cell = $this->sheet->getCellByColumnAndRow($this->column++, $this->row); |
242
|
2 |
|
if ($formats) { |
|
|
|
|
243
|
2 |
|
$style = $cell->getStyle(); |
244
|
2 |
|
foreach ($formats as $format) { |
245
|
2 |
|
$style->applyFromArray($format); |
246
|
|
|
} |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
// Automatic conversion of date to Excel format |
250
|
2 |
|
if ($value instanceof DateTimeInterface) { |
251
|
|
|
$dateTime = new DateTimeImmutable($value->format('c')); |
252
|
|
|
$value = Date::PHPToExcel($dateTime); |
253
|
2 |
|
} elseif ($value instanceof Money) { |
254
|
1 |
|
$value = $this->moneyFormatter->format($value); |
255
|
|
|
} |
256
|
|
|
|
257
|
2 |
|
$cell->setValue($value); |
258
|
2 |
|
} |
259
|
|
|
|
260
|
2 |
|
protected function initialize(string $path): void |
261
|
|
|
{ |
262
|
2 |
|
$this->workbook->getDefaultStyle()->applyFromArray(self::$defaultFormat); |
263
|
2 |
|
$this->row = 1; |
264
|
2 |
|
$this->column = 1; |
265
|
2 |
|
$this->writeTitle(); |
266
|
2 |
|
$this->column = 1; |
267
|
2 |
|
$this->writeHeaders(); |
268
|
2 |
|
++$this->row; |
269
|
2 |
|
$this->column = 1; |
270
|
2 |
|
$this->firstDataRow = $this->row; |
271
|
2 |
|
$this->firstDataColumn = $this->column; |
272
|
2 |
|
} |
273
|
|
|
|
274
|
2 |
|
protected function finalize(string $path): void |
275
|
|
|
{ |
276
|
2 |
|
$this->applyZebra(); |
277
|
2 |
|
$this->applyAutoFilter(); |
278
|
|
|
|
279
|
2 |
|
$this->column = 1; |
280
|
2 |
|
$this->row = $this->lastDataRow + 1; |
281
|
2 |
|
$this->writeFooter(); |
282
|
|
|
|
283
|
2 |
|
$writer = new Xlsx($this->workbook); |
284
|
2 |
|
$writer->save($path); |
285
|
2 |
|
} |
286
|
|
|
|
287
|
2 |
|
protected function getExtension(): string |
288
|
|
|
{ |
289
|
2 |
|
return 'xlsx'; |
290
|
|
|
} |
291
|
|
|
|
292
|
2 |
|
private function applyZebra(): void |
293
|
|
|
{ |
294
|
2 |
|
if (!$this->zebra) { |
295
|
1 |
|
return; |
296
|
|
|
} |
297
|
|
|
|
298
|
1 |
|
$zebraRange = Coordinate::stringFromColumnIndex($this->firstDataColumn) . $this->firstDataRow . ':' . Coordinate::stringFromColumnIndex($this->lastDataColumn) . $this->lastDataRow; |
299
|
1 |
|
$zebraCondition = new Conditional(); |
300
|
1 |
|
$zebraCondition->setConditionType(Conditional::CONDITION_EXPRESSION)->setOperatorType(Conditional::OPERATOR_EQUAL)->addCondition('MOD(ROW(),2)=0'); |
301
|
1 |
|
$zebraCondition->getStyle()->applyFromArray(self::$zebraFormat); |
302
|
1 |
|
$this->sheet->getStyle($zebraRange)->setConditionalStyles([$zebraCondition]); |
303
|
1 |
|
} |
304
|
|
|
|
305
|
2 |
|
private function applyAutoFilter(): void |
306
|
|
|
{ |
307
|
2 |
|
if ($this->autoFilter) { |
308
|
1 |
|
$this->sheet->setAutoFilterByColumnAndRow($this->firstDataColumn, $this->firstDataRow - 1, $this->lastDataColumn, $this->lastDataRow); |
309
|
|
|
} |
310
|
2 |
|
} |
311
|
|
|
} |
312
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.