Completed
Push — master ( 91f756...eb84ec )
by Adrien
13:07
created

WorksheetManager::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 19
ccs 9
cts 9
cp 1
rs 9.6333
c 0
b 0
f 0
cc 1
nc 1
nop 8
crap 1

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
namespace Box\Spout\Writer\XLSX\Manager;
4
5
use Box\Spout\Common\Entity\Cell;
6
use Box\Spout\Common\Entity\Row;
7
use Box\Spout\Common\Entity\Style\Style;
8
use Box\Spout\Common\Exception\InvalidArgumentException;
9
use Box\Spout\Common\Exception\IOException;
10
use Box\Spout\Common\Helper\Escaper\XLSX as XLSXEscaper;
11
use Box\Spout\Common\Helper\StringHelper;
12
use Box\Spout\Common\Manager\OptionsManagerInterface;
13
use Box\Spout\Writer\Common\Creator\InternalEntityFactory;
14
use Box\Spout\Writer\Common\Entity\Options;
15
use Box\Spout\Writer\Common\Entity\Worksheet;
16
use Box\Spout\Writer\Common\Helper\CellHelper;
17
use Box\Spout\Writer\Common\Manager\RegisteredStyle;
18
use Box\Spout\Writer\Common\Manager\RowManager;
19
use Box\Spout\Writer\Common\Manager\Style\StyleMerger;
20
use Box\Spout\Writer\Common\Manager\WorksheetManagerInterface;
21
use Box\Spout\Writer\XLSX\Manager\Style\StyleManager;
22
23
/**
24
 * Class WorksheetManager
25
 * XLSX worksheet manager, providing the interfaces to work with XLSX worksheets.
26
 */
