Completed
Pull Request — master (#807)
by Adrien
04:34
created

WorksheetManager::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 20
ccs 11
cts 11
cp 1
rs 9.6
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
    /** @var array Locale info, used for number formatting */
67
    private $localeInfo;
68
69
    /**
70
     * WorksheetManager constructor.
71
     *
72
     * @param OptionsManagerInterface $optionsManager
73
     * @param RowManager $rowManager
74
     * @param StyleManager $styleManager
75
     * @param StyleMerger $styleMerger
76
     * @param SharedStringsManager $sharedStringsManager
77
     * @param XLSXEscaper $stringsEscaper
78
     * @param StringHelper $stringHelper
79
     * @param InternalEntityFactory $entityFactory
80
     */
81 43
    public function __construct(
82
        OptionsManagerInterface $optionsManager,
83
        RowManager $rowManager,
84
        StyleManager $styleManager,
85
        StyleMerger $styleMerger,
86
        SharedStringsManager $sharedStringsManager,
87
        XLSXEscaper $stringsEscaper,
88
        StringHelper $stringHelper,
89
        InternalEntityFactory $entityFactory
90
    ) {
91 43
        $this->shouldUseInlineStrings = $optionsManager->getOption(Options::SHOULD_USE_INLINE_STRINGS);
92 43
        $this->rowManager = $rowManager;
93 43
        $this->styleManager = $styleManager;
94 43
        $this->styleMerger = $styleMerger;
95 43
        $this->sharedStringsManager = $sharedStringsManager;
96 43
        $this->stringsEscaper = $stringsEscaper;
97 43
        $this->stringHelper = $stringHelper;
98 43
        $this->entityFactory = $entityFactory;
99 43
        $this->localeInfo = \localeconv();
100 43
    }
101
102
    /**
103
     * @return SharedStringsManager
104
     */
105 39
    public function getSharedStringsManager()
106
    {
107 39
        return $this->sharedStringsManager;
108
    }
109
110
    /**
111
     * {@inheritdoc}
112
     */
113 43
    public function startSheet(Worksheet $worksheet)
114
    {
115 43
        $sheetFilePointer = \fopen($worksheet->getFilePath(), 'w');
116 43
        $this->throwIfSheetFilePointerIsNotAvailable($sheetFilePointer);
117
118 43
        $worksheet->setFilePointer($sheetFilePointer);
119
120 43
        \fwrite($sheetFilePointer, self::SHEET_XML_FILE_HEADER);
121 43
        \fwrite($sheetFilePointer, '<sheetData>');
122 43
    }
123
124
    /**
125
     * Checks if the sheet has been sucessfully created. Throws an exception if not.
126
     *
127
     * @param bool|resource $sheetFilePointer Pointer to the sheet data file or FALSE if unable to open the file
128
     * @throws IOException If the sheet data file cannot be opened for writing
129
     * @return void
130
     */
131 43
    private function throwIfSheetFilePointerIsNotAvailable($sheetFilePointer)
132
    {
133 43
        if (!$sheetFilePointer) {
134
            throw new IOException('Unable to open sheet for writing.');
135
        }
136 43
    }
137
138
    /**
139
     * {@inheritdoc}
140
     */
141 36
    public function addRow(Worksheet $worksheet, Row $row)
142
    {
143 36
        if (!$this->rowManager->isEmpty($row)) {
144 36
            $this->addNonEmptyRow($worksheet, $row);
145
        }
146
147 35
        $worksheet->setLastWrittenRowIndex($worksheet->getLastWrittenRowIndex() + 1);
148 35
    }
149
150
    /**
151
     * Adds non empty row to the worksheet.
152
     *
153
     * @param Worksheet $worksheet The worksheet to add the row to
154
     * @param Row $row The row to be written
155
     * @throws IOException If the data cannot be written
156
     * @throws InvalidArgumentException If a cell value's type is not supported
157
     * @return void
158
     */
159 36
    private function addNonEmptyRow(Worksheet $worksheet, Row $row)
160
    {
161 36
        $rowStyle = $row->getStyle();
162 36
        $rowIndexOneBased = $worksheet->getLastWrittenRowIndex() + 1;
163 36
        $numCells = $row->getNumCells();
164
165 36
        $rowXML = '<row r="' . $rowIndexOneBased . '" spans="1:' . $numCells . '">';
166
167 36
        foreach ($row->getCells() as $columnIndexZeroBased => $cell) {
168 36
            $registeredStyle = $this->applyStyleAndRegister($cell, $rowStyle);
169 36
            $cellStyle = $registeredStyle->getStyle();
170 36
            if ($registeredStyle->isMatchingRowStyle()) {
171 35
                $rowStyle = $cellStyle; // Replace actual rowStyle (possibly with null id) by registered style (with id)
172
            }
173 36
            $rowXML .= $this->getCellXML($rowIndexOneBased, $columnIndexZeroBased, $cell, $cellStyle->getId());
174
        }
175
176 35
        $rowXML .= '</row>';
177
178 35
        $wasWriteSuccessful = \fwrite($worksheet->getFilePointer(), $rowXML);
179 35
        if ($wasWriteSuccessful === false) {
180
            throw new IOException("Unable to write data in {$worksheet->getFilePath()}");
181
        }
182 35
    }
183
184
    /**
185
     * Applies styles to the given style, merging the cell's style with its row's style
186
     *
187
     * @param Cell  $cell
188
     * @param Style $rowStyle
189
     *
190
     * @throws InvalidArgumentException If the given value cannot be processed
191
     * @return RegisteredStyle
192
     */
193 36
    private function applyStyleAndRegister(Cell $cell, Style $rowStyle) : RegisteredStyle
194
    {
195 36
        $isMatchingRowStyle = false;
196 36
        if ($cell->getStyle()->isEmpty()) {
197 35
            $cell->setStyle($rowStyle);
198
199 35
            $possiblyUpdatedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell);
200
201 35
            if ($possiblyUpdatedStyle->isUpdated()) {
202 1
                $registeredStyle = $this->styleManager->registerStyle($possiblyUpdatedStyle->getStyle());
203
            } else {
204 35
                $registeredStyle = $this->styleManager->registerStyle($rowStyle);
205 35
                $isMatchingRowStyle = true;
206
            }
207
        } else {
208 3
            $mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle);
209 3
            $cell->setStyle($mergedCellAndRowStyle);
210
211 3
            $possiblyUpdatedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell);
