Failed Conditions
Push — master ( ccf9db...e09bcd )
by Sylvain
07:51
created

AbstractExcel::write()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5.1158

Importance

Changes 0
Metric Value
eloc 11
c 0
b 0
f 0
dl 0
loc 19
ccs 10
cts 12
cp 0.8333
rs 9.6111
cc 5
nc 6
nop 3
crap 5.1158
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Application\Handler;
6
7
use Application\Model\AbstractModel;
8
use DateTimeImmutable;
9
use DateTimeInterface;
10
use Doctrine\ORM\Query;
11
use Ecodev\Felix\Handler\AbstractHandler;
12
use Laminas\Diactoros\Response;
13
use Money\Currencies\ISOCurrencies;
14
use Money\Formatter\DecimalMoneyFormatter;
15
use Money\Money;
16
use PhpOffice\PhpSpreadsheet\Shared\Date;
17
use PhpOffice\PhpSpreadsheet\Spreadsheet;
18
use PhpOffice\PhpSpreadsheet\Style\Alignment;
19
use PhpOffice\PhpSpreadsheet\Style\Border;
20
use PhpOffice\PhpSpreadsheet\Style\Fill;
21
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
22
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
23
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
24
use Psr\Http\Message\ResponseInterface;
25
use Psr\Http\Message\ServerRequestInterface;
26
use Psr\Http\Server\RequestHandlerInterface;
27
28
abstract class AbstractExcel extends AbstractHandler
29
{
30
    /**
31
     * Column of current cell we are writing in
32
     *
33
     * @var int
34
     */
35
    protected $column = 1;
36
37
    /**
38
     * Row of current cell we are writing in
39
     *
40
     * @var int
41
     */
42
    protected $row = 1;
43
44
    /**
45
     * @var Spreadsheet
46
     */
47
    protected $workbook;
48
49
    /**
50
     * @var string
51
     */
52
    protected $outputFileName = 'export.xlsx';
53
54
    /**
55
     * @var string
56
     */
57
    protected $tmpDir = 'data/tmp/excel';
58
59
    /**
60
     * @var string
61
     */
62
    protected $hostname;
63
64
    /**
65
     * @var string
66
     */
67
    protected $routeName;
68
69
    protected DecimalMoneyFormatter $moneyFormatter;
70
71
    /**
72
     * The model class name
73
     *
74
     * @return string
75
     */
76
    abstract protected function getModelClass();
77
78
    protected static $dateFormat = [
79
        'numberFormat' => ['formatCode' => NumberFormat::FORMAT_DATE_XLSX14],
80
    ];
81
82
    protected static $defaultFormat = [
83
        'font' => ['size' => 11],
84
        'alignment' => ['vertical' => Alignment::VERTICAL_CENTER],
85
    ];
86
87
    protected static array $titleFormat = [
88
        'font' => ['size' => 14],
89
    ];
90
91
    protected static $headerFormat = [
92
        'font' => ['bold' => true, 'color' => ['argb' => 'FFEAEAEA']],
93
        'alignment' => ['wrapText' => true],
94
        'fill' => [
95
            'fillType' => Fill::FILL_SOLID,
96
            'startColor' => [
97
                'argb' => 'FF666666',
98
            ],
99
        ],
100
    ];
101
102
    protected static $totalFormat = [
103
        'font' => ['bold' => true],
104
        'alignment' => ['wrapText' => true],
105
        'fill' => [
106
            'fillType' => Fill::FILL_SOLID,
107
            'startColor' => [
108
                'argb' => 'FFDDDDDD',
109
            ],
110
        ],
111
    ];
112
113
    protected static $centerFormat = [
114
        'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER],
115
    ];
116
117
    protected static $rightFormat = [
118
        'alignment' => ['horizontal' => Alignment::HORIZONTAL_RIGHT],
119
    ];
120
121
    protected static $wrapFormat = [
122
        'alignment' => ['wrapText' => true],
123
    ];
124
125
    /**
126
     * Define border cells inside list of data (very light borders)
127
     *
128
     * @var array
129
     */
130
    protected static $bordersInside = [
131
        'borders' => [
132
            'inside' => [
133
                'borderStyle' => Border::BORDER_HAIR,
134
            ],
135
            'outline' => [
136
                'borderStyle' => Border::BORDER_MEDIUM,
137
            ],
138
        ],
139
    ];
140
141
    /**
142
     * Define border cells for total row (thick border)
143
     *
144
     * @var array
145
     */
146
    protected static $bordersTotal = [
147
        'borders' => [
148
            'outline' => [
149
                'borderStyle' => Border::BORDER_THICK,
150
            ],
151
        ],
152
    ];
153
154
    /**
155
     * @var array
156
     */
157
    protected static $bordersBottom = [
158
        'borders' => [
159
            'bottom' => [
160
                'borderStyle' => Border::BORDER_MEDIUM,
161
            ],
162
        ],
163
    ];
164
165
    /**
166
     * @var array
167
     */
168
    protected static $bordersBottomLight = [
169
        'borders' => [
170
            'bottom' => [
171
                'borderStyle' => Border::BORDER_HAIR,
172
            ],
173
        ],
174
    ];
175
176
    /**
177
     * Constructor
178
     */
179 2
    public function __construct(string $hostname, string $routeName)
180
    {
181 2
        $this->hostname = $hostname;
182 2
        $this->routeName = $routeName;
183 2
        $currencies = new ISOCurrencies();
184 2
        $this->moneyFormatter = new DecimalMoneyFormatter($currencies);
185 2
        $this->workbook = new Spreadsheet();
186 2
        $this->workbook->setActiveSheetIndex(0);
187 2
    }
188
189
    /**
190
     * @param AbstractModel[] $items
191
     */
192 2
    protected function writeHeaders(Worksheet $sheet, array $items): void
193
    {
194
        // Headers
195 2
        foreach ($this->getHeaders() as $header) {
196
            // Apply width
197 2
            if (isset($header['width'])) {
198 2
                $colDimension = $sheet->getColumnDimensionByColumn($this->column);
199 2
                if ($header['width'] === 'auto') {
200
                    $colDimension->setAutoSize(true);
201
                } else {
202 2
                    $colDimension->setWidth($header['width']);
203
                }
204
            }
205
            // Apply default format
206 2
            if (!isset($header['formats'])) {
207
                $header['formats'] = [self::$headerFormat];
208
            }
209
210 2
            if (isset($header['colspan']) && $header['colspan'] > 1) {
211 1
                $sheet->mergeCellsByColumnAndRow($this->column, $this->row, $this->column + (int) $header['colspan'] - 1, $this->row);
212
            }
213
214 2
            if (isset($header['autofilter'])) {
215 1
                $sheet->setAutoFilterByColumnAndRow($this->column, $this->row, $this->column - 1, $this->row + count($items));
216
            }
217
218 2
            $this->write($sheet, $header['label'], ...$header['formats']);
219
220 2
            if (isset($header['colspan']) && $header['colspan'] > 1) {
221 1
                $this->column += (int) $header['colspan'] - 1;
222
            }
223
        }
224 2
    }
225
226
    /**
227
     * Write the items, one per line, in the body part of the sheet
228
     *
229
     * @param AbstractModel[] $items
230
     */
231
    abstract protected function writeData(Worksheet $sheet, array $items): void;
232
233
    /**
234
     * Write the footer line
235
     *
236
     * @param AbstractModel[] $items
237
     */
238
    abstract protected function writeFooter(Worksheet $sheet, array $items): void;
239
240
    abstract protected function getHeaders(): array;
241
242
    /**
243
     * Write the value and style in the cell selected by `column` and `row` variables and move to next column
244
     *
245
     * @param mixed $value
246
     * @param array[] ...$formats optional list of formats to be applied successively
247
     */
248 2
    protected function write(Worksheet $sheet, $value, array ...$formats): void
249
    {
250 2
        $cell = $sheet->getCellByColumnAndRow($this->column++, $this->row);
251 2
        if ($formats) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $formats of type array<integer,array> is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
252 2
            $style = $cell->getStyle();
253 2
            foreach ($formats as $format) {
254 2
                $style->applyFromArray($format);
255
            }
256
        }
257
258
        // Automatic conversion of date to Excel format
259 2
        if ($value instanceof DateTimeInterface) {
260
            $dateTime = new DateTimeImmutable($value->format('c'));
261
            $value = Date::PHPToExcel($dateTime);
262 2
        } elseif ($value instanceof Money) {
263 1
            $value = $this->moneyFormatter->format($value);
264
        }
265
266 2
        $cell->setValue($value);
267 2
    }
