Completed
Pull Request — master (#765)
by
unknown
15:03
created

WorksheetManager::applyStyleAndGetCellXML()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 8
cts 8
cp 1
rs 9.7998
c 0
b 0
f 0
cc 1
nc 1
nop 4
crap 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\Style\StyleMerger;
14
use Box\Spout\Writer\Common\Manager\WorksheetManagerInterface;
15
use Box\Spout\Writer\ODS\Manager\Style\StyleManager;
16
17
/**
18
 * Class WorksheetManager
19
 * ODS worksheet manager, providing the interfaces to work with ODS worksheets.
20
 */
21
class WorksheetManager implements WorksheetManagerInterface
22
{
23
    /** @var \Box\Spout\Common\Helper\Escaper\ODS Strings escaper */
24
    private $stringsEscaper;
25
26
    /** @var StringHelper String helper */
27
    private $stringHelper;
28
29
    /** @var StyleManager Manages styles */
30
    private $styleManager;
31
32
    /** @var StyleMerger Helper to merge styles together */
33
    private $styleMerger;
34
35
    /**
36
     * WorksheetManager constructor.
37
     *
38
     * @param StyleManager $styleManager
39
     * @param StyleMerger $styleMerger
40
     * @param ODSEscaper $stringsEscaper
41
     * @param StringHelper $stringHelper
42
     */
43 38
    public function __construct(
44
        StyleManager $styleManager,
45
        StyleMerger $styleMerger,
46
        ODSEscaper $stringsEscaper,
47
        StringHelper $stringHelper
48
    ) {
49 38
        $this->styleManager = $styleManager;
50 38
        $this->styleMerger = $styleMerger;
51 38
        $this->stringsEscaper = $stringsEscaper;
52 38
        $this->stringHelper = $stringHelper;
53 38
    }
54
55
    /**
56
     * Prepares the worksheet to accept data
57
     *
58
     * @param Worksheet $worksheet The worksheet to start
59
     * @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing
60
     * @return void
61
     */
62 38
    public function startSheet(Worksheet $worksheet)
63
    {
64 38
        $sheetFilePointer = \fopen($worksheet->getFilePath(), 'w');
65 38
        $this->throwIfSheetFilePointerIsNotAvailable($sheetFilePointer);
66
67 38
        $worksheet->setFilePointer($sheetFilePointer);
68 38
    }
69
70
    /**
71
     * Checks if the sheet has been sucessfully created. Throws an exception if not.
72
     *
73
     * @param bool|resource $sheetFilePointer Pointer to the sheet data file or FALSE if unable to open the file
74
     * @throws IOException If the sheet data file cannot be opened for writing
75
     * @return void
76
     */
77 38
    private function throwIfSheetFilePointerIsNotAvailable($sheetFilePointer)
78
    {
79 38
        if (!$sheetFilePointer) {
80
            throw new IOException('Unable to open sheet for writing.');
81
        }
82 38
    }
83
84
    /**
85
     * Returns the table XML root node as string.
86
     *
87
     * @param Worksheet $worksheet
88
     * @return string <table> node as string
89
     */
90 35
    public function getTableElementStartAsString(Worksheet $worksheet)
91
    {
92 35
        $externalSheet = $worksheet->getExternalSheet();
93 35
        $escapedSheetName = $this->stringsEscaper->escape($externalSheet->getName());
94 35
        $tableStyleName = 'ta' . ($externalSheet->getIndex() + 1);
95
96 35
        $tableElement  = '<table:table table:style-name="' . $tableStyleName . '" table:name="' . $escapedSheetName . '">';
97 35
        $tableElement .= '<table:table-column table:default-cell-style-name="ce1" table:style-name="co1" table:number-columns-repeated="' . $worksheet->getMaxNumColumns() . '"/>';
98
99 35
        return $tableElement;
100
    }
101
102
    /**
103
     * Adds a row to the given worksheet.
104
     *
105
     * @param Worksheet $worksheet The worksheet to add the row to
106
     * @param Row $row The row to be added
107
     * @throws IOException If the data cannot be written
108
     * @throws InvalidArgumentException If a cell value's type is not supported
109
     * @return void
110
     */
111 33
    public function addRow(Worksheet $worksheet, Row $row)
112
    {
113 33
        $cells = $row->getCells();
114 33
        $rowStyle = $row->getStyle();
115
116 33
        $data = '<table:table-row table:style-name="ro1">';
117
118 33
        $currentCellIndex = 0;
119 33
        $nextCellIndex = 1;
120
121 33
        for ($i = 0; $i < $row->getNumCells(); $i++) {
122
            /** @var Cell $cell */
123 33
            $cell = $cells[$currentCellIndex];
124
            /** @var Cell|null $nextCell */
125 33
            $nextCell = isset($cells[$nextCellIndex]) ? $cells[$nextCellIndex] : null;
126
127 33
            if ($nextCell === null || $cell->getValue() !== $nextCell->getValue()) {
128 33
                $data .= $this->applyStyleAndGetCellXML($cell, $rowStyle, $currentCellIndex, $nextCellIndex);
129 32
                $currentCellIndex = $nextCellIndex;
130
            }
131
132 32
            $nextCellIndex++;
133
        }
134
135 32
        $data .= '</table:table-row>';
136
137 32
        $wasWriteSuccessful = \fwrite($worksheet->getFilePointer(), $data);
138 32
        if ($wasWriteSuccessful === false) {
139
            throw new IOException("Unable to write data in {$worksheet->getFilePath()}");
140
        }
141
142
        // only update the count if the write worked
143 32
        $lastWrittenRowIndex = $worksheet->getLastWrittenRowIndex();
144 32
        $worksheet->setLastWrittenRowIndex($lastWrittenRowIndex + 1);
145 32
    }
146
147
    /**
148
     * Applies styles to the given style, merging the cell's style with its row's style
149
     * Then builds and returns xml for the cell.
150
     *
151
     * @param Cell $cell
152
     * @param Style $rowStyle
153
     * @param int $currentCellIndex
154
     * @param int $nextCellIndex
155
     * @throws InvalidArgumentException If a cell value's type is not supported
156
     * @return string
157
     */
158 33
    private function applyStyleAndGetCellXML(Cell $cell, Style $rowStyle, $currentCellIndex, $nextCellIndex)
159
    {
160
        // Apply row and extra styles
161 33
        $mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle);
162 33
        $cell->setStyle($mergedCellAndRowStyle);
163 33
        $newCellStyle = $this->styleManager->applyExtraStylesIfNeeded($cell);
164
165 33
        $registeredStyle = $this->styleManager->registerStyle($newCellStyle);
166 33
        $styleIndex = $registeredStyle->getId() + 1; // 1-based
167
168 33
        $numTimesValueRepeated = ($nextCellIndex - $currentCellIndex);
169
170 33
        return $this->getCellXML($cell, $styleIndex, $numTimesValueRepeated);
171
    }
172
173
    /**
174
     * Returns the cell XML content, given its value.
175
     *
176
     * @param Cell $cell The cell to be written
177
     * @param int $styleIndex Index of the used style
178
     * @param int $numTimesValueRepeated Number of times the value is consecutively repeated
179
     * @throws InvalidArgumentException If a cell value's type is not supported
180
     * @return string The cell XML content
181
     */
182 33
    private function getCellXML(Cell $cell, $styleIndex, $numTimesValueRepeated)
183
    {
184 33
        $data = '<table:table-cell table:style-name="ce' . $styleIndex . '"';
185
186 33
        if ($numTimesValueRepeated !== 1) {
187 4
            $data .= ' table:number-columns-repeated="' . $numTimesValueRepeated . '"';
188
        }
189
190 33
        if ($cell->isString()) {
191 28
            $data .= ' office:value-type="string" calcext:value-type="string">';
192
193 28
            $cellValueLines = \explode("\n", $cell->getValue());
194 28
            foreach ($cellValueLines as $cellValueLine) {
195 28
                $data .= '<text:p>' . $this->stringsEscaper->escape($cellValueLine) . '</text:p>';
196
            }
197
198 28
            $data .= '</table:table-cell>';
199 7
        } elseif ($cell->isBoolean()) {
200 2
            $data .= ' office:value-type="boolean" calcext:value-type="boolean" office:boolean-value="' . $cell->getValue() . '">';
201 2
            $data .= '<text:p>' . $cell->getValue() . '</text:p>';
202 2
            $data .= '</table:table-cell>';
203 6
        } elseif ($cell->isNumeric()) {
204 2
            $data .= ' office:value-type="float" calcext:value-type="float" office:value="' . $cell->getValue() . '">';
205 2
            $data .= '<text:p>' . $cell->getValue() . '</text:p>';
206 2
            $data .= '</table:table-cell>';
207 5
        } elseif ($cell->isDate()) {
208
            $value = $cell->getValue();
209 1
            if ($value instanceof \DateTime) {
210 1
                $datevalue = substr($value->setTimezone(new \DateTimeZone('UTC'))->format(\DateTimeInterface::W3C), 0, -6);
211 1
                $data .= ' office:value-type="date" calcext:value-type="date" office:date-value="' . $datevalue . 'Z">';
212 4
                $data .= '<text:p>' . $datevalue . 'Z</text:p>';
213 2
            } elseif ($value instanceof \DateInterval) {
214
                // workaround for missing DateInterval::format('c'), see https://stackoverflow.com/a/61088115/53538
215 2
                static $f = ['M0S', 'H0M', 'DT0H', 'M0D', 'Y0M', 'P0Y', 'Y0M', 'P0M'];
216
                static $r = ['M', 'H', 'DT', 'M', 'Y0M', 'P', 'Y', 'P'];
217
                $value = rtrim(str_replace($f, $r, $value->format('P%yY%mM%dDT%hH%iM%sS')), 'PT') ?: $default;
0 ignored issues
show
Bug introduced by
The variable $default does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
218 32
                $data .= ' office:value-type="time" office:time-value="' . $value . '">';
219
                $data .= '<text:p>' . $value . '</text:p>';
220
            } else {
221
                throw new InvalidArgumentException('Trying to add a date value with an unsupported type: ' . \gettype($cell->getValue()));
222
            }
223
            $data .= '</table:table-cell>';
224
        } elseif ($cell->isError() && is_string($cell->getValueEvenIfError())) {
225
            // only writes the error value if it's a string
226
            $data .= ' office:value-type="string" calcext:value-type="error" office:value="">';
227 35
            $data .= '<text:p>' . $cell->getValueEvenIfError() . '</text:p>';
228
            $data .= '</table:table-cell>';
229 35
        } elseif ($cell->isEmpty()) {
230
            $data .= '/>';
231 35
        } else {
232
            throw new InvalidArgumentException('Trying to add a value with an unsupported type: ' . \gettype($cell->getValue()));
233
        }
234
235 35
        return $data;
236 35
    }
237
238
    /**
239
     * Closes the worksheet
240
     *
241
     * @param Worksheet $worksheet
242
     * @return void
243
     */
244
    public function close(Worksheet $worksheet)
245
    {
246
        $worksheetFilePointer = $worksheet->getFilePointer();
247
248
        if (!\is_resource($worksheetFilePointer)) {
249
            return;
250
        }
251
252
        \fclose($worksheetFilePointer);
253
    }
254
}
255