AbstractExcel::finalize()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 1

Importance

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