Completed
Pull Request — master (#715)
by
unknown
04:32
created

WorksheetManager   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 282
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 13

Test Coverage

Coverage 91.15%

Importance

Changes 0
Metric Value
wmc 33
lcom 1
cbo 13
dl 0
loc 282
ccs 103
cts 113
cp 0.9115
rs 9.76
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 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
B getCellXML() 0 40 10
A close() 0 10 2
A setDefaultColumnWidth() 0 4 1
A setDefaultRowHeight() 0 4 1
A setColumnWidth() 0 4 1
A setColumnWidthForRange() 0 4 1
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
    /**
37
     * WorksheetManager constructor.
38
     *
39
     * @param StyleManager $styleManager
40
     * @param StyleMerger $styleMerger
41
     * @param ODSEscaper $stringsEscaper
42
     * @param StringHelper $stringHelper
43
     */
44 44
    public function __construct(
45
        StyleManager $styleManager,
46
        StyleMerger $styleMerger,
47
        ODSEscaper $stringsEscaper,
48
        StringHelper $stringHelper
49
    ) {
50 44
        $this->styleManager = $styleManager;
51 44
        $this->styleMerger = $styleMerger;
52 44
        $this->stringsEscaper = $stringsEscaper;
53 44
        $this->stringHelper = $stringHelper;
54 44
    }
55
56
    /**
57
     * Prepares the worksheet to accept data
58
     *
59
     * @param Worksheet $worksheet The worksheet to start
60
     * @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing
61
     * @return void
62
     */
63 44
    public function startSheet(Worksheet $worksheet)
64
    {
65 44
        $sheetFilePointer = \fopen($worksheet->getFilePath(), 'w');
66 44
        $this->throwIfSheetFilePointerIsNotAvailable($sheetFilePointer);
67
68 44
        $worksheet->setFilePointer($sheetFilePointer);
69 44
    }
70
71
    /**
72
     * Checks if the sheet has been sucessfully created. Throws an exception if not.
73
     *
74
     * @param bool|resource $sheetFilePointer Pointer to the sheet data file or FALSE if unable to open the file
75
     * @throws IOException If the sheet data file cannot be opened for writing
76
     * @return void
77
     */
78 44
    private function throwIfSheetFilePointerIsNotAvailable($sheetFilePointer)
79
    {
80 44
        if (!$sheetFilePointer) {
81
            throw new IOException('Unable to open sheet for writing.');
82
        }
83 44
    }
84
85
    /**
86
     * Returns the table XML root node as string.
87
     *
88
     * @param Worksheet $worksheet
89
     * @return string <table> node as string
90
     */
91 41
    public function getTableElementStartAsString(Worksheet $worksheet)
92
    {
93 41
        $externalSheet = $worksheet->getExternalSheet();
94 41
        $escapedSheetName = $this->stringsEscaper->escape($externalSheet->getName());
95 41
        $tableStyleName = 'ta' . ($externalSheet->getIndex() + 1);
96
97 41
        $tableElement = '<table:table table:style-name="' . $tableStyleName . '" table:name="' . $escapedSheetName . '">';
98 41
        $tableElement .= $this->styleManager->getStyledTableColumnXMLContent($worksheet->getMaxNumColumns());
99
100 41
        return $tableElement;
101
    }
102
103
    /**
104
     * Adds a row to the given worksheet.
105
     *
106
     * @param Worksheet $worksheet The worksheet to add the row to
107
     * @param Row $row The row to be added
108
     * @throws InvalidArgumentException If a cell value's type is not supported
109
     * @throws IOException If the data cannot be written
110
     * @return void
111
     */
112 39
    public function addRow(Worksheet $worksheet, Row $row)
113
    {
114 39
        $cells = $row->getCells();
115 39
        $rowStyle = $row->getStyle();
116
117 39
        $data = '<table:table-row table:style-name="ro1">';
118
119 39
        $currentCellIndex = 0;
120 39
        $nextCellIndex = 1;
121
122 39
        for ($i = 0; $i < $row->getNumCells(); $i++) {
123
            /** @var Cell $cell */
124 39
            $cell = $cells[$currentCellIndex];
125
            /** @var Cell|null $nextCell */
126 39
            $nextCell = isset($cells[$nextCellIndex]) ? $cells[$nextCellIndex] : null;
127
128 39
            if ($nextCell === null || $cell->getValue() !== $nextCell->getValue()) {
129 39
                $registeredStyle = $this->applyStyleAndRegister($cell, $rowStyle);
130 39
                $cellStyle = $registeredStyle->getStyle();
131 39
                if ($registeredStyle->isMatchingRowStyle()) {
132 36
                    $rowStyle = $cellStyle; // Replace actual rowStyle (possibly with null id) by registered style (with id)
133
                }
134
135 39
                $data .= $this->getCellXMLWithStyle($cell, $cellStyle, $currentCellIndex, $nextCellIndex);
136 38
                $currentCellIndex = $nextCellIndex;
137
            }
138
139 38
            $nextCellIndex++;
140
        }
141
142 38
        $data .= '</table:table-row>';
143
144 38
        $wasWriteSuccessful = \fwrite($worksheet->getFilePointer(), $data);
145 38
        if ($wasWriteSuccessful === false) {
146
            throw new IOException("Unable to write data in {$worksheet->getFilePath()}");
147
        }
148
149
        // only update the count if the write worked
150 38
        $lastWrittenRowIndex = $worksheet->getLastWrittenRowIndex();
151 38
        $worksheet->setLastWrittenRowIndex($lastWrittenRowIndex + 1);
152 38
    }
153
154
    /**
155
     * Applies styles to the given style, merging the cell's style with its row's style
156
     *
157
     * @param Cell $cell
158
     * @param Style $rowStyle
159
     * @throws InvalidArgumentException If a cell value's type is not supported
160
     * @return RegisteredStyle
161
     */
162 39
    private function applyStyleAndRegister(Cell $cell, Style $rowStyle) : RegisteredStyle
163
    {
164 39
        $isMatchingRowStyle = false;
165 39
        if ($cell->getStyle()->isEmpty()) {
166 38
            $cell->setStyle($rowStyle);
167
168 38
            $possiblyUpdatedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell);
169
170 38
            if ($possiblyUpdatedStyle->isUpdated()) {
171 2
                $registeredStyle = $this->styleManager->registerStyle($possiblyUpdatedStyle->getStyle());
172
            } else {
173 36
                $registeredStyle = $this->styleManager->registerStyle($rowStyle);
174 38
                $isMatchingRowStyle = true;
175
            }
176
        } else {
177 1
            $mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle);
178 1
            $cell->setStyle($mergedCellAndRowStyle);
179
180 1
            $possiblyUpdatedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell);
