Workbook   F
last analyzed

Complexity

Total Complexity 69

Size/Duplication

Total Lines 716
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 14

Importance

Changes 44
Bugs 6 Features 7
Metric Value
wmc 69
c 44
b 6
f 7
lcom 1
cbo 14
dl 0
loc 716
rs 2.7586

39 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 12 1
A addDefaultFormats() 0 12 1
A setCreationTimestamp() 0 4 1
B save() 0 40 3
A storeDrawings() 0 18 3
A storeSupbookInternal() 0 4 1
A closeSheets() 0 6 2
A storeSheets() 0 13 2
A calcSheetRecordsTotalSize() 0 10 2
A getWorksheets() 0 4 1
A getSheetsCount() 0 4 1
A setCountry() 0 4 1
B addWorksheet() 0 36 4
A checkSheetName() 0 9 2
A setActiveSheetIndex() 0 11 3
A getActiveSheetIndex() 0 4 1
A getFirstSheetIndex() 0 4 1
A setFirstSheetIndex() 0 4 1
A hasSheet() 0 4 1
A addFormat() 0 8 1
A addValidator() 0 4 1
A setCustomColor() 0 9 1
A saveOleFile() 0 16 2
A storeAllFonts() 0 6 2
B getFonts() 0 31 3
B storeAllNumFormats() 0 24 4
A storeAllXfs() 0 18 3
A storeAllStyles() 0 4 1
A storeCountry() 0 6 2
A storeDefinedNames() 0 5 1
A storePrintAreaNames() 0 18 3
A getRangeCommonHeader() 0 4 1
A storePrintTitleNames() 0 6 2
A storePrintTitleName() 0 13 2
B getPrintTitleData() 0 25 3
A storeWindow1() 0 12 1
A storeExternsheet() 0 4 1
A storeDatemode() 0 4 1
A storeSharedStringsTable() 0 8 2

How to fix   Complexity   

Complex Class