27
class WorksheetManager implements WorksheetManagerInterface
28
{
29
    /**
30
     * Maximum number of characters a cell can contain
31
     * @see https://support.office.com/en-us/article/Excel-specifications-and-limits-16c69c74-3d6a-4aaf-ba35-e6eb276e8eaa [Excel 2007]
32
     * @see https://support.office.com/en-us/article/Excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3 [Excel 2010]
33
     * @see https://support.office.com/en-us/article/Excel-specifications-and-limits-ca36e2dc-1f09-4620-b726-67c00b05040f [Excel 2013/2016]
34
     */
35
    const MAX_CHARACTERS_PER_CELL = 32767;
36
37
    const SHEET_XML_FILE_HEADER = <<<'EOD'
38
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
39
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
40
EOD;
41
42
    /** @var bool Whether inline or shared strings should be used */
43
    protected $shouldUseInlineStrings;
44
45
    /** @var RowManager Manages rows */
46
    private $rowManager;
47
48
    /** @var StyleManager Manages styles */
49
    private $styleManager;
50
51
    /** @var StyleMerger Helper to merge styles together */
52
    private $styleMerger;
53
54
    /** @var SharedStringsManager Helper to write shared strings */
55
    private $sharedStringsManager;
56
57
    /** @var XLSXEscaper Strings escaper */
58
    private $stringsEscaper;
59
60
    /** @var StringHelper String helper */
61
    private $stringHelper;
62
63
    /** @var InternalEntityFactory Factory to create entities */
64
    private $entityFactory;
65
66
    /**
67
     * WorksheetManager constructor.
68
     *
69
     * @param OptionsManagerInterface $optionsManager
70
     * @param RowManager $rowManager
71
     * @param StyleManager $styleManager
72
     * @param StyleMerger $styleMerger
73
     * @param SharedStringsManager $sharedStringsManager
74
     * @param XLSXEscaper $stringsEscaper
75
     * @param StringHelper $stringHelper
76
     * @param InternalEntityFactory $entityFactory
77 42
     */
78
    public function __construct(
79
        OptionsManagerInterface $optionsManager,
80
        RowManager $rowManager,
81
        StyleManager $styleManager,
82
        StyleMerger $styleMerger,
83
        SharedStringsManager $sharedStringsManager,
84
        XLSXEscaper $stringsEscaper,
85
        StringHelper $stringHelper,
86
        InternalEntityFactory $entityFactory
87 42
    ) {
88 42
        $this->shouldUseInlineStrings = $optionsManager->getOption(Options::SHOULD_USE_INLINE_STRINGS);
89 42
        $this->rowManager = $rowManager;
90 42
        $this->styleManager = $styleManager;
91 42
        $this->styleMerger = $styleMerger;
92 42
        $this->sharedStringsManager = $sharedStringsManager;
93 42
        $this->stringsEscaper = $stringsEscaper;
94 42
        $this->stringHelper = $stringHelper;
95 42
        $this->entityFactory = $entityFactory;
96
    }
97
98
    /**
99
     * @return SharedStringsManager
100 38
     */
101
    public function getSharedStringsManager()
102 38
    {
103
        return $this->sharedStringsManager;
104
    }
105
106
    /**
107
     * {@inheritdoc}
108 42
     */
109
    public function startSheet(Worksheet $worksheet)
110 42
    {
111 42
        $sheetFilePointer = \fopen($worksheet->getFilePath(), 'w');
112
        $this->throwIfSheetFilePointerIsNotAvailable($sheetFilePointer);
113 42
114
        $worksheet->setFilePointer($sheetFilePointer);
115 42
116 42
        \fwrite($sheetFilePointer, self::SHEET_XML_FILE_HEADER);
117 42
        \fwrite($sheetFilePointer, '<sheetData>');
118
    }
119
120
    /**
121
     * Checks if the sheet has been sucessfully created. Throws an exception if not.
122
     *
123
     * @param bool|resource $sheetFilePointer Pointer to the sheet data file or FALSE if unable to open the file
124
     * @throws IOException If the sheet data file cannot be opened for writing
125
     * @return void
126 42
     */
127
    private function throwIfSheetFilePointerIsNotAvailable($sheetFilePointer)
128 42
    {
129
        if (!$sheetFilePointer) {
130
            throw new IOException('Unable to open sheet for writing.');
131 42
        }
132
    }
133
134
    /**
135
     * {@inheritdoc}
136 35
     */
137
    public function addRow(Worksheet $worksheet, Row $row)
138 35
    {
139 35
        if (!$this->rowManager->isEmpty($row)) {
140
            $this->addNonEmptyRow($worksheet, $row);
141
        }
142 34
143 34
        $worksheet->setLastWrittenRowIndex($worksheet->getLastWrittenRowIndex() + 1);
144
    }
145
146
    /**
147
     * Adds non empty row to the worksheet.
148
     *
149
     * @param Worksheet $worksheet The worksheet to add the row to
150
     * @param Row $row The row to be written
151
     * @throws IOException If the data cannot be written
152
     * @throws InvalidArgumentException If a cell value's type is not supported
153
     * @return void
154 35
     */
155
    private function addNonEmptyRow(Worksheet $worksheet, Row $row)
156 35
    {
157 35
        $rowStyle = $row->getStyle();
158 35
        $rowIndexOneBased = $worksheet->getLastWrittenRowIndex() + 1;
159
        $numCells = $row->getNumCells();
160 35
161
        $rowXML = '<row r="' . $rowIndexOneBased . '" spans="1:' . $numCells . '">';
162 35
163 35
        foreach ($row->getCells() as $columnIndexZeroBased => $cell) {
164
            $registeredStyle = $this->applyStyleAndRegister($cell, $rowStyle);
165
            $cellStyle = $registeredStyle->getStyle();
166 34
            if ($registeredStyle->isMatchingRowStyle()) {
167
                $rowStyle = $cellStyle; // Replace actual rowStyle (possibly with null id) by registered style (with id)
168 34
            }
169 34
            $rowXML .= $this->getCellXML($rowIndexOneBased, $columnIndexZeroBased, $cell, $cellStyle->getId());
170
        }
171
172 34
        $rowXML .= '</row>';
173
174
        $wasWriteSuccessful = \fwrite($worksheet->getFilePointer(), $rowXML);
175
        if ($wasWriteSuccessful === false) {
176
            throw new IOException("Unable to write data in {$worksheet->getFilePath()}");
177
        }
178
    }
179
180
    /**
181
     * Applies styles to the given style, merging the cell's style with its row's style
182
     *
183
     * @param Cell  $cell
184
     * @param Style $rowStyle
185
     *
186 35
     * @throws InvalidArgumentException If the given value cannot be processed
187
     * @return RegisteredStyle
188
     */
189 35
    private function applyStyleAndRegister(Cell $cell, Style $rowStyle) : RegisteredStyle
190 35
    {
191 35
        $isMatchingRowStyle = false;
192
        if ($cell->getStyle()->isEmpty()) {
193 35
            $cell->setStyle($rowStyle);
194
195 35
            $possiblyUpdatedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell);
196
197
            if ($possiblyUpdatedStyle->isUpdated()) {
198
                $registeredStyle = $this->styleManager->registerStyle($possiblyUpdatedStyle->getStyle());
199
            } else {
200
                $registeredStyle = $this->styleManager->registerStyle($rowStyle);
201
                $isMatchingRowStyle = true;
202
            }
203
        } else {
204
            $mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle);
205
            $cell->setStyle($mergedCellAndRowStyle);
206
207
            $possiblyUpdatedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell);
