Passed
Push — master ( 047bc4...a2fa3c )
by Sylvain
06:10
created

AbstractExcel::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 9
ccs 7
cts 7
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Application\Service\Exporter;
6
7
use DateTimeImmutable;
8
use DateTimeInterface;
9
use Money\Currencies\ISOCurrencies;
10
use Money\Formatter\DecimalMoneyFormatter;
11
use Money\Money;
12
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
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\Conditional;
18
use PhpOffice\PhpSpreadsheet\Style\Fill;
19
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
20
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
21
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
22
23
/**
24
 * @template T of \Application\Model\AbstractModel|array
25
 *
26
 * @extends AbstractExporter<T>
27
 */
28
abstract class AbstractExcel extends AbstractExporter
29
{
30
    /**
31
     * Zebra striping of data rows.
32
     */
33
    protected bool $zebra = true;
34
35
    /**
36
     * Enable auto filter on the headers.
37
     */
38
    protected bool $autoFilter = true;
39
40
    /**
41
     * Column of current cell we are writing in.
42
     */
43
    protected int $column = 1;
44
45
    /**
46
     * Row of current cell we are writing in.
47
     */
48
    protected int $row = 1;
49
50
    /**
51
     * Index of first column containing data.
52
     */
53
    protected int $firstDataColumn = 1;
54
55
    /**
56
     * Index of first row containing data.
57
     */
58
    protected int $firstDataRow = 1;
59
60
    /**
61
     * Index of last column containing data.
62
     */
63
    protected int $lastDataColumn = 1;
64
65
    /**
66
     * Index of last row containing data.
67
     */
68
    protected int $lastDataRow = 1;
69
70
    private readonly Spreadsheet $workbook;
71
72
    protected Worksheet $sheet;
73
74
    protected DecimalMoneyFormatter $moneyFormatter;
75
76
    protected static array $dateFormat = [
77
        'numberFormat' => ['formatCode' => NumberFormat::FORMAT_DATE_XLSX14],
78
    ];
79
80
    protected static array $defaultFormat = [
81
        'font' => ['size' => 11],
82
        'alignment' => ['vertical' => Alignment::VERTICAL_CENTER],
83
    ];
84
85
    protected static array $titleFormat = [
86
        'font' => ['size' => 14],
87
    ];
88
89
    protected static array $headerFormat = [
90
        'font' => ['bold' => true, 'color' => ['argb' => 'FFEAEAEA']],
91
        'alignment' => ['wrapText' => true],
92
        'fill' => [
93
            'fillType' => Fill::FILL_SOLID,
94
            'startColor' => [
95
                'argb' => 'FF666666',
96
            ],
97
        ],
98
    ];
99
100
    protected static array $zebraFormat = [
101
        'fill' => [
102
            'fillType' => Fill::FILL_SOLID,
103
            'startColor' => [
104
                'argb' => 'FFE6E6E6',
105
            ],
106
            'endColor' => [
107
                'argb' => 'FFE6E6E6',
108
            ],
109
        ],
110
    ];
111
112
    protected static array $totalFormat = [
113
        'font' => ['bold' => true],
114
        'alignment' => ['wrapText' => true],
115
        'fill' => [
116
            'fillType' => Fill::FILL_SOLID,
117
            'startColor' => [
118
                'argb' => 'FFDDDDDD',
119
            ],
120
        ],
121
    ];
122
123
    protected static array $centerFormat = [
124
        'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER],
125
    ];
126
127
    protected static array $rightFormat = [
128
        'alignment' => ['horizontal' => Alignment::HORIZONTAL_RIGHT],
129
    ];
130
131
    protected static array $leftFormat = [
132
        'alignment' => ['horizontal' => Alignment::HORIZONTAL_LEFT],
133
    ];
134
135
    protected static array $wrapFormat = [
136
        'alignment' => ['wrapText' => true],
137
    ];
138
139
    protected static array $inactiveFormat = [
140
        'font' => ['color' => ['argb' => 'FFC0C0C0']],
141
    ];
142
143
    /**
144
     * Define border cells inside list of data (very light borders).
145
     */
146
    protected static array $bordersInside = [
147
        'borders' => [
148
            'inside' => [
149
                'borderStyle' => Border::BORDER_HAIR,
150
            ],
151
            'outline' => [
152
                'borderStyle' => Border::BORDER_MEDIUM,
153
            ],
154
        ],
155
    ];
156
157
    /**
158
     * Define border cells for total row (thick border).
159
     */
160
    protected static array $bordersTotal = [
161
        'borders' => [
162
            'outline' => [
163
                'borderStyle' => Border::BORDER_THICK,
164
            ],
165
        ],
166
    ];
167
168
    protected static array $bordersBottom = [
169
        'borders' => [
170
            'bottom' => [
171
                'borderStyle' => Border::BORDER_MEDIUM,
172
            ],
173
        ],
174
    ];
175
176
    protected static array $bordersBottomLight = [
177
        'borders' => [
178
            'bottom' => [
179
                'borderStyle' => Border::BORDER_HAIR,
180
            ],
181
        ],
182
    ];
183
184
    /**
185
     * Constructor.
186
     */
187 2
    public function __construct(string $hostname)
188
    {
189 2
        parent::__construct($hostname);
190 2
        $currencies = new ISOCurrencies();
191 2
        $this->moneyFormatter = new DecimalMoneyFormatter($currencies);
192 2
        $this->workbook = new Spreadsheet();
0 ignored issues
show
Bug introduced by
The property workbook is declared read-only in Application\Service\Exporter\AbstractExcel.
Loading history...
193 2
        $this->sheet = $this->workbook->getActiveSheet();
194
195 2
        $this->sheet->getDefaultRowDimension()->setRowHeight(20);
196
    }
197
198 1
    protected function writeTitle(): void
199
    {
200 1
    }
201
202
    protected function writeFooter(): void
203
    {
204
    }
205
206 2
    public function writeHeaders(array $headers): void
207
    {
208
        // Headers
209 2
        foreach ($headers as $header) {
210
            // Apply width
211 2
            if (isset($header['width'])) {
212 2
                $colDimension = $this->sheet->getColumnDimensionByColumn($this->column);
213 2
                if ($header['width'] === 'auto') {
214
                    $colDimension->setAutoSize(true);
215
                } else {
216 2
                    $colDimension->setWidth($header['width']);
217
                }
218
            }
219
            // Apply default format
220 2
            if (!isset($header['formats'])) {
221 1
                $header['formats'] = [self::$headerFormat];
222
            }
223
224 2
            if (isset($header['colspan']) && $header['colspan'] > 1) {
225 1
                $this->sheet->mergeCells([$this->column, $this->row, $this->column + (int) $header['colspan'] - 1, $this->row]);
226
            }
227
228 2
            $this->write($header['label'], ...$header['formats']);
229
230 2
            if (isset($header['colspan']) && $header['colspan'] > 1) {
231 1
                $this->column += (int) $header['colspan'] - 1;
232
            }
233
        }
234
    }
235
236
    /**
237
     * Write the value and style in the cell selected by `column` and `row` variables and move to next column.
238
     *
239
     * @param mixed $value
240
     * @param array[] ...$formats optional list of formats to be applied successively
241
     */
242 2
    protected function write($value, array ...$formats): void
243
    {
244 2
        $cell = $this->sheet->getCell([$this->column++, $this->row]);
245 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...
246 2
            $style = $cell->getStyle();
247 2
            foreach ($formats as $format) {
248 2
                $style->applyFromArray($format);
249
            }
250
        }
251
252
        // Automatic conversion of date to Excel format
253 2
        if ($value instanceof DateTimeInterface) {
254
            $dateTime = new DateTimeImmutable($value->format('c'));
255
            $value = Date::PHPToExcel($dateTime);
256 2
        } elseif ($value instanceof Money) {
257 1
            $value = $this->moneyFormatter->format($value);
258
        }
259
260 2
        $cell->setValue($value);
261
    }