Complex classes like Workbook often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Workbook, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace Xls;
3
4
use Xls\OLE\PpsFile;
5
use Xls\OLE\PpsRoot;
6
7
/**
8
 * Class for generating Excel Spreadsheets
9
*/
10
11
class Workbook extends BIFFwriter
12
{
13
    const COUNTRY_NONE = -1;
14
    const COUNTRY_USA = 1;
15
16
    const BOF_TYPE = 0x0005;
17
18
    /**
19
     * Formula parser
20
     * @var FormulaParser
21
     */
22
    protected $formulaParser;
23
24
    /**
25
     * The active worksheet of the workbook (0 indexed)
26
     * @var integer
27
     */
28
    protected $activeSheetIndex = 0;
29
30
    /**
31
     * 1st displayed worksheet in the workbook (0 indexed)
32
     * @var integer
33
     */
34
    protected $firstSheetIndex = 0;
35
36
    /**
37
     * Index for creating adding new formats to the workbook
38
     * 15 style XF's and 1 cell XF
39
     * @var integer
40
     */
41
    protected $xfIndex = 16;
42
43
    /**
44
     * Flag for preventing close from being called twice.
45
     * @var boolean
46
     * @see close()
47
     */
48
    protected $saved = false;
49
50
    /**
51
     * The default XF format.
52
     * @var Format
53
     */
54
    protected $tmpFormat;
55
56
    /**
57
     * Array containing references to all of this workbook's worksheets
58
     * @var Worksheet[]
59
     */
60
    protected $worksheets = array();
61
62
    /**
63
     * Array of sheetnames for creating the EXTERNSHEET records
64
     * @var array
65
     */
66
    protected $sheetNames = array();
67
68
    /**
69
     * Array containing references to all of this workbook's formats
70
     * @var Format[]
71
     */
72
    protected $formats = array();
73
74
    /**
75
     * Array containing the colour palette
76
     * @var array
77
     */
78
    protected $palette = array();
79
80
    /**
81
     * The default format for URLs.
82
     * @var Format
83
     */
84
    protected $urlFormat;
85
86
    /**
87
     * The country code used for localization
88
     * @var integer
89
     */
90
    protected $countryCode = self::COUNTRY_NONE;
91
92
    /**
93
     * @var int
94
     */
95
    protected $creationTimestamp;
96
97
    /**
98
     * @var SharedStringsTable
99
     */
100
    protected $sst;
101
102
    /**
103
     *
104
     */
105
    public function __construct()
106
    {
107
        $this->formulaParser = new FormulaParser();
108
109
        $this->palette = Palette::getXl97Palette();
110
111
        $this->addDefaultFormats();
112
113
        $this->sst = new SharedStringsTable();
114
115
        $this->setCreationTimestamp(time());
116
    }
117
118
    /**
119
     *
120
     */
121
    protected function addDefaultFormats()
122
    {
123
        $this->tmpFormat = new Format();
124
125
        // Add the default format for hyperlinks
126
        $this->urlFormat = $this->addFormat(
127
            array(
128
                'font.color' => 'blue',
129
                'font.underline' => Font::UNDERLINE_ONCE
130
            )
131
        );
132
    }
133
134
    /**
135
     * @param int $creationTime
136
     */
137
    public function setCreationTimestamp($creationTime)
138
    {
139
        $this->creationTimestamp = $creationTime;
140
    }
141
142
    /**
143
     * Assemble worksheets into a workbook and send the BIFF data to an OLE
144
     * storage.
145
     * This method should always be the last one to be called on every workbook
146
     *
147
     * @param string $filePath File path to save
148
     *
149
     * @throws \Exception
150
     */
151
    public function save($filePath)
152
    {
153
        if ($this->saved) {
154
            throw new \Exception('Workbook was already saved!');
155
        }
156
157
        if (count($this->worksheets) == 0) {
158
            throw new \Exception('Cannot save workbook with no sheets');
159
        }
160
161
        $this->appendRecord('Bof', array(static::BOF_TYPE));
162
        $this->appendRecord('Codepage', array(Biff8::CODEPAGE));
163
        $this->storeWindow1();
164
        $this->storeDatemode();
165
166
        $this->storeAllFonts();
167
        $this->storeAllNumFormats();
168
        $this->storeAllXfs();
169
        $this->storeAllStyles();
170
        $this->appendRecord('Palette', array($this->palette));
171
172
        $this->startBufferedWrite();
173
        $this->storeCountry();
174
        $this->appendRecord('RecalcId');
175
        $this->storeSupbookInternal();
176
        $this->storeExternsheet();
177
        $this->storeDefinedNames();
178
        $this->storeDrawings();
179
        $this->storeSharedStringsTable();
180
        $this->appendRecord('Eof');
181
        $this->endBufferedWrite();
182
183
        $this->storeSheets();
184
185
        $this->appendRaw($this->getBuffer());
186
187
        $this->saveOleFile($filePath);
188
189
        $this->saved = true;
190
    }
191
192
    protected function storeDrawings()
193
    {
194
        $totalDrawings = 0;
195
        foreach ($this->getWorksheets() as $sheet) {
196
            $totalDrawings += count($sheet->getDrawings());
197
        }
198
199
        if ($totalDrawings == 0) {
200
            return;
201
        }
202
203
        $data = '0F 00 00 F0 52 00 00 00 00 00 06 F0 18 00 00 00';
204
        $data .= '01 08';
205
        $data .= '00 00 02 00 00 00 03 00 00 00 01 00 00 00 01 00 00 00 03 00 00 00 33 00 0B F0 12 00 00 00 BF 00 08';
206
        $data .= ' 00 08 00 81 01 41 00 00 08 C0 01 40 00 00 08 40 00 1E F1 10 00 00 00 0D 00 00 08 0C 00 00 08 17 00';
207
        $data .= ' 00 08 F7 00 00 10';
208
        $this->appendRecord('MsoDrawingGroup', array($data));
209
    }
210
211
    /**
212
     * Write Internal SUPBOOK record
213
     */
214
    protected function storeSupbookInternal()
215
    {
216
        $this->appendRecord('ExternalBook', array($this->getSheetsCount()));
217
    }
218
219
    /**
220
     * Calculate the number of selected worksheet tabs and call the finalization
221
     * methods for each worksheet
222
     */
223
    protected function closeSheets()
224
    {
225
        foreach ($this->getWorksheets() as $sheet) {
226
            $sheet->close();
227
        }
228
    }
229
230
    /**
231
     *
232
     */
233
    protected function storeSheets()
234
    {
235
        $this->closeSheets();
236
237
        $offset = $this->getDataSize();
238
        $offset += $this->calcSheetRecordsTotalSize();
239
        $offset += $this->getBufferSize();
240
241
        foreach ($this->getWorksheets() as $sheet) {
242
            $this->appendRecord('Sheet', array($sheet->getName(), $offset));
243
            $offset += $sheet->getDataSize();
244
        }
245
    }
246
247
    /**
248
     * @return int
249
     */
250
    protected function calcSheetRecordsTotalSize()
251
    {
252
        $size = 0;
253
        foreach ($this->worksheets as $sheet) {
254
            $recordData = $this->getRecord('Sheet', array($sheet->getName()));
255
            $size += strlen($recordData);
256
        }
257
258
        return $size;
259
    }
260
261
    /**
262
     * Returns an array of the worksheet objects in a workbook
263
     *
264
     * @return Worksheet[]
265
     */
266
    public function getWorksheets()
267
    {
268
        return $this->worksheets;
269
    }
270
271
    /**
272
     * @return int
273
     */
274
    public function getSheetsCount()
275
    {
276
        return count($this->worksheets);
277
    }
278
279
    /**
280
     * Set the country identifier for the workbook
281
     *
282
     * @param integer $code Is the international calling country code for the
283
     *                      chosen country.
284
     */
285
    public function setCountry($code)
286
    {
287
        $this->countryCode = $code;
288
    }
289
290
    /**
291
     * Add a new worksheet to the Excel workbook.
292
     * If no name is given the name of the worksheet will be Sheeti$i, with
293
     * $i in [1..].
294
     *
295
     * @param string $name the optional name of the worksheet
296
     * @throws \Exception
297
     * @return Worksheet
298
     */
299
    public function addWorksheet($name = '')
300
    {
301
        $index = count($this->worksheets);
302
303
        if ($name == '') {
304
            $name = 'Sheet' . ($index + 1);
305
        }
306
307
        $this->checkSheetName($name);
308
309
        if ($this->hasSheet($name)) {
310
            throw new \Exception("Worksheet '$name' already exists");
311
        }
312
313
        $worksheet = new Worksheet(
314
            $name,
315
            $index,
316
            $this,
317
            $this->sst,
318
            $this->urlFormat,
319
            $this->formulaParser
320
        );
321
322
        $this->worksheets[$index] = $worksheet;
323
        $this->sheetNames[$index] = $name;
324
325
        if (count($this->worksheets) == 1) {
326
            $this->setActiveSheetIndex(0);
327
        }
328
329
        // Register worksheet name with parser
330
        $this->formulaParser->addSheet($name, $index);
331
        $this->formulaParser->addRef($index, $index);
332
333
        return $worksheet;
334
    }
335
336
    /**
337
     * @param string $name
338
     *
339
     * @return string
340
     * @throws \Exception
341
     */
342
    protected function checkSheetName($name)
343
    {
344
        $maxLen = Biff8::MAX_SHEET_NAME_LENGTH;
345
        if (strlen($name) > $maxLen) {
346
            throw new \Exception(
347
                "Sheet name must be shorter than $maxLen chars"
348
            );
349
        }
350
    }
351
352
    /**
353
     * @param int $sheetIndex
354
     */
355
    public function setActiveSheetIndex($sheetIndex)
356
    {
357
        $this->activeSheetIndex = $sheetIndex;
358
        foreach ($this->worksheets as $idx => $sheet) {
359
            if ($idx == $sheetIndex) {
360
                $sheet->select();
361
            } else {
362
                $sheet->unselect();
363
            }
364
        }
365
    }
366
367
    /**
368
     * @return int
369
     */
370
    public function getActiveSheetIndex()
371
    {
372
        return $this->activeSheetIndex;
373
    }
374
375
    /**
376
     * @return int
377
     */
378
    public function getFirstSheetIndex()
379
    {
380
        return $this->firstSheetIndex;
381
    }
382
383
    /**
384
     * @param int $firstSheetIndex
385
     */
386
    public function setFirstSheetIndex($firstSheetIndex)
387
    {
388
        $this->firstSheetIndex = $firstSheetIndex;
389
    }
390
391
    /**
392
     * @param string $name
393
     *
394
     * @return bool
395
     */
396
    public function hasSheet($name)
397
    {
398
        return in_array($name, $this->sheetNames, true);
399
    }
400
401
    /**
402
     * Add a new format to the Excel workbook.
403
     * Also, pass any properties to the Format constructor.
404
     *
405
     * @param array $properties array with properties for initializing the format.
406
     * @return Format reference to an Excel Format
407
     */
408
    public function addFormat($properties = array())
409
    {
410
        $format = new Format($this->xfIndex, $properties);
411
        $this->xfIndex++;
412
        $this->formats[] = $format;
413
414
        return $format;
415
    }
416
417
    /**
418
     * Create new validator.
419
     *
420
     * @return Validator reference to a Validator
421
     */
422
    public function addValidator()
423
    {
424
        return new Validator($this->formulaParser);
425
    }
426
427
    /**
428
     * Change the RGB components of the elements in the colour palette.
429
     *
430
     * @param integer $index colour index
431
     * @param integer $red   red RGB value [0-255]
432
     * @param integer $green green RGB value [0-255]
433
     * @param integer $blue  blue RGB value [0-255]
434
     * @throws \Exception
435
     *
436
     * @return integer The palette index for the custom color
437
     */
438
    public function setCustomColor($index, $red, $green, $blue)
439
    {
440
        Palette::validateColor($index, $red, $green, $blue);
441
442
        // Set the RGB value, adjust colour index (wingless dragonfly)
443
        $this->palette[$index - 8] = array($red, $green, $blue, 0);
444
445
        return $index;
446
    }
447
448
    /**
449
     * Store the workbook in an OLE container
450
     * @param string $filePath
451
     */
452
    protected function saveOleFile($filePath)
453
    {
454
        $ole = new PpsFile(Biff8::WORKBOOK_NAME);
455
        $ole->append($this->data);
456
457
        foreach ($this->worksheets as $sheet) {
458
            $ole->append($sheet->getData());
459
        }
460
461
        $root = new PpsRoot(
462
            $this->creationTimestamp,
463
            array($ole)
464
        );
465
466
        $root->save($filePath);
467
    }
468
469
    /**
470
     * Store the Excel FONT records.
471
     */
472
    protected function storeAllFonts()
473
    {
474
        foreach ($this->getFonts() as $font) {
475
            $this->appendRecord('Font', array($font));
476
        }
477
    }
478
479
    /**
480
     * @return Font[]
481
     */
482
    protected function getFonts()
483
    {
484
        $fontsMap = array();
485
486
        $defaultFont = $this->tmpFormat->getFont();
487
        $defaultFont->setIndex(0);
488
489
        $key = $defaultFont->getKey();
490
        $fontsMap[$key] = 1;
491
492
        //add default font for 5 times
493
        $fonts = array_fill(0, 5, $defaultFont);
494
495
        // Iterate through the XF objects and write a FONT record if it isn't the
496
        // same as the default FONT and if it hasn't already been used.
497
        $index = 6; // The first user defined FONT
498
        foreach ($this->formats as $format) {
499
            $font = $format->getFont();
500
            $key = $font->getKey();
501
502
            if (!isset($fontsMap[$key])) {
503
                // Add a new FONT record
504
                $fontsMap[$key] = 1;
505
                $font->setIndex($index);
506
                $fonts[] = $font;
507
                $index++;
508
            }
509
        }
510
511
        return $fonts;
512
    }
513
514
    /**
515
     * Store user defined numerical formats i.e. FORMAT records
516
     */
517
    protected function storeAllNumFormats()
518
    {
519
        $map = array();
520
        $index = 164;
521
522
        // Iterate through the XF objects and write a FORMAT record if it isn't a
523
        // built-in format type and if the FORMAT string hasn't already been used.
524
        foreach ($this->formats as $format) {
525
            $numFormat = $format->getNumFormat();
526
527
            if (NumberFormat::isBuiltIn($numFormat)) {
528
                $format->setNumFormatIndex($numFormat);
529
                continue;
530
            }
531
532
            if (!isset($map[$numFormat])) {
533
                // Add a new FORMAT
534
                $map[$numFormat] = 1;
535
                $format->setNumFormatIndex($index);
536
                $this->appendRecord('Format', array($numFormat, $index));
537
                $index++;
538
            }
539
        }
540
    }
541
542
    /**
543
     * Write all XF records.
544
     */
545
    protected function storeAllXfs()
546
    {
547
        // tmpFormat is added by the constructor. We use this to write the default XF's
548
        // The default font index is 0
549
        for ($i = 0; $i <= 14; $i++) {
550
            $xfRecord = $this->tmpFormat->getXf('style');
551
            $this->append($xfRecord);
552
        }
553
554
        $xfRecord = $this->tmpFormat->getXf('cell');
555
        $this->append($xfRecord);
556
557
        // User defined XFs
558
        foreach ($this->formats as $format) {
559
            $xfRecord = $format->getXf('cell');
560
            $this->append($xfRecord);
561
        }
562
    }
563
564
    /**
565
     * Write all STYLE records.
566
     */
567
    protected function storeAllStyles()
568
    {
569
        $this->appendRecord('Style');
570
    }
571
572
    /**
573
     *
574
     */
575
    protected function storeCountry()
576
    {
577
        if ($this->countryCode != self::COUNTRY_NONE) {
578
            $this->appendRecord('Country', array($this->countryCode));
579
        }
580
    }
581
582
    /**
583
     * Write the NAME record to define the print area and the repeat rows and cols.
584
     */
585
    protected function storeDefinedNames()
586
    {
587
        $this->storePrintAreaNames();
588
        $this->storePrintTitleNames();
589
    }
590
591
    /**
592
     * Create the print area NAME records
593
     */
594
    protected function storePrintAreaNames()
595
    {
596
        foreach ($this->worksheets as $sheet) {
597
            $printSetup = $sheet->getPrintSetup();
598
            if ($printSetup->isPrintAreaSet()) {
599
                $area = $printSetup->getPrintArea();
600
601
                $data = $this->getRangeCommonHeader($sheet);
602
                $data .= \Xls\Subrecord\Range::getData(array($area), false);
0 ignored issues
show
Documentation introduced by
array($area) is of type array<integer,null|objec...l|object<Xls\\Range>"}>, but the function expects a array<integer,object<Xls\Range>>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
603
604
                $this->appendRecord('DefinedName', array(
605
                    Record\DefinedName::BUILTIN_PRINT_AREA,
606
                    $sheet->getIndex() + 1,
607
                    $data
608
                ));
609
            }
610
        }
611
    }
612
613
    protected function getRangeCommonHeader(Worksheet $sheet)
614
    {
615
        return pack('Cv', 0x3B, $sheet->getIndex());
616
    }
617
618
    /**
619
     * Create the print title NAME records
620
     */
621
    protected function storePrintTitleNames()
622
    {
623
        foreach ($this->worksheets as $sheet) {
624
            $this->storePrintTitleName($sheet);
625
        }
626
    }
627
628
    /**
629
     * @param Worksheet $sheet
630
     */
631
    protected function storePrintTitleName(Worksheet $sheet)
632
    {
633
        $printRepeat = $sheet->getPrintSetup()->getPrintRepeat();
634
        if ($printRepeat->isEmpty()) {
635
            return;
636
        }
637
638
        $this->appendRecord('DefinedName', array(
639
            Record\DefinedName::BUILTIN_PRINT_TITLES,
640
            $sheet->getIndex() + 1,
641
            $this->getPrintTitleData($sheet)
642
        ));
643
    }
644
645
    /**
646
     * @param Worksheet $sheet
647
     *
648
     * @return string
649
     */
650
    protected function getPrintTitleData(Worksheet $sheet)
651
    {
652
        $printRepeat = $sheet->getPrintSetup()->getPrintRepeat();
653
654
        $rowmin = $printRepeat->getRowFrom();
655
        $rowmax = $printRepeat->getRowTo();
656
        $colmin = $printRepeat->getColFrom();
657
        $colmax = $printRepeat->getColTo();
658
659
        $rangeHeader = $this->getRangeCommonHeader($sheet);
660
661
        if ($rowmax !== Biff8::MAX_ROW_IDX && $colmax !== Biff8::MAX_COL_IDX) {
662
            $data = pack('Cv', 0x29, 0x17); // tMemFunc
663
            $data .= $rangeHeader;
664
            $data .= pack('v4', 0, Biff8::MAX_ROW_IDX, $colmin, $colmax); // tArea3d
665
            $data .= $rangeHeader;
666
            $data .= pack('v4', $rowmin, $rowmax, 0, Biff8::MAX_COL_IDX); // tArea3d
667
            $data .= pack('C', 0x10); // tList
668
        } else {
669
            $data = $rangeHeader;
670
            $data .= pack('v4', $rowmin, $rowmax, $colmin, $colmax);
671
        }
672
673
        return $data;
674
    }
675
676
    /**
677
     * Write Excel BIFF WINDOW1 record.
678
     */
679
    protected function storeWindow1()
680
    {
681
        $selectedSheetsCount = 1;
682
        $this->appendRecord(
683
            'Window1',
684
            array(
685
                $selectedSheetsCount,
686
                $this->firstSheetIndex,
687
                $this->activeSheetIndex
688
            )
689
        );
690
    }
691
692
    /**
693
     * Writes the Excel BIFF EXTERNSHEET record. These references are used by
694
     * formulas.
695
     */
696
    protected function storeExternsheet()
697
    {
698
        $this->appendRecord('Externsheet', array($this->formulaParser->getReferences()));
699
    }
700
701
    /**
702
     * Write DATEMODE record to indicate the date system in use (1904 or 1900)
703
     * Flag for 1904 date system (0 => base date is 1900, 1 => base date is 1904)
704
     */
705
    protected function storeDatemode()
706
    {
707
        $this->appendRecord('Datemode', array(0));
708
    }
709
710
    /**
711
     * Write all of the workbooks strings into an indexed array.
712
     *
713
     * The Excel documentation says that the SST record should be followed by an
714
     * EXTSST record. The EXTSST record is a hash table that is used to optimise
715
     * access to SST. However, despite the documentation it doesn't seem to be
716
     * required so we will ignore it.
717
     */
718
    protected function storeSharedStringsTable()
719
    {
720
        $this->appendRecord('SharedStringsTable', array($this->sst));
721
722
        foreach ($this->sst->getDataForWrite() as $item) {
723
            $this->append($item);
724
        }
725
    }
726
}
727