Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
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
}