Failed Conditions
Push — master ( 4b335d...ccf9db )
by Sylvain
07:33
created

AbstractExcel   A

Complexity

Total Complexity 19

Size/Duplication

Total Lines 297
Duplicated Lines 0 %

Test Coverage

Coverage 92.86%

Importance

Changes 0
Metric Value
wmc 19
eloc 72
c 0
b 0
f 0
dl 0
loc 297
ccs 65
cts 70
cp 0.9286
rs 10

5 Methods

Rating   Name   Duplication   Size   Complexity  
A write() 0 19 5
A __construct() 0 8 1
A handle() 0 22 2
A generate() 0 22 1
B writeHeaders() 0 32 10
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 2
        $initialColumn = $this->column;
0 ignored issues
show
Unused Code introduced by
The assignment to $initialColumn is dead and can be removed.
Loading history...
195
196
        // Headers
197 2
        foreach ($this->getHeaders() as $header) {
198
            // Apply width
199 2
            if (isset($header['width'])) {
200 2
                $colDimension = $sheet->getColumnDimensionByColumn($this->column);
201 2
                if ($header['width'] === 'auto') {
202
                    $colDimension->setAutoSize(true);
203
                } else {
204 2
                    $colDimension->setWidth($header['width']);
205
                }
206
            }
207
            // Apply default format
208 2
            if (!isset($header['formats'])) {
209
                $header['formats'] = [self::$headerFormat];
210
            }
211
212 2
            if (isset($header['colspan']) && $header['colspan'] > 1) {
213 1
                $sheet->mergeCellsByColumnAndRow($this->column, $this->row, $this->column + (int) $header['colspan'] - 1, $this->row);
214
            }
215
216 2
            if (isset($header['autofilter'])) {
217 1
                $sheet->setAutoFilterByColumnAndRow($this->column, $this->row, $this->column - 1, $this->row + count($items));
218
            }
219
220 2
            $this->write($sheet, $header['label'], ...$header['formats']);
221
222 2
            if (isset($header['colspan']) && $header['colspan'] > 1) {
223 1
                $this->column += (int) $header['colspan'] - 1;
224
            }
225
        }
226 2
    }
227
228
    /**
229
     * Write the items, one per line, in the body part of the sheet
230
     *
231
     * @param AbstractModel[] $items
232
     */
233
    abstract protected function writeData(Worksheet $sheet, array $items): void;
234
235
    /**
236
     * Write the footer line
237
     *
238
     * @param AbstractModel[] $items
239
     */
240
    abstract protected function writeFooter(Worksheet $sheet, array $items): void;
241
242
    abstract protected function getHeaders(): array;
243
244
    /**
245
     * Write the value and style in the cell selected by `column` and `row` variables and move to next column
246
     *
247
     * @param mixed $value
248
     * @param array[] ...$formats optional list of formats to be applied successively
249
     */
250 2
    protected function write(Worksheet $sheet, $value, array ...$formats): void
251
    {
252 2
        $cell = $sheet->getCellByColumnAndRow($this->column++, $this->row);
253 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...
254 2
            $style = $cell->getStyle();
255 2
            foreach ($formats as $format) {
256 2
                $style->applyFromArray($format);
257
            }
258
        }
259
260
        // Automatic conversion of date to Excel format
261 2
        if ($value instanceof DateTimeInterface) {
262
            $dateTime = new DateTimeImmutable($value->format('c'));
263
            $value = Date::PHPToExcel($dateTime);
264 2
        } elseif ($value instanceof Money) {
265 1
            $value = $this->moneyFormatter->format($value);
266
        }
267
268 2
        $cell->setValue($value);
269 2
    }
270
271
    /**
272
     * Called by the field resolver or repository to generate a spreadsheet from the query builder
273
     *
274
     * @return string the generated spreadsheet file path
275
     */
276 1
    public function generate(Query $query): string
277
    {
278 1
        $items = $query->getResult();
279
280 1
        $this->workbook->getDefaultStyle()->applyFromArray(self::$defaultFormat);
281 1
        $sheet = $this->workbook->getActiveSheet();
282 1
        $this->row = 1;
283 1
        $this->column = 1;
284 1
        $this->writeHeaders($sheet, $items);
285 1
        ++$this->row;
286 1
        $this->column = 1;
287 1
        $this->writeData($sheet, $items);
288 1
        $this->column = 1;
289 1
        $this->writeFooter($sheet, $items);
290
291 1
        $writer = new Xlsx($this->workbook);
292
293 1
        $tmpFile = bin2hex(random_bytes(16));
294 1
        @mkdir($this->tmpDir);
295 1
        $writer->save($this->tmpDir . '/' . $tmpFile);
296
297 1
        return 'https://' . $this->hostname . '/export/' . $this->routeName . '/' . $tmpFile . '/' . $this->outputFileName;
298
    }
299
300
    /**
301
     * Process the GET query to download previously generated spreasheet on disk
302
     */
303 2
    public function handle(ServerRequestInterface $request): ResponseInterface
304
    {
305
        // Read file from disk
306 2
        $tmpFile = $this->tmpDir . '/' . $request->getAttribute('key');
307
308 2
        if (!file_exists($tmpFile)) {
309
            return new Response\EmptyResponse(404);
310
        }
311
312 2
        $size = filesize($tmpFile);
313 2
        $output = fopen($tmpFile, 'rb');
314
315 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

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