Failed Conditions
Push — master ( 4dad36...cd91a2 )
by Adrien
13:00
created

AbstractExcel::generate()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 22
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 1

Importance

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

294
        $response = new Response(/** @scrutinizer ignore-type */ $output, 200, [
Loading history...
295 1
            'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
296 1
            'Content-Disposition' => sprintf('attachment; filename=%s', $request->getAttribute('name')),
297 1
            'Access-Control-Expose-Headers' => 'Content-Disposition',
298 1
            'Expire' => 0,
299 1
            'Pragma' => 'public',
300 1
            'Content-Length' => $size,
301
        ]);
302
303 1
        return $response;
304
    }
305
}
306