208
209 35
            if ($possiblyUpdatedStyle->isUpdated()) {
210
                $newCellStyle = $possiblyUpdatedStyle->getStyle();
211 35
            } else {
212 35
                $newCellStyle = $mergedCellAndRowStyle;
213 35
            }
214
215 35
            $registeredStyle = $this->styleManager->registerStyle($newCellStyle);
216 32
        }
217 6
218 1
        return new RegisteredStyle($registeredStyle, $isMatchingRowStyle);
219 6
    }
220 2
221 5
    /**
222
     * Builds and returns xml for a single cell.
223 1
     *
224 4
     * @param int  $rowIndexOneBased
225 2
     * @param int  $columnIndexZeroBased
226 1
     * @param Cell $cell
227
     * @param int  $styleId
228
     *
229
     * @throws InvalidArgumentException If the given value cannot be processed
230 2
     * @return string
231
     */
232
    private function getCellXML($rowIndexOneBased, $columnIndexZeroBased, Cell $cell, $styleId)
233 2
    {
234
        $columnLetters = CellHelper::getColumnLettersFromColumnIndex($columnIndexZeroBased);
235
        $cellXML = '<c r="' . $columnLetters . $rowIndexOneBased . '"';
236 34
        $cellXML .= ' s="' . $styleId . '"';
237
238
        if ($cell->isString()) {
239
            $cellXML .= $this->getCellXMLFragmentForNonEmptyString($cell->getValue());
240
        } elseif ($cell->isBoolean()) {
241
            $cellXML .= ' t="b"><v>' . (int) ($cell->getValue()) . '</v></c>';
242
        } elseif ($cell->isNumeric()) {
243
            $cellXML .= '><v>' . $cell->getValue() . '</v></c>';
244
        } elseif ($cell->isError() && is_string($cell->getValueEvenIfError())) {
245
            // only writes the error value if it's a string
246 32
            $cellXML .= ' t="e"><v>' . $cell->getValueEvenIfError() . '</v></c>';
247
        } elseif ($cell->isEmpty()) {
248 32
            if ($this->styleManager->shouldApplyStyleOnEmptyCell($styleId)) {
249
                $cellXML .= '/>';
250
            } else {
251
                // don't write empty cells that do no need styling
252 32
                // NOTE: not appending to $cellXML is the right behavior!!
253 29
                $cellXML = '';
254
            }
255 3
        } else {
256 3
            throw new InvalidArgumentException('Trying to add a value with an unsupported type: ' . \gettype($cell->getValue()));
257
        }
258
259 32
        return $cellXML;
260
    }
261
262
    /**
263
     * Returns the XML fragment for a cell containing a non empty string
264
     *
265 38
     * @param string $cellValue The cell value
266
     * @throws InvalidArgumentException If the string exceeds the maximum number of characters allowed per cell
267 38
     * @return string The XML fragment representing the cell
268
     */
269 38
    private function getCellXMLFragmentForNonEmptyString($cellValue)
270
    {
271
        if ($this->stringHelper->getStringLength($cellValue) > self::MAX_CHARACTERS_PER_CELL) {
272
            throw new InvalidArgumentException('Trying to add a value that exceeds the maximum number of characters allowed in a cell (32,767)');
273 38
        }
274 38
275 38
        if ($this->shouldUseInlineStrings) {
276 38
            $cellXMLFragment = ' t="inlineStr"><is><t>' . $this->stringsEscaper->escape($cellValue) . '</t></is></c>';
277
        } else {
278
            $sharedStringId = $this->sharedStringsManager->writeString($cellValue);
279
            $cellXMLFragment = ' t="s"><v>' . $sharedStringId . '</v></c>';
280
        }
281
282
        return $cellXMLFragment;
283
    }
284
285
    /**
286
     * {@inheritdoc}
287
     */
288
    public function close(Worksheet $worksheet)
289
    {
290
        $worksheetFilePointer = $worksheet->getFilePointer();
291
292
        if (!\is_resource($worksheetFilePointer)) {
293
            return;
294
        }
295
296
        \fwrite($worksheetFilePointer, '</sheetData>');
297
        \fwrite($worksheetFilePointer, '</worksheet>');
298
        \fclose($worksheetFilePointer);
299
    }
300
}
301