181 1
            if ($possiblyUpdatedStyle->isUpdated()) {
182
                $newCellStyle = $possiblyUpdatedStyle->getStyle();
183
            } else {
184 1
                $newCellStyle = $mergedCellAndRowStyle;
185
            }
186
187 1
            $registeredStyle = $this->styleManager->registerStyle($newCellStyle);
188
        }
189
190 39
        return new RegisteredStyle($registeredStyle, $isMatchingRowStyle);
191
    }
192
193 39
    private function getCellXMLWithStyle(Cell $cell, Style $style, int $currentCellIndex, int $nextCellIndex) : string
194
    {
195 39
        $styleIndex = $style->getId() + 1; // 1-based
196
197 39
        $numTimesValueRepeated = ($nextCellIndex - $currentCellIndex);
198
199 39
        return $this->getCellXML($cell, $styleIndex, $numTimesValueRepeated);
200
    }
201
202
    /**
203
     * Returns the cell XML content, given its value.
204
     *
205
     * @param Cell $cell The cell to be written
206
     * @param int $styleIndex Index of the used style
207
     * @param int $numTimesValueRepeated Number of times the value is consecutively repeated
208
     * @throws InvalidArgumentException If a cell value's type is not supported
209
     * @return string The cell XML content
210
     */
211 39
    private function getCellXML(Cell $cell, $styleIndex, $numTimesValueRepeated)
