Failed Conditions
Push — master ( c4208e...4b96fe )
by Sylvain
09:02
created

AbstractExcel::write()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
eloc 9
nc 4
nop 3
dl 0
loc 17
c 0
b 0
f 0
cc 4
ccs 0
cts 14
cp 0
crap 20
rs 9.9666
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) {
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...
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, [
0 ignored issues
show
Bug introduced by
It seems like $output can also be of type false; however, parameter $body of Zend\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

307
        $response = new Response(/** @scrutinizer ignore-type */ $output, 200, [
Loading history...
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