Completed
Pull Request — master (#807)
by Adrien
15:38 queued 14:14
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 42
    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 42
        $this->shouldUseInlineStrings = $optionsManager->getOption(Options::SHOULD_USE_INLINE_STRINGS);
92 42
        $this->rowManager = $rowManager;
93 42
        $this->styleManager = $styleManager;
94 42
        $this->styleMerger = $styleMerger;
95 42
        $this->sharedStringsManager = $sharedStringsManager;
96 42
        $this->stringsEscaper = $stringsEscaper;
97 42
        $this->stringHelper = $stringHelper;
98 42
        $this->entityFactory = $entityFactory;
99 42
        $this->localeInfo = \localeconv();
100 42
    }
101
102
    /**
103
     * @return SharedStringsManager
104
     */
105 38
    public function getSharedStringsManager()
106
    {
107 38
        return $this->sharedStringsManager;
108
    }
109
110
    /**
111
     * {@inheritdoc}
112
     */
113 42
    public function startSheet(Worksheet $worksheet)
114
    {
115 42
        $sheetFilePointer = \fopen($worksheet->getFilePath(), 'w');
116 42
        $this->throwIfSheetFilePointerIsNotAvailable($sheetFilePointer);
117
118 42
        $worksheet->setFilePointer($sheetFilePointer);
119
120 42
        \fwrite($sheetFilePointer, self::SHEET_XML_FILE_HEADER);
121 42
        \fwrite($sheetFilePointer, '<sheetData>');
122 42
    }
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 42
    private function throwIfSheetFilePointerIsNotAvailable($sheetFilePointer)
132
    {
133 42
        if (!$sheetFilePointer) {
134
            throw new IOException('Unable to open sheet for writing.');
135
        }
136 42
    }
137
138
    /**
139
     * {@inheritdoc}
140
     */
141 35
    public function addRow(Worksheet $worksheet, Row $row)
142
    {
143 35
        if (!$this->rowManager->isEmpty($row)) {
144 35
            $this->addNonEmptyRow($worksheet, $row);
145
        }
146
147 34
        $worksheet->setLastWrittenRowIndex($worksheet->getLastWrittenRowIndex() + 1);
148 34
    }
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 35
    private function addNonEmptyRow(Worksheet $worksheet, Row $row)
160
    {
161 35
        $rowStyle = $row->getStyle();
162 35
        $rowIndexOneBased = $worksheet->getLastWrittenRowIndex() + 1;
163 35
        $numCells = $row->getNumCells();
164
165 35
        $rowXML = '<row r="' . $rowIndexOneBased . '" spans="1:' . $numCells . '">';
166
167 35
        foreach ($row->getCells() as $columnIndexZeroBased => $cell) {
168 35
            $registeredStyle = $this->applyStyleAndRegister($cell, $rowStyle);
169 35
            $cellStyle = $registeredStyle->getStyle();
170 35
            if ($registeredStyle->isMatchingRowStyle()) {
171 34
                $rowStyle = $cellStyle; // Replace actual rowStyle (possibly with null id) by registered style (with id)
172
            }
173 35
            $rowXML .= $this->getCellXML($rowIndexOneBased, $columnIndexZeroBased, $cell, $cellStyle->getId());
174
        }
175
176 34
        $rowXML .= '</row>';
177
178 34
        $wasWriteSuccessful = \fwrite($worksheet->getFilePointer(), $rowXML);
179 34
        if ($wasWriteSuccessful === false) {
180
            throw new IOException("Unable to write data in {$worksheet->getFilePath()}");
181
        }
182 34
    }
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 35
    private function applyStyleAndRegister(Cell $cell, Style $rowStyle) : RegisteredStyle
194
    {
195 35
        $isMatchingRowStyle = false;
196 35
        if ($cell->getStyle()->isEmpty()) {
197 34
            $cell->setStyle($rowStyle);
198
199 34
            $possiblyUpdatedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell);
200
201 34
            if ($possiblyUpdatedStyle->isUpdated()) {
202 1
                $registeredStyle = $this->styleManager->registerStyle($possiblyUpdatedStyle->getStyle());
203
            } else {
204 34
                $registeredStyle = $this->styleManager->registerStyle($rowStyle);
205 34
                $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 35
        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 35
    private function getCellXML($rowIndexOneBased, $columnIndexZeroBased, Cell $cell, $styleId)
237
    {
238 35
        $columnLetters = CellHelper::getColumnLettersFromColumnIndex($columnIndexZeroBased);
239 35
        $cellXML = '<c r="' . $columnLetters . $rowIndexOneBased . '"';
240 35
        $cellXML .= ' s="' . $styleId . '"';
241
242 35
        if ($cell->isString()) {
243 32
            $cellXML .= $this->getCellXMLFragmentForNonEmptyString($cell->getValue());
244 6
        } elseif ($cell->isBoolean()) {
245 1
            $cellXML .= ' t="b"><v>' . (int) ($cell->getValue()) . '</v></c>';
246 6
        } elseif ($cell->isNumeric()) {
247 2
            $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 2
            if (is_float($cellValue)) {
253 2
                $cellValue = str_replace(
254 2
                    [$this->localeInfo['thousands_sep'], $this->localeInfo['decimal_point']],
255 2
                    ['', '.'],
256
                    $cellValue
257
                );
258
            }
259 2
            $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 34
        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 38
    public function close(Worksheet $worksheet)
305
    {
306 38
        $worksheetFilePointer = $worksheet->getFilePointer();
307
308 38
        if (!\is_resource($worksheetFilePointer)) {
309
            return;
310
        }
311
312 38
        \fwrite($worksheetFilePointer, '</sheetData>');
313 38
        \fwrite($worksheetFilePointer, '</worksheet>');
314 38
        \fclose($worksheetFilePointer);
315 38
    }
316
}
317