212
213 3
            if ($possiblyUpdatedStyle->isUpdated()) {
214
                $newCellStyle = $possiblyUpdatedStyle->getStyle();
215
            } else {
216 3
                $newCellStyle = $mergedCellAndRowStyle;
217
            }
218
219 3
            $registeredStyle = $this->styleManager->registerStyle($newCellStyle);
220
        }
221
222 36
        return new RegisteredStyle($registeredStyle, $isMatchingRowStyle);
223
    }
224
225
    /**
226
     * Builds and returns xml for a single cell.
227
     *
228
     * @param int  $rowIndexOneBased
229
     * @param int  $columnIndexZeroBased
230
     * @param Cell $cell
231
     * @param int  $styleId
232
     *
233
     * @throws InvalidArgumentException If the given value cannot be processed
234
     * @return string
235
     */
236 36
    private function getCellXML($rowIndexOneBased, $columnIndexZeroBased, Cell $cell, $styleId)
237
    {
238 36
        $columnLetters = CellHelper::getColumnLettersFromColumnIndex($columnIndexZeroBased);
239 36
        $cellXML = '<c r="' . $columnLetters . $rowIndexOneBased . '"';
240 36
        $cellXML .= ' s="' . $styleId . '"';
241
242 36
        if ($cell->isString()) {
243 32
            $cellXML .= $this->getCellXMLFragmentForNonEmptyString($cell->getValue());
244 7
        } elseif ($cell->isBoolean()) {
245 1
            $cellXML .= ' t="b"><v>' . (int) ($cell->getValue()) . '</v></c>';
246 7
        } elseif ($cell->isNumeric()) {
247 3
            $cellValue = $cell->getValue();
248
            // Formatting of float values is locale dependent. Thousands separators and decimal points
249
            // vary from locale to locale (en_US: 12.34 vs pl_PL: 12,34). However, XLSX values must
250
            // be formatted with no thousands separator and a "." as decimal point to work properly.
251
            // We must then convert the value to the correct format before storing it.
252 3
            if (is_float($cellValue)) {
253 3
                $cellValue = str_replace(
254 3
                    [$this->localeInfo['thousands_sep'], $this->localeInfo['decimal_point']],
255 3
                    ['', '.'],
256
                    $cellValue
257
                );
258
            }
259 3
            $cellXML .= '><v>' . $cellValue . '</v></c>';
260 5
        } elseif ($cell->isError() && is_string($cell->getValueEvenIfError())) {
261
            // only writes the error value if it's a string
262 1
            $cellXML .= ' t="e"><v>' . $cell->getValueEvenIfError() . '</v></c>';
263 4
        } elseif ($cell->isEmpty()) {
264 2
            if ($this->styleManager->shouldApplyStyleOnEmptyCell($styleId)) {
265 1
                $cellXML .= '/>';
266
            } else {
267
                // don't write empty cells that do no need styling
268
                // NOTE: not appending to $cellXML is the right behavior!!
269 2
                $cellXML = '';
270
            }
271
        } else {
272 2
            throw new InvalidArgumentException('Trying to add a value with an unsupported type: ' . \gettype($cell->getValue()));
273
        }
274
275 35
        return $cellXML;
276
    }
277
278
    /**
279
     * Returns the XML fragment for a cell containing a non empty string
280
     *
281
     * @param string $cellValue The cell value
282
     * @throws InvalidArgumentException If the string exceeds the maximum number of characters allowed per cell
283
     * @return string The XML fragment representing the cell
284
     */
285 32
    private function getCellXMLFragmentForNonEmptyString($cellValue)
286
    {
287 32
        if ($this->stringHelper->getStringLength($cellValue) > self::MAX_CHARACTERS_PER_CELL) {
288
            throw new InvalidArgumentException('Trying to add a value that exceeds the maximum number of characters allowed in a cell (32,767)');
289
        }
290
291 32
        if ($this->shouldUseInlineStrings) {
292 29
            $cellXMLFragment = ' t="inlineStr"><is><t>' . $this->stringsEscaper->escape($cellValue) . '</t></is></c>';
293
        } else {
294 3
            $sharedStringId = $this->sharedStringsManager->writeString($cellValue);
295 3
            $cellXMLFragment = ' t="s"><v>' . $sharedStringId . '</v></c>';
296
        }
297
298 32
        return $cellXMLFragment;
299
    }
300
301
    /**
302
     * {@inheritdoc}
303
     */
304 39
    public function close(Worksheet $worksheet)
305
    {
306 39
        $worksheetFilePointer = $worksheet->getFilePointer();
307
308 39
        if (!\is_resource($worksheetFilePointer)) {
309
            return;
310
        }
311
312 39
        \fwrite($worksheetFilePointer, '</sheetData>');
313 39
        \fwrite($worksheetFilePointer, '</worksheet>');
314 39
        \fclose($worksheetFilePointer);
315 39
    }
316
}
317