1 |
efrain |
1 |
<?php
|
|
|
2 |
|
|
|
3 |
declare(strict_types=1);
|
|
|
4 |
|
|
|
5 |
namespace OpenSpout\Writer\ODS\Manager;
|
|
|
6 |
|
|
|
7 |
use DateTimeImmutable;
|
|
|
8 |
use DateTimeInterface;
|
|
|
9 |
use OpenSpout\Common\Entity\Cell;
|
|
|
10 |
use OpenSpout\Common\Entity\Row;
|
|
|
11 |
use OpenSpout\Common\Entity\Style\Style;
|
|
|
12 |
use OpenSpout\Common\Exception\InvalidArgumentException;
|
|
|
13 |
use OpenSpout\Common\Exception\IOException;
|
|
|
14 |
use OpenSpout\Common\Helper\Escaper\ODS as ODSEscaper;
|
|
|
15 |
use OpenSpout\Writer\Common\Entity\Worksheet;
|
|
|
16 |
use OpenSpout\Writer\Common\Helper\CellHelper;
|
|
|
17 |
use OpenSpout\Writer\Common\Manager\RegisteredStyle;
|
|
|
18 |
use OpenSpout\Writer\Common\Manager\Style\StyleMerger;
|
|
|
19 |
use OpenSpout\Writer\Common\Manager\WorksheetManagerInterface;
|
|
|
20 |
use OpenSpout\Writer\ODS\Manager\Style\StyleManager;
|
|
|
21 |
|
|
|
22 |
/**
|
|
|
23 |
* @internal
|
|
|
24 |
*/
|
|
|
25 |
final class WorksheetManager implements WorksheetManagerInterface
|
|
|
26 |
{
|
|
|
27 |
/** @var ODSEscaper Strings escaper */
|
|
|
28 |
private readonly ODSEscaper $stringsEscaper;
|
|
|
29 |
|
|
|
30 |
/** @var StyleManager Manages styles */
|
|
|
31 |
private readonly StyleManager $styleManager;
|
|
|
32 |
|
|
|
33 |
/** @var StyleMerger Helper to merge styles together */
|
|
|
34 |
private readonly StyleMerger $styleMerger;
|
|
|
35 |
|
|
|
36 |
/**
|
|
|
37 |
* WorksheetManager constructor.
|
|
|
38 |
*/
|
|
|
39 |
public function __construct(
|
|
|
40 |
StyleManager $styleManager,
|
|
|
41 |
StyleMerger $styleMerger,
|
|
|
42 |
ODSEscaper $stringsEscaper
|
|
|
43 |
) {
|
|
|
44 |
$this->styleManager = $styleManager;
|
|
|
45 |
$this->styleMerger = $styleMerger;
|
|
|
46 |
$this->stringsEscaper = $stringsEscaper;
|
|
|
47 |
}
|
|
|
48 |
|
|
|
49 |
/**
|
|
|
50 |
* Prepares the worksheet to accept data.
|
|
|
51 |
*
|
|
|
52 |
* @param Worksheet $worksheet The worksheet to start
|
|
|
53 |
*
|
|
|
54 |
* @throws IOException If the sheet data file cannot be opened for writing
|
|
|
55 |
*/
|
|
|
56 |
public function startSheet(Worksheet $worksheet): void
|
|
|
57 |
{
|
|
|
58 |
$sheetFilePointer = fopen($worksheet->getFilePath(), 'w');
|
|
|
59 |
\assert(false !== $sheetFilePointer);
|
|
|
60 |
|
|
|
61 |
$worksheet->setFilePointer($sheetFilePointer);
|
|
|
62 |
}
|
|
|
63 |
|
|
|
64 |
/**
|
|
|
65 |
* Returns the table XML root node as string.
|
|
|
66 |
*
|
|
|
67 |
* @return string "<table>" node as string
|
|
|
68 |
*/
|
|
|
69 |
public function getTableElementStartAsString(Worksheet $worksheet): string
|
|
|
70 |
{
|
|
|
71 |
$externalSheet = $worksheet->getExternalSheet();
|
|
|
72 |
$escapedSheetName = $this->stringsEscaper->escape($externalSheet->getName());
|
|
|
73 |
$tableStyleName = 'ta'.($externalSheet->getIndex() + 1);
|
|
|
74 |
|
|
|
75 |
$tableElement = '<table:table table:style-name="'.$tableStyleName.'" table:name="'.$escapedSheetName.'">';
|
|
|
76 |
$tableElement .= $this->styleManager->getStyledTableColumnXMLContent($worksheet->getMaxNumColumns());
|
|
|
77 |
|
|
|
78 |
return $tableElement;
|
|
|
79 |
}
|
|
|
80 |
|
|
|
81 |
/**
|
|
|
82 |
* Returns the table:database-range XML node for AutoFilter as string.
|
|
|
83 |
*/
|
|
|
84 |
public function getTableDatabaseRangeElementAsString(Worksheet $worksheet): string
|
|
|
85 |
{
|
|
|
86 |
$externalSheet = $worksheet->getExternalSheet();
|
|
|
87 |
$escapedSheetName = $this->stringsEscaper->escape($externalSheet->getName());
|
|
|
88 |
$databaseRange = '';
|
|
|
89 |
|
|
|
90 |
if (null !== $autofilter = $externalSheet->getAutoFilter()) {
|
|
|
91 |
$rangeAddress = sprintf(
|
|
|
92 |
'\'%s\'.%s%s:\'%s\'.%s%s',
|
|
|
93 |
$escapedSheetName,
|
|
|
94 |
CellHelper::getColumnLettersFromColumnIndex($autofilter->fromColumnIndex),
|
|
|
95 |
$autofilter->fromRow,
|
|
|
96 |
$escapedSheetName,
|
|
|
97 |
CellHelper::getColumnLettersFromColumnIndex($autofilter->toColumnIndex),
|
|
|
98 |
$autofilter->toRow
|
|
|
99 |
);
|
|
|
100 |
$databaseRange = '<table:database-range table:name="__Anonymous_Sheet_DB__'.$externalSheet->getIndex().'" table:target-range-address="'.$rangeAddress.'" table:display-filter-buttons="true"/>';
|
|
|
101 |
}
|
|
|
102 |
|
|
|
103 |
return $databaseRange;
|
|
|
104 |
}
|
|
|
105 |
|
|
|
106 |
/**
|
|
|
107 |
* Adds a row to the given worksheet.
|
|
|
108 |
*
|
|
|
109 |
* @param Worksheet $worksheet The worksheet to add the row to
|
|
|
110 |
* @param Row $row The row to be added
|
|
|
111 |
*
|
|
|
112 |
* @throws InvalidArgumentException If a cell value's type is not supported
|
|
|
113 |
* @throws IOException If the data cannot be written
|
|
|
114 |
*/
|
|
|
115 |
public function addRow(Worksheet $worksheet, Row $row): void
|
|
|
116 |
{
|
|
|
117 |
$cells = $row->getCells();
|
|
|
118 |
$rowStyle = $row->getStyle();
|
|
|
119 |
|
|
|
120 |
$data = '<table:table-row table:style-name="ro1">';
|
|
|
121 |
|
|
|
122 |
$currentCellIndex = 0;
|
|
|
123 |
$nextCellIndex = 1;
|
|
|
124 |
|
|
|
125 |
for ($i = 0; $i < $row->getNumCells(); ++$i) {
|
|
|
126 |
/** @var Cell $cell */
|
|
|
127 |
$cell = $cells[$currentCellIndex];
|
|
|
128 |
|
|
|
129 |
/** @var null|Cell $nextCell */
|
|
|
130 |
$nextCell = $cells[$nextCellIndex] ?? null;
|
|
|
131 |
|
|
|
132 |
if (null === $nextCell || $cell->getValue() !== $nextCell->getValue()) {
|
|
|
133 |
$registeredStyle = $this->applyStyleAndRegister($cell, $rowStyle);
|
|
|
134 |
$cellStyle = $registeredStyle->getStyle();
|
|
|
135 |
if ($registeredStyle->isMatchingRowStyle()) {
|
|
|
136 |
$rowStyle = $cellStyle; // Replace actual rowStyle (possibly with null id) by registered style (with id)
|
|
|
137 |
}
|
|
|
138 |
|
|
|
139 |
$data .= $this->getCellXMLWithStyle($cell, $cellStyle, $currentCellIndex, $nextCellIndex);
|
|
|
140 |
$currentCellIndex = $nextCellIndex;
|
|
|
141 |
}
|
|
|
142 |
|
|
|
143 |
++$nextCellIndex;
|
|
|
144 |
}
|
|
|
145 |
|
|
|
146 |
$data .= '</table:table-row>';
|
|
|
147 |
|
|
|
148 |
$wasWriteSuccessful = fwrite($worksheet->getFilePointer(), $data);
|
|
|
149 |
if (false === $wasWriteSuccessful) {
|
|
|
150 |
throw new IOException("Unable to write data in {$worksheet->getFilePath()}");
|
|
|
151 |
}
|
|
|
152 |
|
|
|
153 |
// only update the count if the write worked
|
|
|
154 |
$lastWrittenRowIndex = $worksheet->getLastWrittenRowIndex();
|
|
|
155 |
$worksheet->setLastWrittenRowIndex($lastWrittenRowIndex + 1);
|
|
|
156 |
}
|
|
|
157 |
|
|
|
158 |
/**
|
|
|
159 |
* Closes the worksheet.
|
|
|
160 |
*/
|
|
|
161 |
public function close(Worksheet $worksheet): void
|
|
|
162 |
{
|
|
|
163 |
fclose($worksheet->getFilePointer());
|
|
|
164 |
}
|
|
|
165 |
|
|
|
166 |
/**
|
|
|
167 |
* Applies styles to the given style, merging the cell's style with its row's style.
|
|
|
168 |
*
|
|
|
169 |
* @throws InvalidArgumentException If a cell value's type is not supported
|
|
|
170 |
*/
|
|
|
171 |
private function applyStyleAndRegister(Cell $cell, Style $rowStyle): RegisteredStyle
|
|
|
172 |
{
|
|
|
173 |
$isMatchingRowStyle = false;
|
|
|
174 |
if ($cell->getStyle()->isEmpty()) {
|
|
|
175 |
$cell->setStyle($rowStyle);
|
|
|
176 |
|
|
|
177 |
$possiblyUpdatedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell);
|
|
|
178 |
|
|
|
179 |
if ($possiblyUpdatedStyle->isUpdated()) {
|
|
|
180 |
$registeredStyle = $this->styleManager->registerStyle($possiblyUpdatedStyle->getStyle());
|
|
|
181 |
} else {
|
|
|
182 |
$registeredStyle = $this->styleManager->registerStyle($rowStyle);
|
|
|
183 |
$isMatchingRowStyle = true;
|
|
|
184 |
}
|
|
|
185 |
} else {
|
|
|
186 |
$mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle);
|
|
|
187 |
$cell->setStyle($mergedCellAndRowStyle);
|
|
|
188 |
|
|
|
189 |
$possiblyUpdatedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell);
|
|
|
190 |
if ($possiblyUpdatedStyle->isUpdated()) {
|
|
|
191 |
$newCellStyle = $possiblyUpdatedStyle->getStyle();
|
|
|
192 |
} else {
|
|
|
193 |
$newCellStyle = $mergedCellAndRowStyle;
|
|
|
194 |
}
|
|
|
195 |
|
|
|
196 |
$registeredStyle = $this->styleManager->registerStyle($newCellStyle);
|
|
|
197 |
}
|
|
|
198 |
|
|
|
199 |
return new RegisteredStyle($registeredStyle, $isMatchingRowStyle);
|
|
|
200 |
}
|
|
|
201 |
|
|
|
202 |
private function getCellXMLWithStyle(Cell $cell, Style $style, int $currentCellIndex, int $nextCellIndex): string
|
|
|
203 |
{
|
|
|
204 |
$styleIndex = $style->getId() + 1; // 1-based
|
|
|
205 |
|
|
|
206 |
$numTimesValueRepeated = ($nextCellIndex - $currentCellIndex);
|
|
|
207 |
|
|
|
208 |
return $this->getCellXML($cell, $styleIndex, $numTimesValueRepeated);
|
|
|
209 |
}
|
|
|
210 |
|
|
|
211 |
/**
|
|
|
212 |
* Returns the cell XML content, given its value.
|
|
|
213 |
*
|
|
|
214 |
* @param Cell $cell The cell to be written
|
|
|
215 |
* @param int $styleIndex Index of the used style
|
|
|
216 |
* @param int $numTimesValueRepeated Number of times the value is consecutively repeated
|
|
|
217 |
*
|
|
|
218 |
* @return string The cell XML content
|
|
|
219 |
*
|
|
|
220 |
* @throws InvalidArgumentException If a cell value's type is not supported
|
|
|
221 |
*/
|
|
|
222 |
private function getCellXML(Cell $cell, int $styleIndex, int $numTimesValueRepeated): string
|
|
|
223 |
{
|
|
|
224 |
$data = '<table:table-cell table:style-name="ce'.$styleIndex.'"';
|
|
|
225 |
|
|
|
226 |
if (1 !== $numTimesValueRepeated) {
|
|
|
227 |
$data .= ' table:number-columns-repeated="'.$numTimesValueRepeated.'"';
|
|
|
228 |
}
|
|
|
229 |
|
|
|
230 |
if ($cell instanceof Cell\StringCell) {
|
|
|
231 |
$data .= ' office:value-type="string" calcext:value-type="string">';
|
|
|
232 |
|
|
|
233 |
$cellValueLines = explode("\n", $cell->getValue());
|
|
|
234 |
foreach ($cellValueLines as $cellValueLine) {
|
|
|
235 |
$data .= '<text:p>'.$this->stringsEscaper->escape($cellValueLine).'</text:p>';
|
|
|
236 |
}
|
|
|
237 |
|
|
|
238 |
$data .= '</table:table-cell>';
|
|
|
239 |
} elseif ($cell instanceof Cell\BooleanCell) {
|
|
|
240 |
$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
|
|
|
241 |
$data .= ' office:value-type="boolean" calcext:value-type="boolean" office:boolean-value="'.$value.'">';
|
|
|
242 |
$data .= '<text:p>'.$cell->getValue().'</text:p>';
|
|
|
243 |
$data .= '</table:table-cell>';
|
|
|
244 |
} elseif ($cell instanceof Cell\NumericCell) {
|
|
|
245 |
$cellValue = $cell->getValue();
|
|
|
246 |
$data .= ' office:value-type="float" calcext:value-type="float" office:value="'.$cellValue.'">';
|
|
|
247 |
$data .= '<text:p>'.$cellValue.'</text:p>';
|
|
|
248 |
$data .= '</table:table-cell>';
|
|
|
249 |
} elseif ($cell instanceof Cell\DateTimeCell) {
|
|
|
250 |
$datevalue = substr((new DateTimeImmutable('@'.$cell->getValue()->getTimestamp()))->format(DateTimeInterface::W3C), 0, -6);
|
|
|
251 |
$data .= ' office:value-type="date" calcext:value-type="date" office:date-value="'.$datevalue.'Z">';
|
|
|
252 |
$data .= '<text:p>'.$datevalue.'Z</text:p>';
|
|
|
253 |
$data .= '</table:table-cell>';
|
|
|
254 |
} elseif ($cell instanceof Cell\DateIntervalCell) {
|
|
|
255 |
// workaround for missing DateInterval::format('c'), see https://stackoverflow.com/a/61088115/53538
|
|
|
256 |
static $f = ['M0S', 'H0M', 'DT0H', 'M0D', 'Y0M', 'P0Y', 'Y0M', 'P0M'];
|
|
|
257 |
static $r = ['M', 'H', 'DT', 'M', 'Y0M', 'P', 'Y', 'P'];
|
|
|
258 |
$value = rtrim(str_replace($f, $r, $cell->getValue()->format('P%yY%mM%dDT%hH%iM%sS')), 'PT') ?: 'PT0S';
|
|
|
259 |
$data .= ' office:value-type="time" office:time-value="'.$value.'">';
|
|
|
260 |
$data .= '<text:p>'.$value.'</text:p>';
|
|
|
261 |
$data .= '</table:table-cell>';
|
|
|
262 |
} elseif ($cell instanceof Cell\ErrorCell) {
|
|
|
263 |
// only writes the error value if it's a string
|
|
|
264 |
$data .= ' office:value-type="string" calcext:value-type="error" office:value="">';
|
|
|
265 |
$data .= '<text:p>'.$cell->getRawValue().'</text:p>';
|
|
|
266 |
$data .= '</table:table-cell>';
|
|
|
267 |
} elseif ($cell instanceof Cell\EmptyCell) {
|
|
|
268 |
$data .= '/>';
|
|
|
269 |
}
|
|
|
270 |
|
|
|
271 |
return $data;
|
|
|
272 |
}
|
|
|
273 |
}
|