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

WorksheetManager   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 262
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 12

Test Coverage

Coverage 96.23%

Importance

Changes 0
Metric Value
wmc 30
lcom 1
cbo 12
dl 0
loc 262
ccs 102
cts 106
cp 0.9623
rs 10
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 12 1
A startSheet() 0 7 1
A throwIfSheetFilePointerIsNotAvailable() 0 6 2
A getTableElementStartAsString() 0 11 1
B addRow() 0 41 7
A applyStyleAndRegister() 0 30 4
A getCellXMLWithStyle() 0 8 1
C getCellXML() 0 51 11
A close() 0 10 2
1
<?php
2
3
namespace Box\Spout\Writer\ODS\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\ODS as ODSEscaper;
11
use Box\Spout\Common\Helper\StringHelper;
12
use Box\Spout\Writer\Common\Entity\Worksheet;
13
use Box\Spout\Writer\Common\Manager\RegisteredStyle;
14
use Box\Spout\Writer\Common\Manager\Style\StyleMerger;
15
use Box\Spout\Writer\Common\Manager\WorksheetManagerInterface;
16
use Box\Spout\Writer\ODS\Manager\Style\StyleManager;
17
18
/**
19
 * Class WorksheetManager
20
 * ODS worksheet manager, providing the interfaces to work with ODS worksheets.
21
 */