262
263 2
    protected function initialize(string $path): void
264
    {
265 2
        $this->workbook->getDefaultStyle()->applyFromArray(self::$defaultFormat);
266 2
        $this->row = 1;
267 2
        $this->column = 1;
268 2
        $this->writeTitle();
269 2
        $this->column = 1;
270 2
        $this->firstDataRow = $this->row;
271 2
        $this->firstDataColumn = $this->column;
272
    }
273
274 2
    protected function finalize(string $path): void
275
    {
276 2
        $this->lastDataRow = $this->row - 1;
277
278 2
        $this->applyZebra();
279 2
        $this->applyAutoFilter();
280
281 2
        $this->column = 1;
282 2
        $this->row = $this->lastDataRow + 1;
283 2
        $this->writeFooter();
284
285 2
        $writer = new Xlsx($this->workbook);
286 2
        $writer->save($path);
287
    }
288
289 2
    protected function getExtension(): string
290
    {
291 2
        return 'xlsx';
292
    }
293
294 2
    private function applyZebra(): void
295
    {
296 2
        if (!$this->zebra) {
297 1
            return;
298
        }
299
300 1
        $zebraRange = Coordinate::stringFromColumnIndex($this->firstDataColumn) . $this->firstDataRow . ':' . Coordinate::stringFromColumnIndex($this->lastDataColumn) . $this->lastDataRow;
301 1
        $zebraCondition = new Conditional();
302 1
        $zebraCondition->setConditionType(Conditional::CONDITION_EXPRESSION)->setOperatorType(Conditional::OPERATOR_EQUAL)->addCondition('MOD(ROW(),2)=0');
303 1
        $zebraCondition->getStyle()->applyFromArray(self::$zebraFormat);
304 1
        $this->sheet->getStyle($zebraRange)->setConditionalStyles([$zebraCondition]);
305
    }
306
307 2
    private function applyAutoFilter(): void
308
    {
309 2
        if ($this->autoFilter) {
310 1
            $this->sheet->setAutoFilter([$this->firstDataColumn, $this->firstDataRow - 1, $this->lastDataColumn, $this->lastDataRow]);
311
        }
312
    }
313
}
314