268
269
    /**
270
     * Called by the field resolver or repository to generate a spreadsheet from the query builder
271
     *
272
     * @return string the generated spreadsheet file path
273
     */
274 1
    public function generate(Query $query): string
275
    {
276 1
        $items = $query->getResult();
277
278 1
        $this->workbook->getDefaultStyle()->applyFromArray(self::$defaultFormat);
279 1
        $sheet = $this->workbook->getActiveSheet();
280 1
        $this->row = 1;
281 1
        $this->column = 1;
282 1
        $this->writeHeaders($sheet, $items);
283 1
        ++$this->row;
284 1
        $this->column = 1;
285 1
        $this->writeData($sheet, $items);
286 1
        $this->column = 1;
287 1
        $this->writeFooter($sheet, $items);
288
289 1
        $writer = new Xlsx($this->workbook);
290
291 1
        $tmpFile = bin2hex(random_bytes(16));
292 1
        @mkdir($this->tmpDir);
293 1
        $writer->save($this->tmpDir . '/' . $tmpFile);
294
295 1
        return 'https://' . $this->hostname . '/export/' . $this->routeName . '/' . $tmpFile . '/' . $this->outputFileName;
296
    }
297
298
    /**
299
     * Process the GET query to download previously generated spreasheet on disk
300
     */
301 2
    public function handle(ServerRequestInterface $request): ResponseInterface
302
    {
303
        // Read file from disk
304 2
        $tmpFile = $this->tmpDir . '/' . $request->getAttribute('key');
305
306 2
        if (!file_exists($tmpFile)) {
307
            return new Response\EmptyResponse(404);
308
        }
309
310 2
        $size = filesize($tmpFile);
311 2
        $output = fopen($tmpFile, 'rb');
312
313 2
        $response = new Response($output, 200, [
0 ignored issues
show
Bug introduced by
It seems like $output can also be of type false; however, parameter $body of Laminas\Diactoros\Response::__construct() does only seem to accept Psr\Http\Message\StreamInterface|resource|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

313
        $response = new Response(/** @scrutinizer ignore-type */ $output, 200, [
Loading history...
314 2
            'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
315 2
            'Content-Disposition' => sprintf('attachment; filename=%s', $request->getAttribute('name')),
316 2
            'Access-Control-Expose-Headers' => 'Content-Disposition',
317 2
            'Expire' => 0,
318 2
            'Pragma' => 'public',
319 2
            'Content-Length' => $size,
320
        ]);
321
322 2
        return $response;
323
    }
324
}
325