Passed
Push — master ( a88e47...c63398 )
by Sylvain
08:44
created

AbstractExcel::write()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4.128

Importance

Changes 0
Metric Value
cc 4
eloc 9
nc 4
nop 3
dl 0
loc 17
rs 9.9666
c 0
b 0
f 0
ccs 8
cts 10
cp 0.8
crap 4.128
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 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 Worksheet $sheet
180
     * @param AbstractModel[] $items
181
     */
182 1
    protected function writeHeaders(Worksheet $sheet, array $items): void
183
    {
184 1
        $initialColumn = $this->column;
185
186
        // Headers
187 1
        foreach ($this->getHeaders() as $header) {
188
            // Apply width
189 1
            if (isset($header['width'])) {
190 1
                $colDimension = $sheet->getColumnDimensionByColumn($this->column);
191 1
                if ($header['width'] === 'auto') {
192 1
                    $colDimension->setAutoSize(true);
193
                } else {
194 1
                    $colDimension->setWidth($header['width']);
195
                }
196
            }
197
            // Apply format
198 1
            if (!isset($header['formats'])) {
199
                $header['formats'] = [];
200
            }
201 1
            $header['formats'] = [-1 => self::$headerFormat] + $header['formats'];
202
203 1
            $this->write($sheet, $header['label'], ...$header['formats']);
204
        }
205
206
        // Apply AutoFilters
207 1
        $sheet->setAutoFilterByColumnAndRow($initialColumn, $this->row, $this->column - 1, $this->row + count($items));
208 1
    }
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 1
    protected function write(Worksheet $sheet, $value, array ...$formats): void
239
    {
240 1
        $cell = $sheet->getCellByColumnAndRow($this->column++, $this->row);
241 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...
242 1
            $style = $cell->getStyle();
243 1
            foreach ($formats as $format) {
244 1
                $style->applyFromArray($format);
245
            }
246
        }
247
248
        // Automatic conversion of date to Excel format
249 1
        if ($value instanceof \DateTimeInterface) {
250
            $dateTime = new \DateTime($value->format('c'));
251
            $value = Date::PHPToExcel($dateTime);
252
        }
253
254 1
        $cell->setValue($value);
255 1
    }
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 1
    public function generate(Query $query): string
265
    {
266 1
        $items = $query->setMaxResults(1000)->getResult();
267
268 1
        $this->workbook->getDefaultStyle()->applyFromArray(self::$defaultFormat);
269 1
        $sheet = $this->workbook->getActiveSheet();
270 1
        $this->row = 1;
271 1
        $this->column = 1;
272 1
        $this->writeHeaders($sheet, $items);
273 1
        ++$this->row;
274 1
        $this->column = 1;
275 1
        $this->writeData($sheet, $items);
276 1
        $this->column = 1;
277 1
        $this->writeFooter($sheet, $items);
278
279 1
        $writer = new Xlsx($this->workbook);
280
281 1
        $tmpFile = bin2hex(random_bytes(16));
282 1
        @mkdir($this->tmpDir);
1 ignored issue
show
Security Best Practice introduced by
It seems like you do not handle an error condition for mkdir(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

282
        /** @scrutinizer ignore-unhandled */ @mkdir($this->tmpDir);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
283 1
        $writer->save($this->tmpDir . '/' . $tmpFile);
284
285 1
        return 'https://' . $this->hostname . '/export/' . $this->routeName . '/' . $tmpFile . '/' . $this->outputFileName;
286
    }
287
288
    /**
289
     * Process the GET query to download previously generated spreasheet on disk
290
     *
291
     * @param ServerRequestInterface $request
292
     * @param RequestHandlerInterface $handler
293
     *
294
     * @return ResponseInterface
295
     */
296 1
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
297
    {
298
        // Read file from disk
299 1
        $tmpFile = $this->tmpDir . '/' . $request->getAttribute('key');
300
301 1
        if (!file_exists($tmpFile)) {
302
            return new Response\EmptyResponse(404);
303
        }
304
305 1
        $size = filesize($tmpFile);
306 1
        $output = fopen($tmpFile, 'r');
307
308 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 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

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