22
class WorksheetManager implements WorksheetManagerInterface
23
{
24
    /** @var \Box\Spout\Common\Helper\Escaper\ODS Strings escaper */
25
    private $stringsEscaper;
26
27
    /** @var StringHelper String helper */
28
    private $stringHelper;
29
30
    /** @var StyleManager Manages styles */
31
    private $styleManager;
32
33
    /** @var StyleMerger Helper to merge styles together */
34
    private $styleMerger;
35
36
    /** @var array Locale info, used for number formatting */
37
    private $localeInfo;
38
39
    /**
40
     * WorksheetManager constructor.
41
     *
42
     * @param StyleManager $styleManager
43
     * @param StyleMerger $styleMerger
44
     * @param ODSEscaper $stringsEscaper
45
     * @param StringHelper $stringHelper
46
     */
47 39
    public function __construct(
48
        StyleManager $styleManager,
49
        StyleMerger $styleMerger,
50
        ODSEscaper $stringsEscaper,
51
        StringHelper $stringHelper
52
    ) {
53 39
        $this->styleManager = $styleManager;
54 39
        $this->styleMerger = $styleMerger;
55 39
        $this->stringsEscaper = $stringsEscaper;
56 39
        $this->stringHelper = $stringHelper;
57 39
        $this->localeInfo = \localeconv();
58 39
    }
59
60
    /**
61
     * Prepares the worksheet to accept data
62
     *
63
     * @param Worksheet $worksheet The worksheet to start
64
     * @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing
65
     * @return void
66
     */
67 39
    public function startSheet(Worksheet $worksheet)
68
    {
69 39
        $sheetFilePointer = \fopen($worksheet->getFilePath(), 'w');
70 39
        $this->throwIfSheetFilePointerIsNotAvailable($sheetFilePointer);
71
72 39
        $worksheet->setFilePointer($sheetFilePointer);
73 39
    }
74
75
    /**
76
     * Checks if the sheet has been sucessfully created. Throws an exception if not.
77
     *
78
     * @param bool|resource $sheetFilePointer Pointer to the sheet data file or FALSE if unable to open the file
79
     * @throws IOException If the sheet data file cannot be opened for writing
80
     * @return void
81
     */
82 39
    private function throwIfSheetFilePointerIsNotAvailable($sheetFilePointer)
83
    {
84 39
        if (!$sheetFilePointer) {
85
            throw new IOException('Unable to open sheet for writing.');
86
        }
87 39
    }
88
89
    /**
90
     * Returns the table XML root node as string.
91
     *
92
     * @param Worksheet $worksheet
93
     * @return string <table> node as string
94
     */
95 36
    public function getTableElementStartAsString(Worksheet $worksheet)
96
    {
97 36
        $externalSheet = $worksheet->getExternalSheet();
98 36
        $escapedSheetName = $this->stringsEscaper->escape($externalSheet->getName());
99 36
        $tableStyleName = 'ta' . ($externalSheet->getIndex() + 1);
100
101 36
        $tableElement = '<table:table table:style-name="' . $tableStyleName . '" table:name="' . $escapedSheetName . '">';
102 36
        $tableElement .= '<table:table-column table:default-cell-style-name="ce1" table:style-name="co1" table:number-columns-repeated="' . $worksheet->getMaxNumColumns() . '"/>';
103
104 36
        return $tableElement;
105
    }
106
107
    /**
108
     * Adds a row to the given worksheet.
109
     *
110
     * @param Worksheet $worksheet The worksheet to add the row to
111
     * @param Row $row The row to be added
112
     * @throws InvalidArgumentException If a cell value's type is not supported
113
     * @throws IOException If the data cannot be written
114
     * @return void
115
     */
116 34
    public function addRow(Worksheet $worksheet, Row $row)
117
    {
118 34
        $cells = $row->getCells();
119 34
        $rowStyle = $row->getStyle();
120
121 34
        $data = '<table:table-row table:style-name="ro1">';
122
123 34
        $currentCellIndex = 0;
124 34
        $nextCellIndex = 1;
125
126 34
        for ($i = 0; $i < $row->getNumCells(); $i++) {
127
            /** @var Cell $cell */
128 34
            $cell = $cells[$currentCellIndex];
129
            /** @var Cell|null $nextCell */
130 34
            $nextCell = isset($cells[$nextCellIndex]) ? $cells[$nextCellIndex] : null;
131
132 34
            if ($nextCell === null || $cell->getValue() !== $nextCell->getValue()) {
133 34
                $registeredStyle = $this->applyStyleAndRegister($cell, $rowStyle);
134 34
                $cellStyle = $registeredStyle->getStyle();
135 34
                if ($registeredStyle->isMatchingRowStyle()) {
136 31
                    $rowStyle = $cellStyle; // Replace actual rowStyle (possibly with null id) by registered style (with id)
137
                }
138
139 34
                $data .= $this->getCellXMLWithStyle($cell, $cellStyle, $currentCellIndex, $nextCellIndex);
140 33
                $currentCellIndex = $nextCellIndex;
141
            }
142
143 33
            $nextCellIndex++;
144
        }
145
146 33
        $data .= '</table:table-row>';
147
148 33
        $wasWriteSuccessful = \fwrite($worksheet->getFilePointer(), $data);
149 33
        if ($wasWriteSuccessful === false) {
150
            throw new IOException("Unable to write data in {$worksheet->getFilePath()}");
151
        }
152
153
        // only update the count if the write worked
154 33
        $lastWrittenRowIndex = $worksheet->getLastWrittenRowIndex();
155 33
        $worksheet->setLastWrittenRowIndex($lastWrittenRowIndex + 1);
156 33
    }
157
158
    /**
159
     * Applies styles to the given style, merging the cell's style with its row's style
160
     *
161
     * @param Cell $cell
162
     * @param Style $rowStyle
163
     * @throws InvalidArgumentException If a cell value's type is not supported
164
     * @return RegisteredStyle
165
     */
166 34
    private function applyStyleAndRegister(Cell $cell, Style $rowStyle) : RegisteredStyle
167
    {
168 34
        $isMatchingRowStyle = false;
169 34
        if ($cell->getStyle()->isEmpty()) {
170 33
            $cell->setStyle($rowStyle);
171
172 33
            $possiblyUpdatedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell);
173
174 33
            if ($possiblyUpdatedStyle->isUpdated()) {
175 2
                $registeredStyle = $this->styleManager->registerStyle($possiblyUpdatedStyle->getStyle());
176
            } else {
177 31
                $registeredStyle = $this->styleManager->registerStyle($rowStyle);
178 33
                $isMatchingRowStyle = true;
179
            }
180
        } else {
181 1
            $mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle);
182 1
            $cell->setStyle($mergedCellAndRowStyle);
183
184 1
            $possiblyUpdatedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell);
185 1
            if ($possiblyUpdatedStyle->isUpdated()) {
186
                $newCellStyle = $possiblyUpdatedStyle->getStyle();
187
            } else {
188 1
                $newCellStyle = $mergedCellAndRowStyle;
189
            }
190
191 1
            $registeredStyle = $this->styleManager->registerStyle($newCellStyle);
192
        }
