EOD; /** @var \Box\Spout\Writer\Common\Sheet The "external" sheet */ protected $externalSheet; /** @var string Path to the XML file that will contain the sheet data */ protected $worksheetFilePath; /** @var \Box\Spout\Writer\XLSX\Helper\SharedStringsHelper Helper to write shared strings */ protected $sharedStringsHelper; /** @var \Box\Spout\Writer\XLSX\Helper\StyleHelper Helper to work with styles */ protected $styleHelper; /** @var bool Whether inline or shared strings should be used */ protected $shouldUseInlineStrings; /** @var bool Determine whether cell widths should be calculated */ protected $shouldUseCellAutosizing; /** @var \Box\Spout\Common\Escaper\XLSX Strings escaper */ protected $stringsEscaper; /** @var \Box\Spout\Common\Helper\StringHelper String helper */ protected $stringHelper; /** @var \Box\Spout\Writer\XLSX\Helper\SizeCalculator */ protected $sizeCalculator; /** @var Resource Pointer to the sheet data file (e.g. xl/worksheets/sheet1.xml) */ protected $sheetFilePointer; /** @var int Index of the last written row */ protected $lastWrittenRowIndex = 0; /** @var array Holds the column widths for cell sizing */ protected $columnWidths = []; /** * @param \Box\Spout\Writer\Common\Sheet $externalSheet The associated "external" sheet * @param string $worksheetFilesFolder Temporary folder where the files to create the XLSX will be stored * @param \Box\Spout\Writer\XLSX\Helper\SharedStringsHelper $sharedStringsHelper Helper for shared strings * @param \Box\Spout\Writer\XLSX\Helper\StyleHelper Helper to work with styles * @param \Box\Spout\Writer\XLSX\Helper\SizeCalculator $sizeCalculator To calculate cell sizes * @param bool $shouldUseInlineStrings Whether inline or shared strings should be used * @param bool $shouldUseCellAutosizing Whether cell sizes should be calculated or not * @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing */ public function __construct( $externalSheet, $worksheetFilesFolder, $sharedStringsHelper, $styleHelper, $sizeCalculator, $shouldUseInlineStrings, $shouldUseCellAutosizing ) { $this->externalSheet = $externalSheet; $this->sharedStringsHelper = $sharedStringsHelper; $this->styleHelper = $styleHelper; $this->sizeCalculator = $sizeCalculator; $this->shouldUseInlineStrings = $shouldUseInlineStrings; $this->shouldUseCellAutosizing = $shouldUseCellAutosizing; /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ $this->stringsEscaper = \Box\Spout\Common\Escaper\XLSX::getInstance(); $this->stringHelper = new StringHelper(); $this->worksheetFilePath = $worksheetFilesFolder . '/' . strtolower($this->externalSheet->getName()) . '.xml'; $this->startSheet(); } /** * Prepares the worksheet to accept data and preserves free space at the beginning * of the sheet file to prepend header xml and optional column size data. * * @return void * @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing */ protected function startSheet() { $this->sheetFilePointer = fopen($this->worksheetFilePath, 'w'); $this->throwIfSheetFilePointerIsNotAvailable(); $spaceToPreserve = $this->shouldUseCellAutosizing ? 1024 * 1024 : 512; fwrite($this->sheetFilePointer, str_repeat(' ', $spaceToPreserve)); fwrite($this->sheetFilePointer, ''); } /** * Checks if the book has been created. Throws an exception if not created yet. * * @return void * @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing */ protected function throwIfSheetFilePointerIsNotAvailable() { if (!$this->sheetFilePointer) { throw new IOException('Unable to open sheet for writing.'); } } /** * @return \Box\Spout\Writer\Common\Sheet The "external" sheet */ public function getExternalSheet() { return $this->externalSheet; } /** * @return int The index of the last written row */ public function getLastWrittenRowIndex() { return $this->lastWrittenRowIndex; } /** * @return int The ID of the worksheet */ public function getId() { // sheet index is zero-based, while ID is 1-based return $this->externalSheet->getIndex() + 1; } /** * Adds data to the worksheet. * * @param array $dataRow Array containing data to be written. Cannot be empty. * Example $dataRow = ['data1', 1234, null, '', 'data5']; * @param \Box\Spout\Writer\Style\Style $style Style to be applied to the row. NULL means use default style. * @return void * @throws \Box\Spout\Common\Exception\IOException If the data cannot be written * @throws \Box\Spout\Common\Exception\InvalidArgumentException If a cell value's type is not supported */ public function addRow($dataRow, $style) { if (!$this->isEmptyRow($dataRow)) { $this->addNonEmptyRow($dataRow, $style); } $this->lastWrittenRowIndex++; } /** * Returns whether the given row is empty * * @param array $dataRow Array containing data to be written. Cannot be empty. * Example $dataRow = ['data1', 1234, null, '', 'data5']; * @return bool Whether the given row is empty */ private function isEmptyRow($dataRow) { $numCells = count($dataRow); // using "reset()" instead of "$dataRow[0]" because $dataRow can be an associative array return ($numCells === 1 && CellHelper::isEmpty(reset($dataRow))); } /** * Adds non empty row to the worksheet. * * @param array $dataRow Array containing data to be written. Cannot be empty. * Example $dataRow = ['data1', 1234, null, '', 'data5']; * @param \Box\Spout\Writer\Style\Style $style Style to be applied to the row. NULL means use default style. * @return void * @throws \Box\Spout\Common\Exception\IOException If the data cannot be written * @throws \Box\Spout\Common\Exception\InvalidArgumentException If a cell value's type is not supported */ private function addNonEmptyRow($dataRow, $style) { $cellNumber = 0; $rowIndex = $this->lastWrittenRowIndex + 1; $numCells = count($dataRow); $rowXML = ''; if ($this->shouldUseCellAutosizing) { $this->sizeCalculator->setFont($style->getFontName(), $style->getFontSize()); } foreach($dataRow as $cellValue) { $rowXML .= $this->getCellXML($rowIndex, $cellNumber, $cellValue, $style); $cellNumber++; } $rowXML .= ''; $wasWriteSuccessful = fwrite($this->sheetFilePointer, $rowXML); if ($wasWriteSuccessful === false) { throw new IOException("Unable to write data in {$this->worksheetFilePath}"); } } /** * Build and return xml for a single cell. * * @param int $rowIndex * @param int $cellNumber * @param mixed $cellValue * @param Style $style Style to be applied to the row. NULL means use default style. * * @return string * @throws InvalidArgumentException If the given value cannot be processed */ private function getCellXML($rowIndex, $cellNumber, $cellValue, Style $style) { $columnIndex = CellHelper::getCellIndexFromColumnIndex($cellNumber); $cellXML = 'getId() . '"'; if (CellHelper::isNonEmptyString($cellValue)) { $cellXML .= $this->getCellXMLFragmentForNonEmptyString($cellValue); } else if (CellHelper::isBoolean($cellValue)) { $cellXML .= ' t="b">' . intval($cellValue) . ''; } else if (CellHelper::isNumeric($cellValue)) { $cellXML .= '>' . $cellValue . ''; } else if (empty($cellValue)) { if ($this->styleHelper->shouldApplyStyleOnEmptyCell($style->getId())) { $cellXML .= '/>'; } else { // don't write empty cells that do no need styling // NOTE: not appending to $cellXML is the right behavior!! $cellXML = ''; } } else { throw new InvalidArgumentException('Trying to add a value with an unsupported type: ' . gettype($cellValue)); } $this->updateColumnWidth($cellNumber, $cellValue, $style); return $cellXML; } /** * Returns the XML fragment for a cell containing a non empty string * * @param string $cellValue The cell value * @return string The XML fragment representing the cell * @throws InvalidArgumentException If the string exceeds the maximum number of characters allowed per cell */ private function getCellXMLFragmentForNonEmptyString($cellValue) { if ($this->stringHelper->getStringLength($cellValue) > self::MAX_CHARACTERS_PER_CELL) { throw new InvalidArgumentException('Trying to add a value that exceeds the maximum number of characters allowed in a cell (32,767)'); } if ($this->shouldUseInlineStrings) { $cellXMLFragment = ' t="inlineStr">' . $this->stringsEscaper->escape($cellValue) . ''; } else { $sharedStringId = $this->sharedStringsHelper->writeString($cellValue); $cellXMLFragment = ' t="s">' . $sharedStringId . ''; } return $cellXMLFragment; } /** * Update the width of the current cellNumber, if cell autosizing is enabled * and the width of the current value exceeds a previously calculated one. * * @param int $cellNumber * @param string $cellValue * @param Style $style */ private function updateColumnWidth($cellNumber, $cellValue, $style) { if ($this->shouldUseCellAutosizing) { $cellWidth = $this->sizeCalculator->getCellWidth($cellValue, $style->getFontSize()); if (!isset($this->columnWidths[$cellNumber]) || $cellWidth > $this->columnWidths[$cellNumber]) { $this->columnWidths[$cellNumber] = $cellWidth; } } } /** * Return writable xml string, if column widths have been * calculated or custom widths have been set. * * @return string */ private function getColsXML() { if (0 === count($this->columnWidths)) { return ''; } $colsXml = ''; $colTemplate = ''; foreach ($this->columnWidths as $columnIndex => $columnWidth) { $colsXml .= sprintf($colTemplate, $columnIndex+1, $columnIndex+1, $columnWidth); } $colsXml .= ''; return $colsXml; } /** * Closes the worksheet * * @return void */ public function close() { if (!is_resource($this->sheetFilePointer)) { return; } fwrite($this->sheetFilePointer, ''); rewind($this->sheetFilePointer); fwrite($this->sheetFilePointer, self::SHEET_XML_FILE_HEADER); fwrite($this->sheetFilePointer, $this->getColsXML()); fclose($this->sheetFilePointer); } }