diff --git a/src/Spout/Common/Escaper/XLSX.php b/src/Spout/Common/Escaper/XLSX.php index fcb626a..8d6c255 100644 --- a/src/Spout/Common/Escaper/XLSX.php +++ b/src/Spout/Common/Escaper/XLSX.php @@ -58,8 +58,8 @@ class XLSX implements EscaperInterface */ protected function getControlCharactersEscapingMap() { - $controlCharactersEscapingMap = []; - $whitelistedControlCharacters = ["\t", "\r", "\n"]; + $controlCharactersEscapingMap = array(); + $whitelistedControlCharacters = array("\t", "\r", "\n"); // control characters values are from 0 to 1F (hex values) in the ASCII table for ($charValue = 0x0; $charValue <= 0x1F; $charValue++) { diff --git a/src/Spout/Common/Type.php b/src/Spout/Common/Type.php index 6af7f35..35ca3c9 100644 --- a/src/Spout/Common/Type.php +++ b/src/Spout/Common/Type.php @@ -10,4 +10,6 @@ abstract class Type { const CSV = "csv"; const XLSX = "xlsx"; + const XLS = "xls"; + const HTM = "htm"; } diff --git a/src/Spout/Reader/CSV.php b/src/Spout/Reader/CSV.php index bd48248..2da160f 100644 --- a/src/Spout/Reader/CSV.php +++ b/src/Spout/Reader/CSV.php @@ -52,7 +52,7 @@ class CSV extends AbstractReader * The file must be UTF-8 encoded. * @TODO add encoding detection/conversion * - * @param string $filePath Path of the XLSX file to be read + * @param string $filePath Path of the CSV file to be read * @return void * @throws \Box\Spout\Common\Exception\IOException */ diff --git a/src/Spout/Reader/Helper/XLSX/SharedStringsHelper.php b/src/Spout/Reader/Helper/XLSX/SharedStringsHelper.php index b1a239c..04f49c9 100644 --- a/src/Spout/Reader/Helper/XLSX/SharedStringsHelper.php +++ b/src/Spout/Reader/Helper/XLSX/SharedStringsHelper.php @@ -145,9 +145,9 @@ class SharedStringsHelper */ protected function removeSuperfluousTextNodes($parentNode) { - $tagsToRemove = [ + $tagsToRemove = array( 'rPh', // Pronunciation of the text - ]; + ); foreach ($tagsToRemove as $tagToRemove) { $xpath = '//ns:' . $tagToRemove; diff --git a/src/Spout/Reader/Helper/XLSX/WorksheetHelper.php b/src/Spout/Reader/Helper/XLSX/WorksheetHelper.php index a105e01..14e3b22 100644 --- a/src/Spout/Reader/Helper/XLSX/WorksheetHelper.php +++ b/src/Spout/Reader/Helper/XLSX/WorksheetHelper.php @@ -40,7 +40,7 @@ class WorksheetHelper */ public function getWorksheets() { - $worksheets = []; + $worksheets = array(); $xmlContents = file_get_contents('zip://' . $this->filePath . '#' . self::CONTENT_TYPES_XML_FILE_PATH); diff --git a/src/Spout/Reader/XLSX.php b/src/Spout/Reader/XLSX.php index db46707..f285e89 100644 --- a/src/Spout/Reader/XLSX.php +++ b/src/Spout/Reader/XLSX.php @@ -185,7 +185,7 @@ class XLSX extends AbstractReader } $isInsideRowTag = false; - $rowData = []; + $rowData = array(); while ($this->xmlReader->read()) { if ($this->xmlReader->nodeType == \XMLReader::ELEMENT && $this->xmlReader->name === 'dimension') { @@ -230,7 +230,7 @@ class XLSX extends AbstractReader } // no data means "end of file" - return ($rowData !== []) ? $rowData : null; + return ($rowData !== array()) ? $rowData : null; } /** diff --git a/src/Spout/Writer/AbstractWriter.php b/src/Spout/Writer/AbstractWriter.php index 0e9e883..b9b207e 100644 --- a/src/Spout/Writer/AbstractWriter.php +++ b/src/Spout/Writer/AbstractWriter.php @@ -38,13 +38,14 @@ abstract class AbstractWriter implements WriterInterface abstract protected function openWriter(); /** - * Adds data to the currently openned writer. + * Adds data to the currently opened writer. * * @param array $dataRow Array containing data to be streamed. * Example $dataRow = ['data1', 1234, null, '', 'data5']; + * @param array $metaData Array containing meta-data maps for individual cells, such as 'url' * @return void */ - abstract protected function addRowToWriter(array $dataRow); + abstract protected function addRowToWriter(array $dataRow, array $metaData); /** * Closes the streamer, preventing any additional writing. @@ -138,15 +139,16 @@ abstract class AbstractWriter implements WriterInterface * * @param array $dataRow Array containing data to be streamed. * Example $dataRow = ['data1', 1234, null, '', 'data5']; + * @param array $metaData Array containing meta-data maps for individual cells, such as 'url' * * @return \Box\Spout\Writer\AbstractWriter * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer * @throws \Box\Spout\Common\Exception\IOException If unable to write data */ - public function addRow(array $dataRow) + public function addRow(array $dataRow, array $metaData = array()) { if ($this->isWriterOpened) { - $this->addRowToWriter($dataRow); + $this->addRowToWriter($dataRow, $metaData); } else { throw new WriterNotOpenedException('The writer needs to be opened before adding row.'); } diff --git a/src/Spout/Writer/CSV.php b/src/Spout/Writer/CSV.php index 0ac8d91..1c7ae7e 100644 --- a/src/Spout/Writer/CSV.php +++ b/src/Spout/Writer/CSV.php @@ -68,10 +68,11 @@ class CSV extends AbstractWriter * * @param array $dataRow Array containing data to be written. * Example $dataRow = ['data1', 1234, null, '', 'data5']; + * @param array $metaData Array containing meta-data maps for individual cells, such as 'url' * @return void * @throws \Box\Spout\Common\Exception\IOException If unable to write data */ - protected function addRowToWriter(array $dataRow) + protected function addRowToWriter(array $dataRow, array $metaData) { $wasWriteSuccessful = fputcsv($this->filePointer, $dataRow, $this->fieldDelimiter, $this->fieldEnclosure); if ($wasWriteSuccessful === false) { diff --git a/src/Spout/Writer/HTM.php b/src/Spout/Writer/HTM.php new file mode 100644 index 0000000..dfa7b5b --- /dev/null +++ b/src/Spout/Writer/HTM.php @@ -0,0 +1,109 @@ +filePointer, "\n"); + fwrite($this->filePointer, "\n"); + fwrite($this->filePointer, "" . htmlentities(basename(basename($this->outputFilePath, '.html'), '.htm')) . "\n"); + fwrite($this->filePointer, "\n"); + fwrite($this->filePointer, "\n"); + fwrite($this->filePointer, "\n"); + } + + /** + * Adds data to the currently opened writer. + * + * @param array $dataRow Array containing data to be written. + * Example $dataRow = ['data1', 1234, null, '', 'data5']; + * @param array $metaData Array containing meta-data maps for individual cells, such as 'url' + * @return void + * @throws \Box\Spout\Common\Exception\IOException If unable to write data + */ + protected function addRowToWriter(array $dataRow, array $metaData) + { + $wasWriteSuccessful = true; + if ($this->lastWrittenRowIndex == 0) { + $wasWriteSuccessful = $wasWriteSuccessful && fwrite($this->filePointer, "\n"); + } + if ($this->lastWrittenRowIndex == 1) { + $wasWriteSuccessful = $wasWriteSuccessful && fwrite($this->filePointer, "\n"); + } + $wasWriteSuccessful = $wasWriteSuccessful && fwrite($this->filePointer, "\n"); + foreach ($dataRow as $i => $cell) { + $cell = nl2br(htmlentities($cell)); + + if (isset($metaData[$i]['url'])) + { + $cell = '' . $cell . ''; + } + + if ($this->lastWrittenRowIndex == 0) { + $wasWriteSuccessful = $wasWriteSuccessful && fwrite($this->filePointer, "\t\n"); + } else + { + $wasWriteSuccessful = $wasWriteSuccessful && fwrite($this->filePointer, "\t\n"); + } + } + $wasWriteSuccessful = $wasWriteSuccessful && fwrite($this->filePointer, "\n"); + if ($this->lastWrittenRowIndex == 0) { + $wasWriteSuccessful = $wasWriteSuccessful && fwrite($this->filePointer, "\n"); + } + + if ($wasWriteSuccessful === false) { + throw new IOException('Unable to write data'); + } + + $this->lastWrittenRowIndex++; + if ($this->lastWrittenRowIndex % self::FLUSH_THRESHOLD === 0) { + $this->globalFunctionsHelper->fflush($this->filePointer); + } + } + + /** + * Closes the HTM streamer, preventing any additional writing. + * If set, sets the headers and redirects output to the browser. + * + * @return void + */ + protected function closeWriter() + { + if ($this->filePointer) { + if ($this->lastWrittenRowIndex >= 1) { + fwrite($this->filePointer, "\n"); + } + fwrite($this->filePointer, "
{$cell}{$cell}
\n"); + fwrite($this->filePointer, "\n"); + fwrite($this->filePointer, "\n"); + + $this->globalFunctionsHelper->fclose($this->filePointer); + } + + $this->lastWrittenRowIndex = 0; + } +} diff --git a/src/Spout/Writer/Helper/XLSX/FileSystemHelper.php b/src/Spout/Writer/Helper/XLSX/FileSystemHelper.php index fddbb25..6b84a12 100644 --- a/src/Spout/Writer/Helper/XLSX/FileSystemHelper.php +++ b/src/Spout/Writer/Helper/XLSX/FileSystemHelper.php @@ -20,6 +20,7 @@ class FileSystemHelper extends \Box\Spout\Common\Helper\FileSystemHelper const XL_FOLDER_NAME = 'xl'; const WORKSHEETS_FOLDER_NAME = 'worksheets'; + const STYLES_FILE_NAME = 'styles.xml'; const RELS_FILE_NAME = '.rels'; const APP_XML_FILE_NAME = 'app.xml'; const CORE_XML_FILE_NAME = 'core.xml'; @@ -81,7 +82,8 @@ class FileSystemHelper extends \Box\Spout\Common\Helper\FileSystemHelper ->createRootFolder() ->createRelsFolderAndFile() ->createDocPropsFolderAndFiles() - ->createXlFolderAndSubFolders(); + ->createXlFolderAndSubFolders() + ->createStylesFile(); } /** @@ -179,7 +181,8 @@ EOD; */ protected function createCoreXmlFile() { - $createdDate = (new \DateTime())->format('c'); + $dt = new \DateTime(); + $createdDate = $dt->format('c'); $coreXmlFileContents = << @@ -205,6 +208,7 @@ EOD; $this->xlFolder = $this->createFolder($this->rootFolder, self::XL_FOLDER_NAME); $this->createXlRelsFolder(); $this->createXlWorksheetsFolder(); + $this->createXlWorksheetsRelsFolder(); return $this; } @@ -233,6 +237,60 @@ EOD; return $this; } + /** + * Creates the "_rels" folder under the "worksheets" folder + * + * @return FileSystemHelper + * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder + */ + protected function createXlWorksheetsRelsFolder() + { + $this->createFolder($this->xlWorksheetsFolder, self::RELS_FOLDER_NAME); + return $this; + } + + /** + * Creates the "styles.xml" file under the "xl" folder + * + * @return FileSystemHelper + * @throws \Box\Spout\Common\Exception\IOException If unable to create the file + */ + protected function createStylesFile() + { + $stylesFileContents = << + + + + + + + + + + + + + + + + + + + + + + + + + +EOD; + + $this->createFileWithContents($this->xlFolder, self::STYLES_FILE_NAME, $stylesFileContents); + + return $this; + } + /** * Creates the "[Content_Types].xml" file under the root folder * @@ -247,6 +305,7 @@ EOD; + EOD; @@ -313,6 +372,7 @@ EOD; + EOD; diff --git a/src/Spout/Writer/Internal/XLSX/Workbook.php b/src/Spout/Writer/Internal/XLSX/Workbook.php index 3f8cca4..3261f16 100644 --- a/src/Spout/Writer/Internal/XLSX/Workbook.php +++ b/src/Spout/Writer/Internal/XLSX/Workbook.php @@ -36,7 +36,7 @@ class Workbook protected $sharedStringsHelper; /** @var Worksheet[] Array containing the workbook's sheets */ - protected $worksheets = []; + protected $worksheets = array(); /** @var Worksheet The worksheet where data will be written to */ protected $currentWorksheet; @@ -165,11 +165,12 @@ class Workbook * * @param array $dataRow Array containing data to be written. * Example $dataRow = ['data1', 1234, null, '', 'data5']; + * @param array $metaData Array containing meta-data maps for individual cells, such as 'url' * @return void * @throws \Box\Spout\Common\Exception\IOException If trying to create a new sheet and unable to open the sheet for writing * @throws \Box\Spout\Writer\Exception\WriterException If unable to write data */ - public function addRowToCurrentWorksheet($dataRow) + public function addRowToCurrentWorksheet($dataRow, $metaData) { $currentWorksheet = $this->getCurrentWorksheet(); $hasReachedMaxRows = $this->hasCurrentWorkseetReachedMaxRows(); @@ -179,12 +180,12 @@ class Workbook // ... continue writing in a new sheet if option set if ($this->shouldCreateNewSheetsAutomatically) { $currentWorksheet = $this->addNewSheetAndMakeItCurrent(); - $currentWorksheet->addRow($dataRow); + $currentWorksheet->addRow($dataRow, $metaData); } else { // otherwise, do nothing as the data won't be read anyways } } else { - $currentWorksheet->addRow($dataRow); + $currentWorksheet->addRow($dataRow, $metaData); } } diff --git a/src/Spout/Writer/Internal/XLSX/Worksheet.php b/src/Spout/Writer/Internal/XLSX/Worksheet.php index 326c41b..d49f065 100644 --- a/src/Spout/Writer/Internal/XLSX/Worksheet.php +++ b/src/Spout/Writer/Internal/XLSX/Worksheet.php @@ -25,6 +25,9 @@ EOD; /** @var string Path to the XML file that will contain the sheet data */ protected $worksheetFilePath; + /** @var string Path to the XML file that will contain the sheet rels data */ + protected $worksheetRelsFilePath; + /** @var \Box\Spout\Writer\Helper\XLSX\SharedStringsHelper Helper to write shared strings */ protected $sharedStringsHelper; @@ -40,6 +43,9 @@ EOD; /** @var int */ protected $lastWrittenRowIndex = 0; + /** @var array */ + protected $urls = array(); + /** * @param \Box\Spout\Writer\Sheet $externalSheet The associated "external" sheet * @param string $tempFolder Temporary folder where the files to create the XLSX will be stored @@ -55,6 +61,7 @@ EOD; $this->stringsEscaper = new \Box\Spout\Common\Escaper\XLSX(); $this->worksheetFilePath = $worksheetFilesFolder . DIRECTORY_SEPARATOR . strtolower($this->externalSheet->getName()) . '.xml'; + $this->worksheetRelsFilePath = $worksheetFilesFolder . DIRECTORY_SEPARATOR . '_rels' . DIRECTORY_SEPARATOR . strtolower($this->externalSheet->getName()) . '.xml.rels'; $this->startSheet(); } @@ -116,11 +123,17 @@ EOD; * * @param array $dataRow Array containing data to be written. * Example $dataRow = ['data1', 1234, null, '', 'data5']; + * @param array $metaData Array containing meta-data maps for individual cells, such as 'url' * @return void * @throws \Box\Spout\Common\Exception\IOException If the data cannot be written */ - public function addRow($dataRow) + public function addRow($dataRow, array $metaData = array()) { + if (count($dataRow) == 0) { + // Without this fix, we get a repair issue in regular Microsoft Excel + $dataRow=array(''); + } + $cellNumber = 0; $rowIndex = $this->lastWrittenRowIndex + 1; $numCells = count($dataRow); @@ -129,12 +142,14 @@ EOD; foreach($dataRow as $cellValue) { $columnIndex = CellHelper::getCellIndexFromColumnIndex($cellNumber); - $data .= ' ' . PHP_EOL; } else { if ($this->shouldUseInlineStrings) { @@ -146,6 +161,10 @@ EOD; } } + if (isset($metaData[$cellNumber]['url'])) { + $this->urls[$cellPath] = $metaData[$cellNumber]['url']; + } + $cellNumber++; } @@ -168,6 +187,34 @@ EOD; public function close() { fwrite($this->sheetFilePointer, ' ' . PHP_EOL); + + // Write out any hyperlinks + if (count($this->urls) != 0) { + fwrite($this->sheetFilePointer, ' ' . PHP_EOL); + $i = 0; + foreach ($this->urls as $cellPath => $url) { + $refID = 'rId' . ($i + 1); + fwrite($this->sheetFilePointer, ' ' . PHP_EOL); + $i++; + } + fwrite($this->sheetFilePointer, ' ' . PHP_EOL); + } + + // Write rels file + $sheetRelsFilePointer = fopen($this->worksheetRelsFilePath, 'w'); + if (!$sheetRelsFilePointer) throw new IOException('Unable to open rels sheet for writing.'); + fwrite($sheetRelsFilePointer, '' . PHP_EOL); + fwrite($sheetRelsFilePointer, '' . PHP_EOL); + $i = 0; + foreach ($this->urls as $url) { + $refID = 'rId' . ($i + 1); + fwrite($sheetRelsFilePointer, '' . PHP_EOL); + $i++; + } + fwrite($sheetRelsFilePointer, ''); + fclose($sheetRelsFilePointer); + + // Finish file fwrite($this->sheetFilePointer, ''); fclose($this->sheetFilePointer); } diff --git a/src/Spout/Writer/WriterFactory.php b/src/Spout/Writer/WriterFactory.php index 321f0f1..5635ff4 100644 --- a/src/Spout/Writer/WriterFactory.php +++ b/src/Spout/Writer/WriterFactory.php @@ -33,6 +33,12 @@ class WriterFactory case Type::XLSX: $writer = new XLSX(); break; + case Type::XLS: + $writer = new XLS(); + break; + case Type::HTM: + $writer = new HTM(); + break; default: throw new UnsupportedTypeException('No writers supporting the given type: ' . $writerType); } diff --git a/src/Spout/Writer/WriterInterface.php b/src/Spout/Writer/WriterInterface.php index 1324053..847c95a 100644 --- a/src/Spout/Writer/WriterInterface.php +++ b/src/Spout/Writer/WriterInterface.php @@ -34,11 +34,12 @@ interface WriterInterface * * @param array $dataRow Array containing data to be streamed. * Example $dataRow = ['data1', 1234, null, '', 'data5']; + * @param array $metaData Array containing meta-data maps for individual cells, such as 'url' * @return \Box\Spout\Writer\WriterInterface * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yetthe writer * @throws \Box\Spout\Common\Exception\IOException If unable to write data */ - public function addRow(array $dataRow); + public function addRow(array $dataRow, array $metaData = array()); /** * Write given data to the output. New data will be appended to end of stream. diff --git a/src/Spout/Writer/XLS.php b/src/Spout/Writer/XLS.php new file mode 100644 index 0000000..cf91d2c --- /dev/null +++ b/src/Spout/Writer/XLS.php @@ -0,0 +1,194 @@ +col = 0; + $this->row = 0; + $this->bofMarker(); + } + + /** + * Writes the Excel Beginning of File marker + * + * @see pack() + * @return nothing + */ + private function bofMarker() + { + fwrite($this->filePointer, pack("ssssss", 0x809, 0x8, 0x0, 0x10, 0x0, 0x0)); + } + + /** + * Moves internal cursor all the way left, col = 0 + * + * @return nothing + */ + function home() + { + $this->col = 0; + } + + /** + * Moves internal cursor right by the amount specified + * + * @param optional integer $amount The amount to move right by, defaults to 1 + * @return integer The current column after the move + */ + function right($amount = 1) + { + $this->col += $amount; + return $this->col; + } + + /** + * Moves internal cursor down by amount + * + * @param optional integer $amount The amount to move down by, defaults to 1 + * @return integer The current row after the move + */ + function down($amount = 1) + { + $this->row += $amount; + return $this->row; + } + + /** + * Writes a number to the Excel Spreadsheet + * + * @see pack() + * @param integer $value The value to write out + * @return boolean Success status + */ + function number($value) + { + $wasWriteSuccessful = true; + $wasWriteSuccessful = $wasWriteSuccessful && fwrite($this->filePointer, pack("sssss", 0x203, 14, $this->row, $this->col, 0x0)); + $wasWriteSuccessful = $wasWriteSuccessful && fwrite($this->filePointer, pack("d", $value)); + return $wasWriteSuccessful; + } + + /** + * Writes a string (or label) to the Excel Spreadsheet + * + * @see pack() + * @param string $value The value to write out + * @return boolean Success status + */ + function label($value) + { + $value = str_replace(array("\r\n", "\r"), "\n", $value); + + // We're doing BIFF5, not BIFF8 - meaning a 255 char limit. If you want something good, use XLSX, else XLS for compatibility. + if (strlen($value) >= 255) { + $value = substr($value, 0, 255); + } + + $length = strlen($value); + $wasWriteSuccessful = true; + $wasWriteSuccessful = $wasWriteSuccessful && fwrite($this->filePointer, pack("ssssss", 0x204, 8 + $length, $this->row, $this->col, 0x0, $length)); + if ($value !='') { + $wasWriteSuccessful = $wasWriteSuccessful && fwrite($this->filePointer, $value); + } + return $wasWriteSuccessful; + } + + /** + * Adds data to the currently opened writer. + * + * @param array $dataRow Array containing data to be written. + * Example $dataRow = ['data1', 1234, null, '', 'data5']; + * @param array $metaData Array containing meta-data maps for individual cells, such as 'url' + * @return void + * @throws \Box\Spout\Common\Exception\IOException If unable to write data + */ + protected function addRowToWriter(array $dataRow, array $metaData) + { + if ($this->row == 65536) { + // Hit the limit. You should have chosen XLSX + return; + } + + $wasWriteSuccessful = true; + + $this->home(); + foreach ($dataRow as $cell) { + if (trim($cell, '0123456789.') == '' /*similar to is_numeric without having PHPs regular quirkiness*/) { + $wasWriteSuccessful = $wasWriteSuccessful && $this->number($cell); + } else + { + $wasWriteSuccessful = $wasWriteSuccessful && $this->label($cell); + } + $this->right(); + } + $this->down(); + + if ($wasWriteSuccessful === false) { + throw new IOException('Unable to write data'); + } + + if ($this->row % self::FLUSH_THRESHOLD === 0) { + $this->globalFunctionsHelper->fflush($this->filePointer); + } + } + + /** + * Writes the Excel End of File marker + * + * @see pack() + * @return nothing + */ + private function eofMarker() + { + fwrite($this->filePointer, pack("ss", 0x0A, 0x00)); + } + + /** + * Closes the XLS streamer, preventing any additional writing. + * If set, sets the headers and redirects output to the browser. + * + * @return void + */ + protected function closeWriter() + { + if ($this->filePointer) { + $this->eofMarker(); + + $this->globalFunctionsHelper->fclose($this->filePointer); + } + + $this->row = 0; + $this->col = 0; + } +} diff --git a/src/Spout/Writer/XLSX.php b/src/Spout/Writer/XLSX.php index 62e98f1..2b470c8 100644 --- a/src/Spout/Writer/XLSX.php +++ b/src/Spout/Writer/XLSX.php @@ -88,7 +88,7 @@ class XLSX extends AbstractWriter { $this->throwIfBookIsNotAvailable(); - $externalSheets = []; + $externalSheets = array(); $worksheets = $this->book->getWorksheets(); /** @var Internal\XLSX\Worksheet $worksheet */ @@ -160,14 +160,15 @@ class XLSX extends AbstractWriter * * @param array $dataRow Array containing data to be written. * Example $dataRow = ['data1', 1234, null, '', 'data5']; + * @param array $metaData Array containing meta-data maps for individual cells, such as 'url' * @return void * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the book is not created yet * @throws \Box\Spout\Common\Exception\IOException If unable to write data */ - protected function addRowToWriter(array $dataRow) + protected function addRowToWriter(array $dataRow, array $metaData) { $this->throwIfBookIsNotAvailable(); - $this->book->addRowToCurrentWorksheet($dataRow); + $this->book->addRowToCurrentWorksheet($dataRow, $metaData); } /**