1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace Application\Action; |
6
|
|
|
|
7
|
|
|
use Application\Model\AbstractModel; |
8
|
|
|
use Doctrine\ORM\Query; |
9
|
|
|
use PhpOffice\PhpSpreadsheet\Shared\Date; |
10
|
|
|
use PhpOffice\PhpSpreadsheet\Spreadsheet; |
11
|
|
|
use PhpOffice\PhpSpreadsheet\Style\Alignment; |
12
|
|
|
use PhpOffice\PhpSpreadsheet\Style\Border; |
13
|
|
|
use PhpOffice\PhpSpreadsheet\Style\Fill; |
14
|
|
|
use PhpOffice\PhpSpreadsheet\Style\NumberFormat; |
15
|
|
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; |
16
|
|
|
use PhpOffice\PhpSpreadsheet\Writer\Xlsx; |
17
|
|
|
use Psr\Http\Message\ResponseInterface; |
18
|
|
|
use Psr\Http\Message\ServerRequestInterface; |
19
|
|
|
use Psr\Http\Server\RequestHandlerInterface; |
20
|
|
|
use Zend\Diactoros\Response; |
21
|
|
|
|
22
|
|
|
abstract class AbstractExcel extends AbstractAction |
23
|
|
|
{ |
24
|
|
|
/** |
25
|
|
|
* Column of current cell we are writing in |
26
|
|
|
* |
27
|
|
|
* @var int |
28
|
|
|
*/ |
29
|
|
|
protected $column = 1; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* Row of current cell we are writing in |
33
|
|
|
* |
34
|
|
|
* @var int |
35
|
|
|
*/ |
36
|
|
|
protected $row = 1; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* @var Spreadsheet |
40
|
|
|
*/ |
41
|
|
|
protected $workbook; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* @var string |
45
|
|
|
*/ |
46
|
|
|
protected $outputFileName = 'export.xlsx'; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* @var string |
50
|
|
|
*/ |
51
|
|
|
protected $tmpDir = 'data/tmp/excel'; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* @var string |
55
|
|
|
*/ |
56
|
|
|
protected $hostname; |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* @var string |
60
|
|
|
*/ |
61
|
|
|
protected $routeName; |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* The model class name |
65
|
|
|
* |
66
|
|
|
* @return string |
67
|
|
|
*/ |
68
|
|
|
abstract protected function getModelClass(); |
69
|
|
|
|
70
|
|
|
protected static $dateFormat = [ |
71
|
|
|
'numberFormat' => ['formatCode' => NumberFormat::FORMAT_DATE_XLSX14], |
72
|
|
|
]; |
73
|
|
|
|
74
|
|
|
protected static $defaultFormat = [ |
75
|
|
|
'font' => ['size' => 11], |
76
|
|
|
'alignment' => ['vertical' => Alignment::VERTICAL_CENTER], |
77
|
|
|
]; |
78
|
|
|
|
79
|
|
|
protected static $headerFormat = [ |
80
|
|
|
'font' => ['bold' => true, 'color' => ['argb' => 'FFEAEAEA']], |
81
|
|
|
'alignment' => ['wrapText' => true], |
82
|
|
|
'fill' => [ |
83
|
|
|
'fillType' => Fill::FILL_SOLID, |
84
|
|
|
'startColor' => [ |
85
|
|
|
'argb' => 'FF666666', |
86
|
|
|
], |
87
|
|
|
], |
88
|
|
|
]; |
89
|
|
|
|
90
|
|
|
protected static $totalFormat = [ |
91
|
|
|
'font' => ['bold' => true], |
92
|
|
|
'alignment' => ['wrapText' => true], |
93
|
|
|
'fill' => [ |
94
|
|
|
'fillType' => Fill::FILL_SOLID, |
95
|
|
|
'startColor' => [ |
96
|
|
|
'argb' => 'FFDDDDDD', |
97
|
|
|
], |
98
|
|
|
], |
99
|
|
|
]; |
100
|
|
|
|
101
|
|
|
protected static $centerFormat = [ |
102
|
|
|
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER], |
103
|
|
|
]; |
104
|
|
|
|
105
|
|
|
protected static $rightFormat = [ |
106
|
|
|
'alignment' => ['horizontal' => Alignment::HORIZONTAL_RIGHT], |
107
|
|
|
]; |
108
|
|
|
|
109
|
|
|
protected static $wrapFormat = [ |
110
|
|
|
'alignment' => ['wrapText' => true], |
111
|
|
|
]; |
112
|
|
|
|
113
|
|
|
/** |
114
|
|
|
* Define border cells inside list of data (very light borders) |
115
|
|
|
* |
116
|
|
|
* @var array |
117
|
|
|
*/ |
118
|
|
|
protected static $bordersInside = [ |
119
|
|
|
'borders' => [ |
120
|
|
|
'inside' => [ |
121
|
|
|
'borderStyle' => Border::BORDER_HAIR, |
122
|
|
|
], |
123
|
|
|
'outline' => [ |
124
|
|
|
'borderStyle' => Border::BORDER_MEDIUM, |
125
|
|
|
], |
126
|
|
|
], |
127
|
|
|
]; |
128
|
|
|
|
129
|
|
|
/** |
130
|
|
|
* Define border cells for total row (thick border) |
131
|
|
|
* |
132
|
|
|
* @var array |
133
|
|
|
*/ |
134
|
|
|
protected static $bordersTotal = [ |
135
|
|
|
'borders' => [ |
136
|
|
|
'outline' => [ |
137
|
|
|
'borderStyle' => Border::BORDER_THICK, |
138
|
|
|
], |
139
|
|
|
], |
140
|
|
|
]; |
141
|
|
|
|
142
|
|
|
/** |
143
|
|
|
* @var array |
144
|
|
|
*/ |
145
|
|
|
protected static $bordersBottom = [ |
146
|
|
|
'borders' => [ |
147
|
|
|
'bottom' => [ |
148
|
|
|
'borderStyle' => Border::BORDER_MEDIUM, |
149
|
|
|
], |
150
|
|
|
], |
151
|
|
|
]; |
152
|
|
|
|
153
|
|
|
/** |
154
|
|
|
* @var array |
155
|
|
|
*/ |
156
|
|
|
protected static $bordersBottomLight = [ |
157
|
|
|
'borders' => [ |
158
|
|
|
'bottom' => [ |
159
|
|
|
'borderStyle' => Border::BORDER_HAIR, |
160
|
|
|
], |
161
|
|
|
], |
162
|
|
|
]; |
163
|
|
|
|
164
|
|
|
/** |
165
|
|
|
* Constructor |
166
|
|
|
* |
167
|
|
|
* @param string $hostname |
168
|
|
|
* @param string $routeName |
169
|
|
|
*/ |
170
|
|
|
public function __construct(string $hostname, string $routeName) |
171
|
|
|
{ |
172
|
|
|
$this->hostname = $hostname; |
173
|
|
|
$this->routeName = $routeName; |
174
|
|
|
$this->workbook = new Spreadsheet(); |
175
|
|
|
$this->workbook->setActiveSheetIndex(0); |
176
|
|
|
} |
177
|
|
|
|
178
|
|
|
/** |
179
|
|
|
* @param Worksheet $sheet |
180
|
|
|
* @param AbstractModel[] $items |
181
|
|
|
*/ |
182
|
|
|
protected function writeHeaders(Worksheet $sheet, array $items): void |
183
|
|
|
{ |
184
|
|
|
$initialColumn = $this->column; |
185
|
|
|
|
186
|
|
|
// Headers |
187
|
|
|
foreach ($this->getHeaders() as $header) { |
188
|
|
|
// Apply width |
189
|
|
|
if (isset($header['width'])) { |
190
|
|
|
$colDimension = $sheet->getColumnDimensionByColumn($this->column); |
191
|
|
|
if ($header['width'] === 'auto') { |
192
|
|
|
$colDimension->setAutoSize(true); |
193
|
|
|
} else { |
194
|
|
|
$colDimension->setWidth($header['width']); |
195
|
|
|
} |
196
|
|
|
} |
197
|
|
|
// Apply format |
198
|
|
|
if (!isset($header['formats'])) { |
199
|
|
|
$header['formats'] = []; |
200
|
|
|
} |
201
|
|
|
$header['formats'] = [-1 => self::$headerFormat] + $header['formats']; |
202
|
|
|
|
203
|
|
|
$this->write($sheet, $header['label'], ...$header['formats']); |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
// Apply AutoFilters |
207
|
|
|
$sheet->setAutoFilterByColumnAndRow($initialColumn, $this->row, $this->column - 1, $this->row + count($items)); |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
/** |
211
|
|
|
* Write the items, one per line, in the body part of the sheet |
212
|
|
|
* |
213
|
|
|
* @param Worksheet $sheet |
214
|
|
|
* @param AbstractModel[] $items |
215
|
|
|
*/ |
216
|
|
|
abstract protected function writeData(Worksheet $sheet, array $items): void; |
217
|
|
|
|
218
|
|
|
/** |
219
|
|
|
* Write the footer line |
220
|
|
|
* |
221
|
|
|
* @param Worksheet $sheet |
222
|
|
|
* @param AbstractModel[] $items |
223
|
|
|
*/ |
224
|
|
|
abstract protected function writeFooter(Worksheet $sheet, array $items): void; |
225
|
|
|
|
226
|
|
|
/** |
227
|
|
|
* @return array |
228
|
|
|
*/ |
229
|
|
|
abstract protected function getHeaders(): array; |
230
|
|
|
|
231
|
|
|
/** |
232
|
|
|
* Write the value and style in the cell selected by `column` and `row` variables and move to next column |
233
|
|
|
* |
234
|
|
|
* @param \PhpOffice\PhpSpreadsheet\Worksheet $sheet |
235
|
|
|
* @param mixed $value |
236
|
|
|
* @param array[] ...$formats optional list of formats to be applied successively |
237
|
|
|
*/ |
238
|
|
|
protected function write(Worksheet $sheet, $value, array ...$formats): void |
239
|
|
|
{ |
240
|
|
|
$cell = $sheet->getCellByColumnAndRow($this->column++, $this->row); |
241
|
|
|
if ($formats) { |
|
|
|
|
242
|
|
|
$style = $cell->getStyle(); |
243
|
|
|
foreach ($formats as $format) { |
244
|
|
|
$style->applyFromArray($format); |
245
|
|
|
} |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
// Automatic conversion of date to Excel format |
249
|
|
|
if ($value instanceof \DateTimeInterface) { |
250
|
|
|
$dateTime = new \DateTime($value->format('c')); |
251
|
|
|
$value = Date::PHPToExcel($dateTime); |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
$cell->setValue($value); |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
/* |
258
|
|
|
* Called by the field resolver or repository to generate a spreadsheet from the query builder |
259
|
|
|
* |
260
|
|
|
* @param Query $query |
261
|
|
|
* |
262
|
|
|
* @return string the generated spreadsheet file path |
263
|
|
|
*/ |
264
|
|
|
public function generate(Query $query): string |
265
|
|
|
{ |
266
|
|
|
$items = $query->setMaxResults(1000)->getResult(); |
267
|
|
|
|
268
|
|
|
$this->workbook->getDefaultStyle()->applyFromArray(self::$defaultFormat); |
269
|
|
|
$sheet = $this->workbook->getActiveSheet(); |
270
|
|
|
$this->row = 1; |
271
|
|
|
$this->column = 1; |
272
|
|
|
$this->writeHeaders($sheet, $items); |
273
|
|
|
++$this->row; |
274
|
|
|
$this->column = 1; |
275
|
|
|
$this->writeData($sheet, $items); |
276
|
|
|
$this->column = 1; |
277
|
|
|
$this->writeFooter($sheet, $items); |
278
|
|
|
|
279
|
|
|
$writer = new Xlsx($this->workbook); |
280
|
|
|
|
281
|
|
|
$tmpFile = bin2hex(random_bytes(16)); |
282
|
|
|
$writer->save($this->tmpDir . '/' . $tmpFile); |
283
|
|
|
|
284
|
|
|
return 'https://' . $this->hostname . '/export/' . $this->routeName . '/' . $tmpFile . '/' . $this->outputFileName; |
285
|
|
|
} |
286
|
|
|
|
287
|
|
|
/** |
288
|
|
|
* Process the GET query to download previously generated spreasheet on disk |
289
|
|
|
* |
290
|
|
|
* @param ServerRequestInterface $request |
291
|
|
|
* @param RequestHandlerInterface $handler |
292
|
|
|
* |
293
|
|
|
* @return ResponseInterface |
294
|
|
|
*/ |
295
|
|
|
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface |
296
|
|
|
{ |
297
|
|
|
// Read file from disk |
298
|
|
|
$tmpFile = $this->tmpDir . '/' . $request->getAttribute('key'); |
299
|
|
|
|
300
|
|
|
if (!file_exists($tmpFile)) { |
301
|
|
|
return new Response\EmptyResponse(404); |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
$size = filesize($tmpFile); |
305
|
|
|
$output = fopen($tmpFile, 'r'); |
306
|
|
|
|
307
|
|
|
$response = new Response($output, 200, [ |
|
|
|
|
308
|
|
|
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', |
309
|
|
|
'Content-Disposition' => sprintf('attachment; filename=%s', $request->getAttribute('name')), |
310
|
|
|
'Access-Control-Expose-Headers' => 'Content-Disposition', |
311
|
|
|
'Expire' => 0, |
312
|
|
|
'Pragma' => 'public', |
313
|
|
|
'Content-Length' => $size, |
314
|
|
|
]); |
315
|
|
|
|
316
|
|
|
return $response; |
317
|
|
|
} |
318
|
|
|
} |
319
|
|
|
|
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.