LibXLWriter::getMetadataSpecs()   F
last analyzed

Complexity

Conditions 21
Paths 433

Size

Total Lines 124

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 462

Importance

Changes 0
Metric Value
dl 0
loc 124
ccs 0
cts 98
cp 0
rs 0.63
c 0
b 0
f 0
cc 21
nc 433
nop 1
crap 462

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/*
4
 * soluble-flexstore library
5
 *
6
 * @author    Vanvelthem Sébastien
7
 * @link      https://github.com/belgattitude/soluble-flexstore
8
 * @copyright Copyright (c) 2016-2017 Vanvelthem Sébastien
9
 * @license   MIT License https://github.com/belgattitude/soluble-flexstore/blob/master/LICENSE.md
10
 *
11
 */
12
13
namespace Soluble\FlexStore\Writer\Excel;
14
15
use Soluble\FlexStore\Writer\AbstractSendableWriter;
16
use Soluble\FlexStore\Writer\Exception;
17
use Soluble\FlexStore\Writer\Excel\Exception\ExtensionNotLoadedException;
18
use Soluble\Spreadsheet\Library\LibXL;
19
use Soluble\FlexStore\Writer\Http\SimpleHeaders;
20
use Soluble\FlexStore\Column\ColumnType;
21
use Soluble\FlexStore\Options;
22
use Soluble\FlexStore\Store\StoreInterface;
23
use ExcelBook;
24
use ExcelFormat;
25
use ArrayObject;
26
27
class LibXLWriter extends AbstractSendableWriter
28
{
29
    /**
30
     * Cache for currency formats.
31
     *
32
     * @var ArrayObject
33
     */
34
    protected $currency_formats;
35
36
    /**
37
     * Cache for unit formats.
38
     *
39
     * @var ArrayObject
40
     */
41
    protected $unit_formats;
42
43
    /**
44
     * @var SimpleHeaders
45
     */
46
    protected $headers;
47
    protected $column_width_multiplier = 1.7;
48
    protected $column_max_width = 75;
49
50
    /**
51
     * @var array
52
     */
53
    protected static $default_license;
54
55
    /**
56
     * @var ExcelBook
57
     */
58
    protected $excelBook;
59
60
    /**
61
     * @var string
62
     */
63
    protected $file_format = LibXL::FILE_FORMAT_XLSX;
64
65
    /**
66
     * @var array
67
     */
68
    protected $currencyMap = [
69
        'EUR' => '€',
70
        'GBP' => '£',
71
        'CNY' => 'CN¥',
72
        'USD' => '$',
73
        'CAD' => 'CA$'
74
    ];
75
76
    /**
77
     * @var array
78
     */
79
    protected $typeMap = [
80
        ColumnType::TYPE_BIT => 'number',
81
        ColumnType::TYPE_BLOB => 'text',
82
        ColumnType::TYPE_BOOLEAN => 'number',
83
        ColumnType::TYPE_DATE => 'date',
84
        ColumnType::TYPE_DATETIME => 'datetime',
85
        ColumnType::TYPE_DECIMAL => 'number',
86
        ColumnType::TYPE_INTEGER => 'number',
87
        ColumnType::TYPE_STRING => 'text',
88
        ColumnType::TYPE_TIME => 'text',
89
    ];
90
91
    /**
92
     * @param StoreInterface|null     $store
93
     * @param array|\Traversable|null $options
94
     */
95
    public function __construct(StoreInterface $store = null, $options = null)
96
    {
97
        $this->currency_formats = new ArrayObject();
98
        $this->unit_formats = new ArrayObject();
99
        parent::__construct($store, $options);
100
    }
101
102
    /**
103
     * Set file format (xls, xlsx), default is xlsx.
104
     *
105
     * @param string $file_format
106
     *
107
     * @throws Exception\InvalidArgumentException
108
     */
109
    public function setFormat($file_format): self
110
    {
111
        if (!LibXL::isSupportedFormat($file_format)) {
112
            throw new Exception\InvalidArgumentException(__METHOD__ . " Unsupported format given '$file_format'");
113
        }
114
        $this->file_format = $file_format;
115
116
        return $this;
117
    }
118
119
    /**
120
     * @param ExcelBook $book
121
     * @param string    $currency
122
     * @param int       $decimals
123
     *
124
     * @return ExcelFormat
125
     */
126
    protected function getCurrencyFormat(ExcelBook $book, $currency, $decimals)
127
    {
128
        $id = "$currency/$decimals";
129
        if (!$this->currency_formats->offsetExists($id)) {
130
            if (array_key_exists($currency, $this->currencyMap)) {
131
                $symbol = $this->currencyMap[$currency];
132
            } else {
133
                $symbol = $currency;
134
            }
135
136
            $formatString = '#,##0';
137
138
            if ($decimals > 0) {
139
                $zeros = str_repeat('0', $decimals);
140
                $formatString = $formatString . '.' . $zeros;
141
            }
142
            $formatString = $formatString . ' "' . $symbol . '"_-';
143
144
            //$formatString = $formatString . ' [$€-80C]_-';
145
            //$format_code = '"C$"' . $format_code . '_-';
146
147
            $cfid = $book->addCustomFormat($formatString);
148
            $format = $book->addFormat();
149
            $format->numberFormat($cfid);
150
            $this->currency_formats->offsetSet($id, $format);
151
        }
152
153
        return $this->currency_formats->offsetGet($id);
154
    }
155
156
    /**
157
     * @param ExcelBook $book
158
     * @param string    $unit
159
     * @param int       $decimals
160
     *
161
     * @return ExcelFormat
162
     */
163
    protected function getUnitFormat(ExcelBook $book, $unit, $decimals)
164
    {
165
        $id = "$unit/$decimals";
166
        if (!$this->unit_formats->offsetExists($id)) {
167
            $symbol = $unit;
168
169
            $formatString = '#,##0';
170
171
            if ($decimals > 0) {
172
                $zeros = str_repeat('0', $decimals);
173
                $formatString = $formatString . '.' . $zeros;
174
            }
175
            $formatString = $formatString . ' "' . $symbol . '"_-';
176
177
            $cfid = $book->addCustomFormat($formatString);
178
            $format = $book->addFormat();
179
            $format->numberFormat($cfid);
180
            $this->unit_formats->offsetSet($id, $format);
181
        }
182
183
        return $this->unit_formats->offsetGet($id);
184
    }
185
186
    /**
187
     * @throws ExtensionNotLoadedException
188
     * @throws Exception\InvalidArgumentException
189
     *
190
     * @param string $file_format
191
     * @param string $locale      default to en_US.UTF-8
192
     *
193
     * @return ExcelBook
194
     */
195
    public function getExcelBook($file_format = null, $locale = 'en_US.UTF-8')
196
    {
197
        if (!extension_loaded('excel')) {
198
            throw new ExtensionNotLoadedException(__METHOD__ . ' LibXLWriter requires excel (php_exccel) extension to be loaded');
199
        }
200
201
        if ($this->excelBook === null) {
202
            $libXL = new LibXL();
203
            if (is_array(self::$default_license)) {
204
                $libXL->setLicense(self::$default_license);
205
            }
206
            if ($file_format === null) {
207
                $file_format = $this->file_format;
208
            } elseif (!LibXL::isSupportedFormat($file_format)) {
209
                throw new Exception\InvalidArgumentException(__METHOD__ . " Unsupported format given '$file_format'");
210
            }
211
212
            $this->excelBook = $libXL->getExcelBook($file_format, $locale);
213
        }
214
215
        return $this->excelBook;
216
    }
217
218
    /**
219
     * @param Options $options
220
     *
221
     * @return string
222
     */
223
    public function getData(Options $options = null)
224
    {
225
        if ($options === null) {
226
            // Take store global/default options
227
            $options = $this->store->getOptions();
228
        }
229
230
        // Get unformatted data when using excel writer
231
        $options->getHydrationOptions()->disableFormatters();
232
233
        // Some colu
234
        $options->getHydrationOptions()->disableColumnExclusion();
235
236
        $book = $this->getExcelBook();
237
        $this->generateExcel($book, $options);
238
        //$book->setLocale($locale);
239
240
        $temp_dir = sys_get_temp_dir();
241
        if (!is_dir($temp_dir)) {
242
            throw new \Exception(__METHOD__ . " System temporary directory '$temp_dir' does not exists.");
243
        }
244
245
        if (!is_writable($temp_dir)) {
246
            throw new \Exception(__METHOD__ . " System temporary directory '$temp_dir' is not writable.");
247
        }
248
249
        $filename = tempnam($temp_dir, 'libxl');
250
251
        try {
252
            $book->save($filename);
253
        } catch (\Exception $e) {
254
            unlink($filename);
255
            throw $e;
256
        }
257
258
        $data = file_get_contents($filename);
259
        unlink($filename);
260
261
        return $data;
262
    }
263
264
    /**
265
     * @return ArrayObject
266
     */
267
    protected function getMetadataSpecs(ExcelBook $book)
268
    {
269
        $hide_thousands_separator = true;
270
271
        $specs = new ArrayObject();
272
        $cm = $this->store->getColumnModel();
273
        $metadata = $cm->getMetadata();
274
275
        $columns = $cm->getColumns();
276
        foreach ($columns as $name => $column) {
277
            $decimals = null;
278
            $format = null;
279
            $custom_column = null;
280
            $formatter = $column->getFormatter();
281
282
            if ($formatter instanceof \Soluble\FlexStore\Formatter\FormatterNumberInterface) {
283
                $type = 'number';
284
                $decimals = $formatter->getDecimals();
285
286
                if ($formatter instanceof \Soluble\FlexStore\Formatter\CurrencyFormatter) {
287
                    $currency = $formatter->getCurrencyCode();
288
                    if ($currency instanceof \Soluble\FlexStore\Formatter\RowColumn) {
289
                        // TODO better handling of callbacks
290
                        $format = function (ExcelBook $book, $currency, $decimals) {
291
                            return $this->getCurrencyFormat($book, $currency, $decimals);
292
                        };
293
                        $custom_column = $currency->getColumnName();
294
                    } else {
295
                        $format = $this->getCurrencyFormat($book, $currency, $decimals);
296
                    }
297
                } elseif ($formatter instanceof \Soluble\FlexStore\Formatter\UnitFormatter) {
298
                    $unit = $formatter->getUnit();
299
                    if ($unit instanceof \Soluble\FlexStore\Formatter\RowColumn) {
300
                        // TODO better handling of callbacks
301
                        $format = function (ExcelBook $book, $unit, $decimals) {
302
                            return $this->getUnitFormat($book, $unit, $decimals);
303
                        };
304
                        $custom_column = $unit->getColumnName();
305
                    } else {
306
                        $format = $this->getUnitFormat($book, $unit, $decimals);
307
                    }
308
                }
309
            } else {
310
                $model_type = $column->getType()->getName();
311
                //$spec['meta_type'] = $model_type;
312
                if ($model_type == ColumnType::TYPE_INTEGER) {
313
                    $decimals = 0;
314
                }
315
                if (array_key_exists($model_type, $this->typeMap)) {
316
                    $type = $this->typeMap[$model_type];
317
                } else {
318
                    $type = 'text';
319
                }
320
            }
321
322
            // We now have the type
323
            if ($type === 'number' && $decimals === null && $metadata !== null && $metadata->offsetExists($name)) {
324
                // try to guess from metadata
325
                $decimals = $metadata->offsetGet($name)->getNumericPrecision();
326
                if (!$decimals) {
327
                    $decimals = 0;
328
                }
329
            }
330
331
            // Let's make the format
332
333
            if ($format === null) {
334
                switch ($type) {
335
                    case 'date':
336
                        $mask = 'd/mm/yyyy';
337
                        $cfid = $book->addCustomFormat($mask);
338
                        $format = $book->addFormat();
339
                        $format->numberFormat($cfid);
340
                        break;
341
                    case 'datetime':
342
                        $mask = 'd/mm/yyyy h:mm';
343
                        $cfid = $book->addCustomFormat($mask);
344
                        $format = $book->addFormat();
345
                        $format->numberFormat($cfid);
346
                        break;
347
                    case 'number':
348
                        if ($hide_thousands_separator) {
349
                            $formatString = '0';
350
                        } else {
351
                            $formatString = '#,##0';
352
                        }
353
                        if ($decimals > 0) {
354
                            $zeros = str_repeat('0', $decimals);
355
                            $formatString = $formatString . '.' . $zeros;
356
                        }
357
                        $cfid = $book->addCustomFormat($formatString);
358
                        $format = $book->addFormat();
359
                        $format->numberFormat($cfid);
360
361
                        break;
362
                    default:
363
                        $format = null;
364
                }
365
            }
366
367
            if ($format === null) {
368
                $format = $this->getDefaultTextFormat($book);
369
            } else {
370
                /* Not yet supported, waiting php_excel 1.1
371
                $format->horizontalAlign($this->getFormatStyle('horizontalAlign'));
372
                $format->verticalAlign($this->getFormatStyle('verticalAlign'));
373
374
 */
375
            }
376
377
            // Save the spec
378
            $spec = new ArrayObject();
379
            $spec['name'] = $name;
380
            $spec['header'] = $column->getHeader();
381
            $spec['type'] = $type;
382
            $spec['decimals'] = $decimals;
383
            $spec['format'] = $format;
384
            $spec['custom_column'] = $custom_column;
385
            $specs->offsetSet($name, $spec);
386
        }
387
388
        //var_dump((array) $specs);
389
        return $specs;
390
    }
391
392
    protected function getDefaultTextFormat(ExcelBook $book)
393
    {
394
        $format = $book->addFormat();
395
        /* Not yet supported, waiting php_excel 1.1
396
                $format->horizontalAlign($this->getFormatStyle('horizontalFormat'));
397
                $format->verticalAlign($this->getFormatStyle('verticalFormat'));
398
         *
399
         */
400
        return $format;
401
    }
402
403
    protected function getHeaderFormat(ExcelBook $book)
404
    {
405
        // Font selection
406
        $headerFont = $book->addFont();
407
        $headerFont->name('Tahoma');
408
        $headerFont->size(12);
409
        $headerFont->color(ExcelFormat::COLOR_WHITE);
410
411
        $headerFormat = $book->addFormat();
412
413
        $headerFormat->setFont($headerFont);
414
415
        $headerFormat->borderStyle(ExcelFormat::BORDERSTYLE_THIN);
416
        $headerFormat->verticalAlign(ExcelFormat::ALIGNV_CENTER);
417
        $headerFormat->borderColor(ExcelFormat::COLOR_GRAY50);
418
        //$headerFormat->patternBackgroundColor(ExcelFormat:COLOR_LIGHTBLUE);
419
        $headerFormat->patternForegroundColor(ExcelFormat::COLOR_LIGHTBLUE);
420
        $headerFormat->fillPattern(ExcelFormat::FILLPATTERN_SOLID);
421
422
        return $headerFormat;
423
    }
424
425
    /**
426
     * @param ExcelBook $book
427
     * @param Options   $options
428
     *
429
     * @return ExcelBook
430
     */
431
    protected function generateExcel(ExcelBook $book, Options $options = null)
432
    {
433
        $sheet = $book->addSheet('Sheet');
434
        $headerFormat = $this->getHeaderFormat($book);
435
436
        // Step 1, print header
437
        $specs = $this->getMetadataSpecs($book);
438
        $column_max_widths = array_fill_keys(array_keys((array) $specs), 0);
439
        $col_idx = 0;
440
        foreach ($specs as $name => $spec) {
441
            $sheet->write($row = 0, $col_idx, $spec['header'], $headerFormat);
442
            $column_max_widths[$name] = max(strlen($spec['header']) * $this->column_width_multiplier, $column_max_widths[$name]);
443
            ++$col_idx;
444
        }
445
446
        $sheet->setRowHeight(0, 30);
447
448
        // Fix the header
449
        $sheet->splitSheet(1, 0);
450
451
        // Fill document content
452
453
        $data = $this->store->getData($options);
454
455
        foreach ($data as $idx => $row) {
456
            $col_idx = 0;
457
            $row_idx = $idx + 1;
458
            $rowHeight = $sheet->rowHeight($row_idx);
459
460
            foreach ($specs as $name => $spec) {
461
                // Row may have no key in the case of customer renderers,
462
                // to prevent notice let's make it to null
463
                $value = isset($row[$name]) ? $row[$name] : null;
464
465
                if ($spec['format'] !== null) {
466
                    $format = $spec['format'];
467
                    if (is_callable($format)) {
468
                        // Dynamic column format
469
                        $sheet->write($row_idx, $col_idx, (string) $value, $format($book, $row[$spec['custom_column']], $spec['decimals']), ExcelFormat::AS_NUMERIC_STRING);
470
                    } else {
471
                        switch ($spec['type']) {
472
                            case 'number':
473
                                $sheet->write($row_idx, $col_idx, (string) $value, $spec['format'], ExcelFormat::AS_NUMERIC_STRING);
474
                                break;
475
                            case 'date':
476
                            case 'datetime':
477
                                if ($value != '') {
478
                                    $time = strtotime($value);
479
                                } else {
480
                                    $time = null;
481
                                }
482
                                $sheet->write($row_idx, $col_idx, $time, $spec['format'], ExcelFormat::AS_DATE);
483
                                break;
484
                            default:
485
                                if (preg_match('/(\n)/', $value)) {
486
                                    // More height when one cell contains multiple lines
487
                                    $sheet->setRowHeight($row_idx, ceil($rowHeight * 1.9));
488
                                }
489
490
                                $sheet->write($row_idx, $col_idx, $value, $format);
491
                        }
492
                    }
493
                } else {
494
                    $sheet->write($row_idx, $col_idx, $value);
495
                }
496
                $column_max_widths[$name] = max(strlen((string) $value) * $this->column_width_multiplier, $column_max_widths[$name]);
497
                ++$col_idx;
498
            }
499
        }
500
501
        foreach (array_values($column_max_widths) as $idx => $width) {
502
            $sheet->setColWidth($idx, ceil($idx), min($width, $this->column_max_width));
503
        }
504
505
        $sheet->setPrintGridlines(true);
506
        //$sheet->setPrintRepeatRows(1, 2);
507
        //$sheet->setPrintHeaders(true);
508
        //$sheet->setVerPageBreak($col_idx, true);
509
510
        return $book;
511
    }
512
513
    /**
514
     * @param string $license_name
515
     * @param string $license_key
516
     */
517
    public static function setDefaultLicense($license_name, $license_key)
518
    {
519
        self::$default_license = ['name' => $license_name, 'key' => $license_key];
520
    }
521
522
    public function getFormatStyle($style)
523
    {
524
        $styles = $this->getFormatStyles();
525
526
        return $styles[$style];
527
    }
528
529
    public function getFormatStyles()
530
    {
531
        $styles = [
532
            'horizontalAlign' => ExcelFormat::ALIGNH_LEFT,
533
            'verticalAlign' => ExcelFormat::ALIGNV_TOP
534
        ];
535
536
        return $styles;
537
    }
538
539
    /**
540
     * Return default headers for sending store data via http.
541
     *
542
     * @return SimpleHeaders
543
     */
544
    public function getHttpHeaders()
545
    {
546
        if ($this->headers === null) {
547
            $this->headers = new SimpleHeaders();
548
            $this->headers->setContentType('application/excel', 'utf-8');
549
            $this->headers->setContentDispositionType(SimpleHeaders::DIPOSITION_ATTACHEMENT);
550
        }
551
552
        return $this->headers;
553
    }
554
}
555