Differences Between: [Versions 402 and 403]
1 <?php 2 3 declare(strict_types=1); 4 5 namespace OpenSpout\Writer\XLSX\Manager; 6 7 use OpenSpout\Common\Entity\Cell; 8 use OpenSpout\Common\Entity\Row; 9 use OpenSpout\Common\Entity\Style\Style; 10 use OpenSpout\Common\Exception\InvalidArgumentException; 11 use OpenSpout\Common\Exception\IOException; 12 use OpenSpout\Common\Helper\Escaper\XLSX as XLSXEscaper; 13 use OpenSpout\Common\Helper\StringHelper; 14 use OpenSpout\Writer\Common\Entity\Worksheet; 15 use OpenSpout\Writer\Common\Helper\CellHelper; 16 use OpenSpout\Writer\Common\Manager\RegisteredStyle; 17 use OpenSpout\Writer\Common\Manager\Style\StyleMerger; 18 use OpenSpout\Writer\Common\Manager\WorksheetManagerInterface; 19 use OpenSpout\Writer\XLSX\Helper\DateHelper; 20 use OpenSpout\Writer\XLSX\Manager\Style\StyleManager; 21 use OpenSpout\Writer\XLSX\Options; 22 23 /** 24 * @internal 25 */ 26 final class WorksheetManager implements WorksheetManagerInterface 27 { 28 /** 29 * Maximum number of characters a cell can contain. 30 * 31 * @see https://support.office.com/en-us/article/Excel-specifications-and-limits-16c69c74-3d6a-4aaf-ba35-e6eb276e8eaa [Excel 2007] 32 * @see https://support.office.com/en-us/article/Excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3 [Excel 2010] 33 * @see https://support.office.com/en-us/article/Excel-specifications-and-limits-ca36e2dc-1f09-4620-b726-67c00b05040f [Excel 2013/2016] 34 */ 35 public const MAX_CHARACTERS_PER_CELL = 32767; 36 37 /** @var CommentsManager Manages comments */ 38 private CommentsManager $commentsManager; 39 40 private Options $options; 41 42 /** @var StyleManager Manages styles */ 43 private StyleManager $styleManager; 44 45 /** @var StyleMerger Helper to merge styles together */ 46 private StyleMerger $styleMerger; 47 48 /** @var SharedStringsManager Helper to write shared strings */ 49 private SharedStringsManager $sharedStringsManager; 50 51 /** @var XLSXEscaper Strings escaper */ 52 private XLSXEscaper $stringsEscaper; 53 54 /** @var StringHelper String helper */ 55 private StringHelper $stringHelper; 56 57 /** 58 * WorksheetManager constructor. 59 */ 60 public function __construct( 61 Options $options, 62 StyleManager $styleManager, 63 StyleMerger $styleMerger, 64 CommentsManager $commentsManager, 65 SharedStringsManager $sharedStringsManager, 66 XLSXEscaper $stringsEscaper, 67 StringHelper $stringHelper 68 ) { 69 $this->options = $options; 70 $this->styleManager = $styleManager; 71 $this->styleMerger = $styleMerger; 72 $this->commentsManager = $commentsManager; 73 $this->sharedStringsManager = $sharedStringsManager; 74 $this->stringsEscaper = $stringsEscaper; 75 $this->stringHelper = $stringHelper; 76 } 77 78 public function getSharedStringsManager(): SharedStringsManager 79 { 80 return $this->sharedStringsManager; 81 } 82 83 public function startSheet(Worksheet $worksheet): void 84 { 85 $sheetFilePointer = fopen($worksheet->getFilePath(), 'w'); 86 \assert(false !== $sheetFilePointer); 87 88 $worksheet->setFilePointer($sheetFilePointer); 89 $this->commentsManager->createWorksheetCommentFiles($worksheet); 90 } 91 92 public function addRow(Worksheet $worksheet, Row $row): void 93 { 94 if (!$row->isEmpty()) { 95 $this->addNonEmptyRow($worksheet, $row); 96 $this->commentsManager->addComments($worksheet, $row); 97 } 98 99 $worksheet->setLastWrittenRowIndex($worksheet->getLastWrittenRowIndex() + 1); 100 } 101 102 public function close(Worksheet $worksheet): void 103 { 104 $this->commentsManager->closeWorksheetCommentFiles($worksheet); 105 fclose($worksheet->getFilePointer()); 106 } 107 108 /** 109 * Adds non empty row to the worksheet. 110 * 111 * @param Worksheet $worksheet The worksheet to add the row to 112 * @param Row $row The row to be written 113 * 114 * @throws InvalidArgumentException If a cell value's type is not supported 115 * @throws IOException If the data cannot be written 116 */ 117 private function addNonEmptyRow(Worksheet $worksheet, Row $row): void 118 { 119 $sheetFilePointer = $worksheet->getFilePointer(); 120 $rowStyle = $row->getStyle(); 121 $rowIndexOneBased = $worksheet->getLastWrittenRowIndex() + 1; 122 $numCells = $row->getNumCells(); 123 124 $rowHeight = $row->getHeight(); 125 $hasCustomHeight = ($this->options->DEFAULT_ROW_HEIGHT > 0 || $rowHeight > 0) ? '1' : '0'; 126 $rowXML = "<row r=\"{$rowIndexOneBased}\" spans=\"1:{$numCells}\" ".($rowHeight > 0 ? "ht=\"{$rowHeight}\" " : '')."customHeight=\"{$hasCustomHeight}\">"; 127 128 foreach ($row->getCells() as $columnIndexZeroBased => $cell) { 129 $registeredStyle = $this->applyStyleAndRegister($cell, $rowStyle); 130 $cellStyle = $registeredStyle->getStyle(); 131 if ($registeredStyle->isMatchingRowStyle()) { 132 $rowStyle = $cellStyle; // Replace actual rowStyle (possibly with null id) by registered style (with id) 133 } 134 $rowXML .= $this->getCellXML($rowIndexOneBased, $columnIndexZeroBased, $cell, $cellStyle->getId()); 135 } 136 137 $rowXML .= '</row>'; 138 139 $wasWriteSuccessful = fwrite($sheetFilePointer, $rowXML); 140 if (false === $wasWriteSuccessful) { 141 throw new IOException("Unable to write data in {$worksheet->getFilePath()}"); 142 } 143 } 144 145 /** 146 * Applies styles to the given style, merging the cell's style with its row's style. 147 * 148 * @throws InvalidArgumentException If the given value cannot be processed 149 */ 150 private function applyStyleAndRegister(Cell $cell, Style $rowStyle): RegisteredStyle 151 { 152 $isMatchingRowStyle = false; 153 if ($cell->getStyle()->isEmpty()) { 154 $cell->setStyle($rowStyle); 155 156 $possiblyUpdatedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell); 157 158 if ($possiblyUpdatedStyle->isUpdated()) { 159 $registeredStyle = $this->styleManager->registerStyle($possiblyUpdatedStyle->getStyle()); 160 } else { 161 $registeredStyle = $this->styleManager->registerStyle($rowStyle); 162 $isMatchingRowStyle = true; 163 } 164 } else { 165 $mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle); 166 $cell->setStyle($mergedCellAndRowStyle); 167 168 $possiblyUpdatedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell); 169 170 if ($possiblyUpdatedStyle->isUpdated()) { 171 $newCellStyle = $possiblyUpdatedStyle->getStyle(); 172 } else { 173 $newCellStyle = $mergedCellAndRowStyle; 174 } 175 176 $registeredStyle = $this->styleManager->registerStyle($newCellStyle); 177 } 178 179 return new RegisteredStyle($registeredStyle, $isMatchingRowStyle); 180 } 181 182 /** 183 * Builds and returns xml for a single cell. 184 * 185 * @throws InvalidArgumentException If the given value cannot be processed 186 */ 187 private function getCellXML(int $rowIndexOneBased, int $columnIndexZeroBased, Cell $cell, ?int $styleId): string 188 { 189 $columnLetters = CellHelper::getColumnLettersFromColumnIndex($columnIndexZeroBased); 190 $cellXML = '<c r="'.$columnLetters.$rowIndexOneBased.'"'; 191 $cellXML .= ' s="'.$styleId.'"'; 192 193 if ($cell instanceof Cell\StringCell) { 194 $cellXML .= $this->getCellXMLFragmentForNonEmptyString($cell->getValue()); 195 } elseif ($cell instanceof Cell\BooleanCell) { 196 $cellXML .= ' t="b"><v>'.(int) $cell->getValue().'</v></c>'; 197 } elseif ($cell instanceof Cell\NumericCell) { 198 $cellXML .= '><v>'.$cell->getValue().'</v></c>'; 199 } elseif ($cell instanceof Cell\FormulaCell) { 200 $cellXML .= '><f>'.substr($cell->getValue(), 1).'</f></c>'; 201 } elseif ($cell instanceof Cell\DateTimeCell) { 202 $cellXML .= '><v>'.DateHelper::toExcel($cell->getValue()).'</v></c>'; 203 } elseif ($cell instanceof Cell\ErrorCell) { 204 // only writes the error value if it's a string 205 $cellXML .= ' t="e"><v>'.$cell->getRawValue().'</v></c>'; 206 } elseif ($cell instanceof Cell\EmptyCell) { 207 if ($this->styleManager->shouldApplyStyleOnEmptyCell($styleId)) { 208 $cellXML .= '/>'; 209 } else { 210 // don't write empty cells that do no need styling 211 // NOTE: not appending to $cellXML is the right behavior!! 212 $cellXML = ''; 213 } 214 } 215 216 return $cellXML; 217 } 218 219 /** 220 * Returns the XML fragment for a cell containing a non empty string. 221 * 222 * @param string $cellValue The cell value 223 * 224 * @return string The XML fragment representing the cell 225 * 226 * @throws InvalidArgumentException If the string exceeds the maximum number of characters allowed per cell 227 */ 228 private function getCellXMLFragmentForNonEmptyString(string $cellValue): string 229 { 230 if ($this->stringHelper->getStringLength($cellValue) > self::MAX_CHARACTERS_PER_CELL) { 231 throw new InvalidArgumentException('Trying to add a value that exceeds the maximum number of characters allowed in a cell (32,767)'); 232 } 233 234 if ($this->options->SHOULD_USE_INLINE_STRINGS) { 235 $cellXMLFragment = ' t="inlineStr"><is><t>'.$this->stringsEscaper->escape($cellValue).'</t></is></c>'; 236 } else { 237 $sharedStringId = $this->sharedStringsManager->writeString($cellValue); 238 $cellXMLFragment = ' t="s"><v>'.$sharedStringId.'</v></c>'; 239 } 240 241 return $cellXMLFragment; 242 } 243 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body