193
194 34
        return new RegisteredStyle($registeredStyle, $isMatchingRowStyle);
195
    }
196
197 34
    private function getCellXMLWithStyle(Cell $cell, Style $style, int $currentCellIndex, int $nextCellIndex) : string
198
    {
199 34
        $styleIndex = $style->getId() + 1; // 1-based
200
201 34
        $numTimesValueRepeated = ($nextCellIndex - $currentCellIndex);
202
203 34
        return $this->getCellXML($cell, $styleIndex, $numTimesValueRepeated);
204
    }
205
206
    /**
207
     * Returns the cell XML content, given its value.
208
     *
209
     * @param Cell $cell The cell to be written
210
     * @param int $styleIndex Index of the used style
211
     * @param int $numTimesValueRepeated Number of times the value is consecutively repeated
212
     * @throws InvalidArgumentException If a cell value's type is not supported
213
     * @return string The cell XML content
214
     */
215 34
    private function getCellXML(Cell $cell, $styleIndex, $numTimesValueRepeated)
216
    {
217 34
        $data = '<table:table-cell table:style-name="ce' . $styleIndex . '"';
218
219 34
        if ($numTimesValueRepeated !== 1) {
220 4
            $data .= ' table:number-columns-repeated="' . $numTimesValueRepeated . '"';
221
        }
222
223 34
        if ($cell->isString()) {
224 28
            $data .= ' office:value-type="string" calcext:value-type="string">';
225
226 28
            $cellValueLines = \explode("\n", $cell->getValue());
227 28
            foreach ($cellValueLines as $cellValueLine) {
228 28
                $data .= '<text:p>' . $this->stringsEscaper->escape($cellValueLine) . '</text:p>';
229
            }
230
231 28
            $data .= '</table:table-cell>';
232 8
        } elseif ($cell->isBoolean()) {
233 2
            $value = $cell->getValue() ? 'true' : 'false'; // boolean-value spec: http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#datatype-boolean
234 2
            $data .= ' office:value-type="boolean" calcext:value-type="boolean" office:boolean-value="' . $value . '">';
235 2
            $data .= '<text:p>' . $cell->getValue() . '</text:p>';
236 2
            $data .= '</table:table-cell>';
237 7
        } elseif ($cell->isNumeric()) {
238 3
            $cellValue = $cell->getValue();
239
            // Formatting of float values is locale dependent. Thousands separators and decimal points
240
            // vary from locale to locale (en_US: 12.34 vs pl_PL: 12,34). However, ODS values must
241
            // be formatted with no thousands separator and a "." as decimal point to work properly.
242
            // We must then convert the value to the correct format before storing it.
243 3
            if (is_float($cellValue)) {
244 3
                $cellValue = str_replace(
245 3
                    [$this->localeInfo['thousands_sep'], $this->localeInfo['decimal_point']],
246 3
                    ['', '.'],
247
                    $cellValue
248
                );
249
            }
250 3
            $data .= ' office:value-type="float" calcext:value-type="float" office:value="' . $cellValue . '">';
251 3
            $data .= '<text:p>' . $cellValue . '</text:p>';
252 3
            $data .= '</table:table-cell>';
253 5
        } elseif ($cell->isError() && is_string($cell->getValueEvenIfError())) {
254
            // only writes the error value if it's a string
255 1
            $data .= ' office:value-type="string" calcext:value-type="error" office:value="">';
256 1
            $data .= '<text:p>' . $cell->getValueEvenIfError() . '</text:p>';
257 1
            $data .= '</table:table-cell>';
258 4
        } elseif ($cell->isEmpty()) {
259 2
            $data .= '/>';
260
        } else {
261 2
            throw new InvalidArgumentException('Trying to add a value with an unsupported type: ' . \gettype($cell->getValue()));
262
        }
263
264 33
        return $data;
265
    }
266
267
    /**
268
     * Closes the worksheet
269
     *
270
     * @param Worksheet $worksheet
271
     * @return void
272
     */
273 36
    public function close(Worksheet $worksheet)
274
    {
275 36
        $worksheetFilePointer = $worksheet->getFilePointer();
276
277 36
        if (!\is_resource($worksheetFilePointer)) {
278
            return;
279
        }
280
281 36
        \fclose($worksheetFilePointer);
282 36
    }
283
}
284