212
    {
213 39
        $data = '<table:table-cell table:style-name="ce' . $styleIndex . '"';
214
215 39
        if ($numTimesValueRepeated !== 1) {
216 4
            $data .= ' table:number-columns-repeated="' . $numTimesValueRepeated . '"';
217
        }
218
219 39
        if ($cell->isString()) {
220 33
            $data .= ' office:value-type="string" calcext:value-type="string">';
221
222 33
            $cellValueLines = \explode("\n", $cell->getValue());
223 33
            foreach ($cellValueLines as $cellValueLine) {
224 33
                $data .= '<text:p>' . $this->stringsEscaper->escape($cellValueLine) . '</text:p>';
225
            }
226
227 33
            $data .= '</table:table-cell>';
228 8
        } elseif ($cell->isBoolean()) {
229 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
230 2
            $data .= ' office:value-type="boolean" calcext:value-type="boolean" office:boolean-value="' . $value . '">';
231 2
            $data .= '<text:p>' . $cell->getValue() . '</text:p>';
232 2
            $data .= '</table:table-cell>';
233 7
        } elseif ($cell->isNumeric()) {
234 3
            $cellValue = $this->stringHelper->formatNumericValue($cell->getValue());
235 3
            $data .= ' office:value-type="float" calcext:value-type="float" office:value="' . $cellValue . '">';
236 3
            $data .= '<text:p>' . $cellValue . '</text:p>';
237 3
            $data .= '</table:table-cell>';
238 5
        } elseif ($cell->isError() && is_string($cell->getValueEvenIfError())) {
239
            // only writes the error value if it's a string
240 1
            $data .= ' office:value-type="string" calcext:value-type="error" office:value="">';
241 1
            $data .= '<text:p>' . $cell->getValueEvenIfError() . '</text:p>';
242 1
            $data .= '</table:table-cell>';
243 4
        } elseif ($cell->isEmpty()) {
244 2
            $data .= '/>';
245
        } else {
246 2
            throw new InvalidArgumentException('Trying to add a value with an unsupported type: ' . \gettype($cell->getValue()));
247
        }
248
249 38
        return $data;
250
    }
251
252
    /**
253
     * Closes the worksheet
254
     *
255
     * @param Worksheet $worksheet
256
     * @return void
257
     */
258 41
    public function close(Worksheet $worksheet)
259
    {
260 41
        $worksheetFilePointer = $worksheet->getFilePointer();
261
262 41
        if (!\is_resource($worksheetFilePointer)) {
263
            return;
264
        }
265
266 41
        \fclose($worksheetFilePointer);
267 41
    }
268
269
    /**
270
     * @param float|null $width
271
     */
272
    public function setDefaultColumnWidth($width)
273
    {
274
        $this->styleManager->setDefaultColumnWidth($width);
275
    }
276
277
    /**
278
     * @param float|null $height
279
     */
280
    public function setDefaultRowHeight($height)
281
    {
282
        $this->styleManager->setDefaultRowHeight($height);
283
    }
284
285
    /**
286
     * @param float $width
287
     * @param array $columns One or more columns with this width
288
     */
289 3
    public function setColumnWidth(float $width, ...$columns)
290
    {
291 3
        $this->styleManager->setColumnWidth($width, ...$columns);
292 3
    }
293
294
    /**
295
     * @param float $width The width to set
296
     * @param int $start First column index of the range
297
     * @param int $end Last column index of the range
298
     */
299 1
    public function setColumnWidthForRange(float $width, int $start, int $end)
300
    {
301 1
        $this->styleManager->setColumnWidthForRange($width, $start, $end);
302 1
    }
303
}
304