File manager - Edit - /home/monara/public_html/test.athavaneng.com/Spout.tar
Back
Autoloader/Psr4Autoloader.php 0000644 00000010760 15073227061 0012233 0 ustar 00 <?php namespace Box\Spout\Autoloader; /** * Class Psr4Autoloader * @see https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader-examples.md#class-example * * @package Box\Spout\Autoloader */ class Psr4Autoloader { /** * An associative array where the key is a namespace prefix and the value * is an array of base directories for classes in that namespace. * * @var array */ protected $prefixes = array(); /** * Register loader with SPL autoloader stack. * * @return void */ public function register() { spl_autoload_register(array($this, 'loadClass')); } /** * Adds a base directory for a namespace prefix. * * @param string $prefix The namespace prefix. * @param string $baseDir A base directory for class files in the * namespace. * @param bool $prepend If true, prepend the base directory to the stack * instead of appending it; this causes it to be searched first rather * than last. * @return void */ public function addNamespace($prefix, $baseDir, $prepend = false) { // normalize namespace prefix $prefix = trim($prefix, '\\') . '\\'; // normalize the base directory with a trailing separator $baseDir = rtrim($baseDir, DIRECTORY_SEPARATOR) . '/'; // initialize the namespace prefix array if (isset($this->prefixes[$prefix]) === false) { $this->prefixes[$prefix] = array(); } // retain the base directory for the namespace prefix if ($prepend) { array_unshift($this->prefixes[$prefix], $baseDir); } else { array_push($this->prefixes[$prefix], $baseDir); } } /** * Loads the class file for a given class name. * * @param string $class The fully-qualified class name. * @return mixed The mapped file name on success, or boolean false on * failure. */ public function loadClass($class) { // the current namespace prefix $prefix = $class; // work backwards through the namespace names of the fully-qualified // class name to find a mapped file name while (false !== $pos = strrpos($prefix, '\\')) { // retain the trailing namespace separator in the prefix $prefix = substr($class, 0, $pos + 1); // the rest is the relative class name $relativeClass = substr($class, $pos + 1); // try to load a mapped file for the prefix and relative class $mappedFile = $this->loadMappedFile($prefix, $relativeClass); if ($mappedFile !== false) { return $mappedFile; } // remove the trailing namespace separator for the next iteration // of strrpos() $prefix = rtrim($prefix, '\\'); } // never found a mapped file return false; } /** * Load the mapped file for a namespace prefix and relative class. * * @param string $prefix The namespace prefix. * @param string $relativeClass The relative class name. * @return mixed Boolean false if no mapped file can be loaded, or the * name of the mapped file that was loaded. */ protected function loadMappedFile($prefix, $relativeClass) { // are there any base directories for this namespace prefix? if (isset($this->prefixes[$prefix]) === false) { return false; } // look through base directories for this namespace prefix foreach ($this->prefixes[$prefix] as $baseDir) { // replace the namespace prefix with the base directory, // replace namespace separators with directory separators // in the relative class name, append with .php $file = $baseDir . str_replace('\\', '/', $relativeClass) . '.php'; // if the mapped file exists, require it if ($this->requireFile($file)) { // yes, we're done return $file; } } // never found it return false; } /** * If a file exists, require it from the file system. * * @param string $file The file to require. * @return bool True if the file exists, false if not. */ protected function requireFile($file) { if (file_exists($file)) { require $file; return true; } return false; } } Autoloader/autoload.php 0000644 00000000533 15073227061 0011170 0 ustar 00 <?php namespace Box\Spout\Autoloader; require_once 'Psr4Autoloader.php'; /** * @var string $srcBaseDirectory * Full path to "src/Spout" which is what we want "Box\Spout" to map to. */ $srcBaseDirectory = dirname(dirname(__FILE__)); $loader = new Psr4Autoloader(); $loader->register(); $loader->addNamespace('Box\Spout', $srcBaseDirectory); Reader/SheetInterface.php 0000644 00000000430 15073227061 0011350 0 ustar 00 <?php namespace Box\Spout\Reader; /** * Interface SheetInterface * * @package Box\Spout\Reader */ interface SheetInterface { /** * Returns an iterator to iterate over the sheet's rows. * * @return \Iterator */ public function getRowIterator(); } Reader/IteratorInterface.php 0000644 00000000441 15073227061 0012073 0 ustar 00 <?php namespace Box\Spout\Reader; /** * Interface IteratorInterface * * @package Box\Spout\Reader */ interface IteratorInterface extends \Iterator { /** * Cleans up what was created to iterate over the object. * * @return void */ public function end(); } Reader/XLSX/RowIterator.php 0000644 00000035667 15073227061 0011562 0 ustar 00 <?php namespace Box\Spout\Reader\XLSX; use Box\Spout\Common\Exception\IOException; use Box\Spout\Reader\Exception\XMLProcessingException; use Box\Spout\Reader\IteratorInterface; use Box\Spout\Reader\Wrapper\XMLReader; use Box\Spout\Reader\XLSX\Helper\CellHelper; use Box\Spout\Reader\XLSX\Helper\CellValueFormatter; use Box\Spout\Reader\XLSX\Helper\StyleHelper; use Box\Spout\Reader\Common\XMLProcessor; /** * Class RowIterator * * @package Box\Spout\Reader\XLSX */ class RowIterator implements IteratorInterface { /** Definition of XML nodes names used to parse data */ const XML_NODE_DIMENSION = 'dimension'; const XML_NODE_WORKSHEET = 'worksheet'; const XML_NODE_ROW = 'row'; const XML_NODE_CELL = 'c'; /** Definition of XML attributes used to parse data */ const XML_ATTRIBUTE_REF = 'ref'; const XML_ATTRIBUTE_SPANS = 'spans'; const XML_ATTRIBUTE_ROW_INDEX = 'r'; const XML_ATTRIBUTE_CELL_INDEX = 'r'; /** @var string Path of the XLSX file being read */ protected $filePath; /** @var string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml */ protected $sheetDataXMLFilePath; /** @var \Box\Spout\Reader\Wrapper\XMLReader The XMLReader object that will help read sheet's XML data */ protected $xmlReader; /** @var \Box\Spout\Reader\Common\XMLProcessor Helper Object to process XML nodes */ protected $xmlProcessor; /** @var Helper\CellValueFormatter Helper to format cell values */ protected $cellValueFormatter; /** @var Helper\StyleHelper $styleHelper Helper to work with styles */ protected $styleHelper; /** * TODO: This variable can be deleted when row indices get preserved * @var int Number of read rows */ protected $numReadRows = 0; /** @var array Contains the data for the currently processed row (key = cell index, value = cell value) */ protected $currentlyProcessedRowData = []; /** @var array|null Buffer used to store the row data, while checking if there are more rows to read */ protected $rowDataBuffer = null; /** @var bool Indicates whether all rows have been read */ protected $hasReachedEndOfFile = false; /** @var int The number of columns the sheet has (0 meaning undefined) */ protected $numColumns = 0; /** @var bool Whether empty rows should be returned or skipped */ protected $shouldPreserveEmptyRows; /** @var int Last row index processed (one-based) */ protected $lastRowIndexProcessed = 0; /** @var int Row index to be processed next (one-based) */ protected $nextRowIndexToBeProcessed = 0; /** @var int Last column index processed (zero-based) */ protected $lastColumnIndexProcessed = -1; /** * @param string $filePath Path of the XLSX file being read * @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml * @param \Box\Spout\Reader\XLSX\ReaderOptions $options Reader's current options * @param Helper\SharedStringsHelper $sharedStringsHelper Helper to work with shared strings */ public function __construct($filePath, $sheetDataXMLFilePath, $options, $sharedStringsHelper) { $this->filePath = $filePath; $this->sheetDataXMLFilePath = $this->normalizeSheetDataXMLFilePath($sheetDataXMLFilePath); $this->xmlReader = new XMLReader(); $this->styleHelper = new StyleHelper($filePath); $this->cellValueFormatter = new CellValueFormatter($sharedStringsHelper, $this->styleHelper, $options->shouldFormatDates()); $this->shouldPreserveEmptyRows = $options->shouldPreserveEmptyRows(); // Register all callbacks to process different nodes when reading the XML file $this->xmlProcessor = new XMLProcessor($this->xmlReader); $this->xmlProcessor->registerCallback(self::XML_NODE_DIMENSION, XMLProcessor::NODE_TYPE_START, [$this, 'processDimensionStartingNode']); $this->xmlProcessor->registerCallback(self::XML_NODE_ROW, XMLProcessor::NODE_TYPE_START, [$this, 'processRowStartingNode']); $this->xmlProcessor->registerCallback(self::XML_NODE_CELL, XMLProcessor::NODE_TYPE_START, [$this, 'processCellStartingNode']); $this->xmlProcessor->registerCallback(self::XML_NODE_ROW, XMLProcessor::NODE_TYPE_END, [$this, 'processRowEndingNode']); $this->xmlProcessor->registerCallback(self::XML_NODE_WORKSHEET, XMLProcessor::NODE_TYPE_END, [$this, 'processWorksheetEndingNode']); } /** * @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml * @return string Path of the XML file containing the sheet data, * without the leading slash. */ protected function normalizeSheetDataXMLFilePath($sheetDataXMLFilePath) { return ltrim($sheetDataXMLFilePath, '/'); } /** * Rewind the Iterator to the first element. * Initializes the XMLReader object that reads the associated sheet data. * The XMLReader is configured to be safe from billion laughs attack. * @link http://php.net/manual/en/iterator.rewind.php * * @return void * @throws \Box\Spout\Common\Exception\IOException If the sheet data XML cannot be read */ public function rewind() { $this->xmlReader->close(); if ($this->xmlReader->openFileInZip($this->filePath, $this->sheetDataXMLFilePath) === false) { throw new IOException("Could not open \"{$this->sheetDataXMLFilePath}\"."); } $this->numReadRows = 0; $this->lastRowIndexProcessed = 0; $this->nextRowIndexToBeProcessed = 0; $this->rowDataBuffer = null; $this->hasReachedEndOfFile = false; $this->numColumns = 0; $this->next(); } /** * Checks if current position is valid * @link http://php.net/manual/en/iterator.valid.php * * @return bool */ public function valid() { return (!$this->hasReachedEndOfFile); } /** * Move forward to next element. Reads data describing the next unprocessed row. * @link http://php.net/manual/en/iterator.next.php * * @return void * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If a shared string was not found * @throws \Box\Spout\Common\Exception\IOException If unable to read the sheet data XML */ public function next() { $this->nextRowIndexToBeProcessed++; if ($this->doesNeedDataForNextRowToBeProcessed()) { $this->readDataForNextRow(); } } /** * Returns whether we need data for the next row to be processed. * We don't need to read data if: * we have already read at least one row * AND * we need to preserve empty rows * AND * the last row that was read is not the row that need to be processed * (i.e. if we need to return empty rows) * * @return bool Whether we need data for the next row to be processed. */ protected function doesNeedDataForNextRowToBeProcessed() { $hasReadAtLeastOneRow = ($this->lastRowIndexProcessed !== 0); return ( !$hasReadAtLeastOneRow || !$this->shouldPreserveEmptyRows || $this->lastRowIndexProcessed < $this->nextRowIndexToBeProcessed ); } /** * @return void * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If a shared string was not found * @throws \Box\Spout\Common\Exception\IOException If unable to read the sheet data XML */ protected function readDataForNextRow() { $this->currentlyProcessedRowData = []; try { $this->xmlProcessor->readUntilStopped(); } catch (XMLProcessingException $exception) { throw new IOException("The {$this->sheetDataXMLFilePath} file cannot be read. [{$exception->getMessage()}]"); } $this->rowDataBuffer = $this->currentlyProcessedRowData; } /** * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<dimension>" starting node * @return int A return code that indicates what action should the processor take next */ protected function processDimensionStartingNode($xmlReader) { // Read dimensions of the sheet $dimensionRef = $xmlReader->getAttribute(self::XML_ATTRIBUTE_REF); // returns 'A1:M13' for instance (or 'A1' for empty sheet) if (preg_match('/[A-Z]+\d+:([A-Z]+\d+)/', $dimensionRef, $matches)) { $this->numColumns = CellHelper::getColumnIndexFromCellIndex($matches[1]) + 1; } return XMLProcessor::PROCESSING_CONTINUE; } /** * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<row>" starting node * @return int A return code that indicates what action should the processor take next */ protected function processRowStartingNode($xmlReader) { // Reset index of the last processed column $this->lastColumnIndexProcessed = -1; // Mark the last processed row as the one currently being read $this->lastRowIndexProcessed = $this->getRowIndex($xmlReader); // Read spans info if present $numberOfColumnsForRow = $this->numColumns; $spans = $xmlReader->getAttribute(self::XML_ATTRIBUTE_SPANS); // returns '1:5' for instance if ($spans) { list(, $numberOfColumnsForRow) = explode(':', $spans); $numberOfColumnsForRow = intval($numberOfColumnsForRow); } $this->currentlyProcessedRowData = ($numberOfColumnsForRow !== 0) ? array_fill(0, $numberOfColumnsForRow, '') : []; return XMLProcessor::PROCESSING_CONTINUE; } /** * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<cell>" starting node * @return int A return code that indicates what action should the processor take next */ protected function processCellStartingNode($xmlReader) { $currentColumnIndex = $this->getColumnIndex($xmlReader); // NOTE: expand() will automatically decode all XML entities of the child nodes $node = $xmlReader->expand(); $this->currentlyProcessedRowData[$currentColumnIndex] = $this->getCellValue($node); $this->lastColumnIndexProcessed = $currentColumnIndex; return XMLProcessor::PROCESSING_CONTINUE; } /** * @return int A return code that indicates what action should the processor take next */ protected function processRowEndingNode() { // if the fetched row is empty and we don't want to preserve it.., if (!$this->shouldPreserveEmptyRows && $this->isEmptyRow($this->currentlyProcessedRowData)) { // ... skip it return XMLProcessor::PROCESSING_CONTINUE; } $this->numReadRows++; // If needed, we fill the empty cells if ($this->numColumns === 0) { $this->currentlyProcessedRowData = CellHelper::fillMissingArrayIndexes($this->currentlyProcessedRowData); } // at this point, we have all the data we need for the row // so that we can populate the buffer return XMLProcessor::PROCESSING_STOP; } /** * @return int A return code that indicates what action should the processor take next */ protected function processWorksheetEndingNode() { // The closing "</worksheet>" marks the end of the file $this->hasReachedEndOfFile = true; return XMLProcessor::PROCESSING_STOP; } /** * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<row>" node * @return int Row index * @throws \Box\Spout\Common\Exception\InvalidArgumentException When the given cell index is invalid */ protected function getRowIndex($xmlReader) { // Get "r" attribute if present (from something like <row r="3"...> $currentRowIndex = $xmlReader->getAttribute(self::XML_ATTRIBUTE_ROW_INDEX); return ($currentRowIndex !== null) ? intval($currentRowIndex) : $this->lastRowIndexProcessed + 1; } /** * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<c>" node * @return int Column index * @throws \Box\Spout\Common\Exception\InvalidArgumentException When the given cell index is invalid */ protected function getColumnIndex($xmlReader) { // Get "r" attribute if present (from something like <c r="A1"...> $currentCellIndex = $xmlReader->getAttribute(self::XML_ATTRIBUTE_CELL_INDEX); return ($currentCellIndex !== null) ? CellHelper::getColumnIndexFromCellIndex($currentCellIndex) : $this->lastColumnIndexProcessed + 1; } /** * Returns the (unescaped) correctly marshalled, cell value associated to the given XML node. * * @param \DOMNode $node * @return string|int|float|bool|\DateTime|null The value associated with the cell (null when the cell has an error) */ protected function getCellValue($node) { return $this->cellValueFormatter->extractAndFormatNodeValue($node); } /** * @param array $rowData * @return bool Whether the given row is empty */ protected function isEmptyRow($rowData) { return (count($rowData) === 1 && key($rowData) === ''); } /** * Return the current element, either an empty row or from the buffer. * @link http://php.net/manual/en/iterator.current.php * * @return array|null */ public function current() { $rowDataForRowToBeProcessed = $this->rowDataBuffer; if ($this->shouldPreserveEmptyRows) { // when we need to preserve empty rows, we will either return // an empty row or the last row read. This depends whether the // index of last row that was read matches the index of the last // row whose value should be returned. if ($this->lastRowIndexProcessed !== $this->nextRowIndexToBeProcessed) { // return empty row if mismatch between last processed row // and the row that needs to be returned $rowDataForRowToBeProcessed = ['']; } } return $rowDataForRowToBeProcessed; } /** * Return the key of the current element. Here, the row index. * @link http://php.net/manual/en/iterator.key.php * * @return int */ public function key() { // TODO: This should return $this->nextRowIndexToBeProcessed // but to avoid a breaking change, the return value for // this function has been kept as the number of rows read. return $this->shouldPreserveEmptyRows ? $this->nextRowIndexToBeProcessed : $this->numReadRows; } /** * Cleans up what was created to iterate over the object. * * @return void */ public function end() { $this->xmlReader->close(); } } Reader/XLSX/Helper/DateFormatHelper.php 0000644 00000013404 15073227061 0013667 0 ustar 00 <?php namespace Box\Spout\Reader\XLSX\Helper; /** * Class DateFormatHelper * This class provides helper functions to format Excel dates * * @package Box\Spout\Reader\XLSX\Helper */ class DateFormatHelper { const KEY_GENERAL = 'general'; const KEY_HOUR_12 = '12h'; const KEY_HOUR_24 = '24h'; /** * This map is used to replace Excel format characters by their PHP equivalent. * Keys should be ordered from longest to smallest. * * @var array Mapping between Excel format characters and PHP format characters */ private static $excelDateFormatToPHPDateFormatMapping = [ self::KEY_GENERAL => [ // Time 'am/pm' => 'A', // Uppercase Ante meridiem and Post meridiem ':mm' => ':i', // Minutes with leading zeros - if preceded by a ":" (otherwise month) 'mm:' => 'i:', // Minutes with leading zeros - if followed by a ":" (otherwise month) 'ss' => 's', // Seconds, with leading zeros '.s' => '', // Ignore (fractional seconds format does not exist in PHP) // Date 'e' => 'Y', // Full numeric representation of a year, 4 digits 'yyyy' => 'Y', // Full numeric representation of a year, 4 digits 'yy' => 'y', // Two digit representation of a year 'mmmmm' => 'M', // Short textual representation of a month, three letters ("mmmmm" should only contain the 1st letter...) 'mmmm' => 'F', // Full textual representation of a month 'mmm' => 'M', // Short textual representation of a month, three letters 'mm' => 'm', // Numeric representation of a month, with leading zeros 'm' => 'n', // Numeric representation of a month, without leading zeros 'dddd' => 'l', // Full textual representation of the day of the week 'ddd' => 'D', // Textual representation of a day, three letters 'dd' => 'd', // Day of the month, 2 digits with leading zeros 'd' => 'j', // Day of the month without leading zeros ], self::KEY_HOUR_12 => [ 'hh' => 'h', // 12-hour format of an hour without leading zeros 'h' => 'g', // 12-hour format of an hour without leading zeros ], self::KEY_HOUR_24 => [ 'hh' => 'H', // 24-hour hours with leading zero 'h' => 'G', // 24-hour format of an hour without leading zeros ], ]; /** * Converts the given Excel date format to a format understandable by the PHP date function. * * @param string $excelDateFormat Excel date format * @return string PHP date format (as defined here: http://php.net/manual/en/function.date.php) */ public static function toPHPDateFormat($excelDateFormat) { // Remove brackets potentially present at the beginning of the format string // and text portion of the format at the end of it (starting with ";") // See §18.8.31 of ECMA-376 for more detail. $dateFormat = preg_replace('/^(?:\[\$[^\]]+?\])?([^;]*).*/', '$1', $excelDateFormat); // Double quotes are used to escape characters that must not be interpreted. // For instance, ["Day " dd] should result in "Day 13" and we should not try to interpret "D", "a", "y" // By exploding the format string using double quote as a delimiter, we can get all parts // that must be transformed (even indexes) and all parts that must not be (odd indexes). $dateFormatParts = explode('"', $dateFormat); foreach ($dateFormatParts as $partIndex => $dateFormatPart) { // do not look at odd indexes if ($partIndex % 2 === 1) { continue; } // Make sure all characters are lowercase, as the mapping table is using lowercase characters $transformedPart = strtolower($dateFormatPart); // Remove escapes related to non-format characters $transformedPart = str_replace('\\', '', $transformedPart); // Apply general transformation first... $transformedPart = strtr($transformedPart, self::$excelDateFormatToPHPDateFormatMapping[self::KEY_GENERAL]); // ... then apply hour transformation, for 12-hour or 24-hour format if (self::has12HourFormatMarker($dateFormatPart)) { $transformedPart = strtr($transformedPart, self::$excelDateFormatToPHPDateFormatMapping[self::KEY_HOUR_12]); } else { $transformedPart = strtr($transformedPart, self::$excelDateFormatToPHPDateFormatMapping[self::KEY_HOUR_24]); } // overwrite the parts array with the new transformed part $dateFormatParts[$partIndex] = $transformedPart; } // Merge all transformed parts back together $phpDateFormat = implode('"', $dateFormatParts); // Finally, to have the date format compatible with the DateTime::format() function, we need to escape // all characters that are inside double quotes (and double quotes must be removed). // For instance, ["Day " dd] should become [\D\a\y\ dd] $phpDateFormat = preg_replace_callback('/"(.+?)"/', function($matches) { $stringToEscape = $matches[1]; $letters = preg_split('//u', $stringToEscape, -1, PREG_SPLIT_NO_EMPTY); return '\\' . implode('\\', $letters); }, $phpDateFormat); return $phpDateFormat; } /** * @param string $excelDateFormat Date format as defined by Excel * @return bool Whether the given date format has the 12-hour format marker */ private static function has12HourFormatMarker($excelDateFormat) { return (stripos($excelDateFormat, 'am/pm') !== false); } } Reader/XLSX/Helper/CellValueFormatter.php 0000644 00000027105 15073227061 0014244 0 ustar 00 <?php namespace Box\Spout\Reader\XLSX\Helper; /** * Class CellValueFormatter * This class provides helper functions to format cell values * * @package Box\Spout\Reader\XLSX\Helper */ class CellValueFormatter { /** Definition of all possible cell types */ const CELL_TYPE_INLINE_STRING = 'inlineStr'; const CELL_TYPE_STR = 'str'; const CELL_TYPE_SHARED_STRING = 's'; const CELL_TYPE_BOOLEAN = 'b'; const CELL_TYPE_NUMERIC = 'n'; const CELL_TYPE_DATE = 'd'; const CELL_TYPE_ERROR = 'e'; /** Definition of XML nodes names used to parse data */ const XML_NODE_VALUE = 'v'; const XML_NODE_INLINE_STRING_VALUE = 't'; /** Definition of XML attributes used to parse data */ const XML_ATTRIBUTE_TYPE = 't'; const XML_ATTRIBUTE_STYLE_ID = 's'; /** Constants used for date formatting */ const NUM_SECONDS_IN_ONE_DAY = 86400; const NUM_SECONDS_IN_ONE_HOUR = 3600; const NUM_SECONDS_IN_ONE_MINUTE = 60; /** * February 29th, 1900 is NOT a leap year but Excel thinks it is... * @see https://en.wikipedia.org/wiki/Year_1900_problem#Microsoft_Excel */ const ERRONEOUS_EXCEL_LEAP_YEAR_DAY = 60; /** @var SharedStringsHelper Helper to work with shared strings */ protected $sharedStringsHelper; /** @var StyleHelper Helper to work with styles */ protected $styleHelper; /** @var bool Whether date/time values should be returned as PHP objects or be formatted as strings */ protected $shouldFormatDates; /** @var \Box\Spout\Common\Escaper\XLSX Used to unescape XML data */ protected $escaper; /** * @param SharedStringsHelper $sharedStringsHelper Helper to work with shared strings * @param StyleHelper $styleHelper Helper to work with styles * @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings */ public function __construct($sharedStringsHelper, $styleHelper, $shouldFormatDates) { $this->sharedStringsHelper = $sharedStringsHelper; $this->styleHelper = $styleHelper; $this->shouldFormatDates = $shouldFormatDates; /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ $this->escaper = \Box\Spout\Common\Escaper\XLSX::getInstance(); } /** * Returns the (unescaped) correctly marshalled, cell value associated to the given XML node. * * @param \DOMNode $node * @return string|int|float|bool|\DateTime|null The value associated with the cell (null when the cell has an error) */ public function extractAndFormatNodeValue($node) { // Default cell type is "n" $cellType = $node->getAttribute(self::XML_ATTRIBUTE_TYPE) ?: self::CELL_TYPE_NUMERIC; $cellStyleId = intval($node->getAttribute(self::XML_ATTRIBUTE_STYLE_ID)); $vNodeValue = $this->getVNodeValue($node); if (($vNodeValue === '') && ($cellType !== self::CELL_TYPE_INLINE_STRING)) { return $vNodeValue; } switch ($cellType) { case self::CELL_TYPE_INLINE_STRING: return $this->formatInlineStringCellValue($node); case self::CELL_TYPE_SHARED_STRING: return $this->formatSharedStringCellValue($vNodeValue); case self::CELL_TYPE_STR: return $this->formatStrCellValue($vNodeValue); case self::CELL_TYPE_BOOLEAN: return $this->formatBooleanCellValue($vNodeValue); case self::CELL_TYPE_NUMERIC: return $this->formatNumericCellValue($vNodeValue, $cellStyleId); case self::CELL_TYPE_DATE: return $this->formatDateCellValue($vNodeValue); default: return null; } } /** * Returns the cell's string value from a node's nested value node * * @param \DOMNode $node * @return string The value associated with the cell */ protected function getVNodeValue($node) { // for cell types having a "v" tag containing the value. // if not, the returned value should be empty string. $vNode = $node->getElementsByTagName(self::XML_NODE_VALUE)->item(0); return ($vNode !== null) ? $vNode->nodeValue : ''; } /** * Returns the cell String value where string is inline. * * @param \DOMNode $node * @return string The value associated with the cell (null when the cell has an error) */ protected function formatInlineStringCellValue($node) { // inline strings are formatted this way: // <c r="A1" t="inlineStr"><is><t>[INLINE_STRING]</t></is></c> $tNode = $node->getElementsByTagName(self::XML_NODE_INLINE_STRING_VALUE)->item(0); $cellValue = $this->escaper->unescape($tNode->nodeValue); return $cellValue; } /** * Returns the cell String value from shared-strings file using nodeValue index. * * @param string $nodeValue * @return string The value associated with the cell (null when the cell has an error) */ protected function formatSharedStringCellValue($nodeValue) { // shared strings are formatted this way: // <c r="A1" t="s"><v>[SHARED_STRING_INDEX]</v></c> $sharedStringIndex = intval($nodeValue); $escapedCellValue = $this->sharedStringsHelper->getStringAtIndex($sharedStringIndex); $cellValue = $this->escaper->unescape($escapedCellValue); return $cellValue; } /** * Returns the cell String value, where string is stored in value node. * * @param string $nodeValue * @return string The value associated with the cell (null when the cell has an error) */ protected function formatStrCellValue($nodeValue) { $escapedCellValue = trim($nodeValue); $cellValue = $this->escaper->unescape($escapedCellValue); return $cellValue; } /** * Returns the cell Numeric value from string of nodeValue. * The value can also represent a timestamp and a DateTime will be returned. * * @param string $nodeValue * @param int $cellStyleId 0 being the default style * @return int|float|\DateTime|null The value associated with the cell */ protected function formatNumericCellValue($nodeValue, $cellStyleId) { // Numeric values can represent numbers as well as timestamps. // We need to look at the style of the cell to determine whether it is one or the other. $shouldFormatAsDate = $this->styleHelper->shouldFormatNumericValueAsDate($cellStyleId); if ($shouldFormatAsDate) { return $this->formatExcelTimestampValue(floatval($nodeValue), $cellStyleId); } else { $nodeIntValue = intval($nodeValue); return ($nodeIntValue == $nodeValue) ? $nodeIntValue : floatval($nodeValue); } } /** * Returns a cell's PHP Date value, associated to the given timestamp. * NOTE: The timestamp is a float representing the number of days since January 1st, 1900. * NOTE: The timestamp can also represent a time, if it is a value between 0 and 1. * * @param float $nodeValue * @param int $cellStyleId 0 being the default style * @return \DateTime|null The value associated with the cell or NULL if invalid date value */ protected function formatExcelTimestampValue($nodeValue, $cellStyleId) { // Fix for the erroneous leap year in Excel if (ceil($nodeValue) > self::ERRONEOUS_EXCEL_LEAP_YEAR_DAY) { --$nodeValue; } if ($nodeValue >= 1) { // Values greater than 1 represent "dates". The value 1.0 representing the "base" date: 1900-01-01. return $this->formatExcelTimestampValueAsDateValue($nodeValue, $cellStyleId); } else if ($nodeValue >= 0) { // Values between 0 and 1 represent "times". return $this->formatExcelTimestampValueAsTimeValue($nodeValue, $cellStyleId); } else { // invalid date return null; } } /** * Returns a cell's PHP DateTime value, associated to the given timestamp. * Only the time value matters. The date part is set to Jan 1st, 1900 (base Excel date). * * @param float $nodeValue * @param int $cellStyleId 0 being the default style * @return \DateTime|string The value associated with the cell */ protected function formatExcelTimestampValueAsTimeValue($nodeValue, $cellStyleId) { $time = round($nodeValue * self::NUM_SECONDS_IN_ONE_DAY); $hours = floor($time / self::NUM_SECONDS_IN_ONE_HOUR); $minutes = floor($time / self::NUM_SECONDS_IN_ONE_MINUTE) - ($hours * self::NUM_SECONDS_IN_ONE_MINUTE); $seconds = $time - ($hours * self::NUM_SECONDS_IN_ONE_HOUR) - ($minutes * self::NUM_SECONDS_IN_ONE_MINUTE); // using the base Excel date (Jan 1st, 1900) - not relevant here $dateObj = new \DateTime('1900-01-01'); $dateObj->setTime($hours, $minutes, $seconds); if ($this->shouldFormatDates) { $styleNumberFormatCode = $this->styleHelper->getNumberFormatCode($cellStyleId); $phpDateFormat = DateFormatHelper::toPHPDateFormat($styleNumberFormatCode); return $dateObj->format($phpDateFormat); } else { return $dateObj; } } /** * Returns a cell's PHP Date value, associated to the given timestamp. * NOTE: The timestamp is a float representing the number of days since January 1st, 1900. * * @param float $nodeValue * @param int $cellStyleId 0 being the default style * @return \DateTime|string|null The value associated with the cell or NULL if invalid date value */ protected function formatExcelTimestampValueAsDateValue($nodeValue, $cellStyleId) { // Do not use any unix timestamps for calculation to prevent // issues with numbers exceeding 2^31. $secondsRemainder = fmod($nodeValue, 1) * self::NUM_SECONDS_IN_ONE_DAY; $secondsRemainder = round($secondsRemainder, 0); try { $dateObj = \DateTime::createFromFormat('|Y-m-d', '1899-12-31'); $dateObj->modify('+' . intval($nodeValue) . 'days'); $dateObj->modify('+' . $secondsRemainder . 'seconds'); if ($this->shouldFormatDates) { $styleNumberFormatCode = $this->styleHelper->getNumberFormatCode($cellStyleId); $phpDateFormat = DateFormatHelper::toPHPDateFormat($styleNumberFormatCode); return $dateObj->format($phpDateFormat); } else { return $dateObj; } } catch (\Exception $e) { return null; } } /** * Returns the cell Boolean value from a specific node's Value. * * @param string $nodeValue * @return bool The value associated with the cell */ protected function formatBooleanCellValue($nodeValue) { // !! is similar to boolval() $cellValue = !!$nodeValue; return $cellValue; } /** * Returns a cell's PHP Date value, associated to the given stored nodeValue. * @see ECMA-376 Part 1 - §18.17.4 * * @param string $nodeValue ISO 8601 Date string * @return \DateTime|string|null The value associated with the cell or NULL if invalid date value */ protected function formatDateCellValue($nodeValue) { // Mitigate thrown Exception on invalid date-time format (http://php.net/manual/en/datetime.construct.php) try { return ($this->shouldFormatDates) ? $nodeValue : new \DateTime($nodeValue); } catch (\Exception $e) { return null; } } } Reader/XLSX/Helper/SharedStringsCaching/InMemoryStrategy.php 0000644 00000005036 15073227061 0020022 0 ustar 00 <?php namespace Box\Spout\Reader\XLSX\Helper\SharedStringsCaching; use Box\Spout\Reader\Exception\SharedStringNotFoundException; /** * Class InMemoryStrategy * * This class implements the in-memory caching strategy for shared strings. * This strategy is used when the number of unique strings is low, compared to the memory available. * * @package Box\Spout\Reader\XLSX\Helper\SharedStringsCaching */ class InMemoryStrategy implements CachingStrategyInterface { /** @var \SplFixedArray Array used to cache the shared strings */ protected $inMemoryCache; /** @var bool Whether the cache has been closed */ protected $isCacheClosed; /** * @param int $sharedStringsUniqueCount Number of unique shared strings */ public function __construct($sharedStringsUniqueCount) { $this->inMemoryCache = new \SplFixedArray($sharedStringsUniqueCount); $this->isCacheClosed = false; } /** * Adds the given string to the cache. * * @param string $sharedString The string to be added to the cache * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file * @return void */ public function addStringForIndex($sharedString, $sharedStringIndex) { if (!$this->isCacheClosed) { $this->inMemoryCache->offsetSet($sharedStringIndex, $sharedString); } } /** * Closes the cache after the last shared string was added. * This prevents any additional string from being added to the cache. * * @return void */ public function closeCache() { $this->isCacheClosed = true; } /** * Returns the string located at the given index from the cache. * * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file * @return string The shared string at the given index * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If no shared string found for the given index */ public function getStringAtIndex($sharedStringIndex) { try { return $this->inMemoryCache->offsetGet($sharedStringIndex); } catch (\RuntimeException $e) { throw new SharedStringNotFoundException("Shared string not found for index: $sharedStringIndex"); } } /** * Destroys the cache, freeing memory and removing any created artifacts * * @return void */ public function clearCache() { unset($this->inMemoryCache); $this->isCacheClosed = false; } } Reader/XLSX/Helper/SharedStringsCaching/CachingStrategyInterface.php 0000644 00000002472 15073227061 0021441 0 ustar 00 <?php namespace Box\Spout\Reader\XLSX\Helper\SharedStringsCaching; /** * Interface CachingStrategyInterface * * @package Box\Spout\Reader\XLSX\Helper\SharedStringsCaching */ interface CachingStrategyInterface { /** * Adds the given string to the cache. * * @param string $sharedString The string to be added to the cache * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file * @return void */ public function addStringForIndex($sharedString, $sharedStringIndex); /** * Closes the cache after the last shared string was added. * This prevents any additional string from being added to the cache. * * @return void */ public function closeCache(); /** * Returns the string located at the given index from the cache. * * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file * @return string The shared string at the given index * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If no shared string found for the given index */ public function getStringAtIndex($sharedStringIndex); /** * Destroys the cache, freeing memory and removing any created artifacts * * @return void */ public function clearCache(); } Reader/XLSX/Helper/SharedStringsCaching/FileBasedStrategy.php 0000644 00000016026 15073227061 0020102 0 ustar 00 <?php namespace Box\Spout\Reader\XLSX\Helper\SharedStringsCaching; use Box\Spout\Common\Helper\FileSystemHelper; use Box\Spout\Common\Helper\GlobalFunctionsHelper; use Box\Spout\Reader\Exception\SharedStringNotFoundException; /** * Class FileBasedStrategy * * This class implements the file-based caching strategy for shared strings. * Shared strings are stored in small files (with a max number of strings per file). * This strategy is slower than an in-memory strategy but is used to avoid out of memory crashes. * * @package Box\Spout\Reader\XLSX\Helper\SharedStringsCaching */ class FileBasedStrategy implements CachingStrategyInterface { /** Value to use to escape the line feed character ("\n") */ const ESCAPED_LINE_FEED_CHARACTER = '_x000A_'; /** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */ protected $globalFunctionsHelper; /** @var \Box\Spout\Common\Helper\FileSystemHelper Helper to perform file system operations */ protected $fileSystemHelper; /** @var string Temporary folder where the temporary files will be created */ protected $tempFolder; /** * @var int Maximum number of strings that can be stored in one temp file * @see CachingStrategyFactory::MAX_NUM_STRINGS_PER_TEMP_FILE */ protected $maxNumStringsPerTempFile; /** @var resource Pointer to the last temp file a shared string was written to */ protected $tempFilePointer; /** * @var string Path of the temporary file whose contents is currently stored in memory * @see CachingStrategyFactory::MAX_NUM_STRINGS_PER_TEMP_FILE */ protected $inMemoryTempFilePath; /** * @var array Contents of the temporary file that was last read * @see CachingStrategyFactory::MAX_NUM_STRINGS_PER_TEMP_FILE */ protected $inMemoryTempFileContents; /** * @param string|null $tempFolder Temporary folder where the temporary files to store shared strings will be stored * @param int $maxNumStringsPerTempFile Maximum number of strings that can be stored in one temp file */ public function __construct($tempFolder, $maxNumStringsPerTempFile) { $rootTempFolder = ($tempFolder) ?: sys_get_temp_dir(); $this->fileSystemHelper = new FileSystemHelper($rootTempFolder); $this->tempFolder = $this->fileSystemHelper->createFolder($rootTempFolder, uniqid('sharedstrings')); $this->maxNumStringsPerTempFile = $maxNumStringsPerTempFile; $this->globalFunctionsHelper = new GlobalFunctionsHelper(); $this->tempFilePointer = null; } /** * Adds the given string to the cache. * * @param string $sharedString The string to be added to the cache * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file * @return void */ public function addStringForIndex($sharedString, $sharedStringIndex) { $tempFilePath = $this->getSharedStringTempFilePath($sharedStringIndex); if (!$this->globalFunctionsHelper->file_exists($tempFilePath)) { if ($this->tempFilePointer) { $this->globalFunctionsHelper->fclose($this->tempFilePointer); } $this->tempFilePointer = $this->globalFunctionsHelper->fopen($tempFilePath, 'w'); } // The shared string retrieval logic expects each cell data to be on one line only // Encoding the line feed character allows to preserve this assumption $lineFeedEncodedSharedString = $this->escapeLineFeed($sharedString); $this->globalFunctionsHelper->fwrite($this->tempFilePointer, $lineFeedEncodedSharedString . PHP_EOL); } /** * Returns the path for the temp file that should contain the string for the given index * * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file * @return string The temp file path for the given index */ protected function getSharedStringTempFilePath($sharedStringIndex) { $numTempFile = intval($sharedStringIndex / $this->maxNumStringsPerTempFile); return $this->tempFolder . '/sharedstrings' . $numTempFile; } /** * Closes the cache after the last shared string was added. * This prevents any additional string from being added to the cache. * * @return void */ public function closeCache() { // close pointer to the last temp file that was written if ($this->tempFilePointer) { $this->globalFunctionsHelper->fclose($this->tempFilePointer); } } /** * Returns the string located at the given index from the cache. * * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file * @return string The shared string at the given index * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If no shared string found for the given index */ public function getStringAtIndex($sharedStringIndex) { $tempFilePath = $this->getSharedStringTempFilePath($sharedStringIndex); $indexInFile = $sharedStringIndex % $this->maxNumStringsPerTempFile; if (!$this->globalFunctionsHelper->file_exists($tempFilePath)) { throw new SharedStringNotFoundException("Shared string temp file not found: $tempFilePath ; for index: $sharedStringIndex"); } if ($this->inMemoryTempFilePath !== $tempFilePath) { // free memory unset($this->inMemoryTempFileContents); $this->inMemoryTempFileContents = explode(PHP_EOL, $this->globalFunctionsHelper->file_get_contents($tempFilePath)); $this->inMemoryTempFilePath = $tempFilePath; } $sharedString = null; // Using isset here because it is way faster than array_key_exists... if (isset($this->inMemoryTempFileContents[$indexInFile])) { $escapedSharedString = $this->inMemoryTempFileContents[$indexInFile]; $sharedString = $this->unescapeLineFeed($escapedSharedString); } if ($sharedString === null) { throw new SharedStringNotFoundException("Shared string not found for index: $sharedStringIndex"); } return rtrim($sharedString, PHP_EOL); } /** * Escapes the line feed characters (\n) * * @param string $unescapedString * @return string */ private function escapeLineFeed($unescapedString) { return str_replace("\n", self::ESCAPED_LINE_FEED_CHARACTER, $unescapedString); } /** * Unescapes the line feed characters (\n) * * @param string $escapedString * @return string */ private function unescapeLineFeed($escapedString) { return str_replace(self::ESCAPED_LINE_FEED_CHARACTER, "\n", $escapedString); } /** * Destroys the cache, freeing memory and removing any created artifacts * * @return void */ public function clearCache() { if ($this->tempFolder) { $this->fileSystemHelper->deleteFolderRecursively($this->tempFolder); } } } Reader/XLSX/Helper/SharedStringsCaching/CachingStrategyFactory.php 0000644 00000013321 15073227061 0021143 0 ustar 00 <?php namespace Box\Spout\Reader\XLSX\Helper\SharedStringsCaching; /** * Class CachingStrategyFactory * * @package Box\Spout\Reader\XLSX\Helper\SharedStringsCaching */ class CachingStrategyFactory { /** * The memory amount needed to store a string was obtained empirically from this data: * * ------------------------------------ * | Number of chars⁺ | Memory needed | * ------------------------------------ * | 3,000 | 1 MB | * | 15,000 | 2 MB | * | 30,000 | 5 MB | * | 75,000 | 11 MB | * | 150,000 | 21 MB | * | 300,000 | 43 MB | * | 750,000 | 105 MB | * | 1,500,000 | 210 MB | * | 2,250,000 | 315 MB | * | 3,000,000 | 420 MB | * | 4,500,000 | 630 MB | * ------------------------------------ * * ⁺ All characters were 1 byte long * * This gives a linear graph where each 1-byte character requires about 150 bytes to be stored. * Given that some characters can take up to 4 bytes, we need 600 bytes per character to be safe. * Also, there is on average about 20 characters per cell (this is entirely empirical data...). * * This means that in order to store one shared string in memory, the memory amount needed is: * => 20 * 600 ≈ 12KB */ const AMOUNT_MEMORY_NEEDED_PER_STRING_IN_KB = 12; /** * To avoid running out of memory when extracting a huge number of shared strings, they can be saved to temporary files * instead of in memory. Then, when accessing a string, the corresponding file contents will be loaded in memory * and the string will be quickly retrieved. * The performance bottleneck is not when creating these temporary files, but rather when loading their content. * Because the contents of the last loaded file stays in memory until another file needs to be loaded, it works * best when the indexes of the shared strings are sorted in the sheet data. * 10,000 was chosen because it creates small files that are fast to be loaded in memory. */ const MAX_NUM_STRINGS_PER_TEMP_FILE = 10000; /** @var CachingStrategyFactory|null Singleton instance */ protected static $instance = null; /** * Private constructor for singleton */ private function __construct() { } /** * Returns the singleton instance of the factory * * @return CachingStrategyFactory */ public static function getInstance() { if (self::$instance === null) { self::$instance = new CachingStrategyFactory(); } return self::$instance; } /** * Returns the best caching strategy, given the number of unique shared strings * and the amount of memory available. * * @param int|null $sharedStringsUniqueCount Number of unique shared strings (NULL if unknown) * @param string|void $tempFolder Temporary folder where the temporary files to store shared strings will be stored * @return CachingStrategyInterface The best caching strategy */ public function getBestCachingStrategy($sharedStringsUniqueCount, $tempFolder = null) { if ($this->isInMemoryStrategyUsageSafe($sharedStringsUniqueCount)) { return new InMemoryStrategy($sharedStringsUniqueCount); } else { return new FileBasedStrategy($tempFolder, self::MAX_NUM_STRINGS_PER_TEMP_FILE); } } /** * Returns whether it is safe to use in-memory caching, given the number of unique shared strings * and the amount of memory available. * * @param int|null $sharedStringsUniqueCount Number of unique shared strings (NULL if unknown) * @return bool */ protected function isInMemoryStrategyUsageSafe($sharedStringsUniqueCount) { // if the number of shared strings in unknown, do not use "in memory" strategy if ($sharedStringsUniqueCount === null) { return false; } $memoryAvailable = $this->getMemoryLimitInKB(); if ($memoryAvailable === -1) { // if cannot get memory limit or if memory limit set as unlimited, don't trust and play safe return ($sharedStringsUniqueCount < self::MAX_NUM_STRINGS_PER_TEMP_FILE); } else { $memoryNeeded = $sharedStringsUniqueCount * self::AMOUNT_MEMORY_NEEDED_PER_STRING_IN_KB; return ($memoryAvailable > $memoryNeeded); } } /** * Returns the PHP "memory_limit" in Kilobytes * * @return float */ protected function getMemoryLimitInKB() { $memoryLimitFormatted = $this->getMemoryLimitFromIni(); $memoryLimitFormatted = strtolower(trim($memoryLimitFormatted)); // No memory limit if ($memoryLimitFormatted === '-1') { return -1; } if (preg_match('/(\d+)([bkmgt])b?/', $memoryLimitFormatted, $matches)) { $amount = intval($matches[1]); $unit = $matches[2]; switch ($unit) { case 'b': return ($amount / 1024); case 'k': return $amount; case 'm': return ($amount * 1024); case 'g': return ($amount * 1024 * 1024); case 't': return ($amount * 1024 * 1024 * 1024); } } return -1; } /** * Returns the formatted "memory_limit" value * * @return string */ protected function getMemoryLimitFromIni() { return ini_get('memory_limit'); } } Reader/XLSX/Helper/SheetHelper.php 0000644 00000015013 15073227061 0012707 0 ustar 00 <?php namespace Box\Spout\Reader\XLSX\Helper; use Box\Spout\Reader\Wrapper\XMLReader; use Box\Spout\Reader\XLSX\Sheet; /** * Class SheetHelper * This class provides helper functions related to XLSX sheets * * @package Box\Spout\Reader\XLSX\Helper */ class SheetHelper { /** Paths of XML files relative to the XLSX file root */ const WORKBOOK_XML_RELS_FILE_PATH = 'xl/_rels/workbook.xml.rels'; const WORKBOOK_XML_FILE_PATH = 'xl/workbook.xml'; /** Definition of XML node names used to parse data */ const XML_NODE_WORKBOOK_VIEW = 'workbookView'; const XML_NODE_SHEET = 'sheet'; const XML_NODE_SHEETS = 'sheets'; const XML_NODE_RELATIONSHIP = 'Relationship'; /** Definition of XML attributes used to parse data */ const XML_ATTRIBUTE_ACTIVE_TAB = 'activeTab'; const XML_ATTRIBUTE_R_ID = 'r:id'; const XML_ATTRIBUTE_NAME = 'name'; const XML_ATTRIBUTE_ID = 'Id'; const XML_ATTRIBUTE_TARGET = 'Target'; /** @var string Path of the XLSX file being read */ protected $filePath; /** @var \Box\Spout\Reader\XLSX\ReaderOptions Reader's current options */ protected $options; /** @var \Box\Spout\Reader\XLSX\Helper\SharedStringsHelper Helper to work with shared strings */ protected $sharedStringsHelper; /** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */ protected $globalFunctionsHelper; /** * @param string $filePath Path of the XLSX file being read * @param \Box\Spout\Reader\XLSX\ReaderOptions $options Reader's current options * @param \Box\Spout\Reader\XLSX\Helper\SharedStringsHelper Helper to work with shared strings * @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper */ public function __construct($filePath, $options, $sharedStringsHelper, $globalFunctionsHelper) { $this->filePath = $filePath; $this->options = $options; $this->sharedStringsHelper = $sharedStringsHelper; $this->globalFunctionsHelper = $globalFunctionsHelper; } /** * Returns the sheets metadata of the file located at the previously given file path. * The paths to the sheets' data are read from the [Content_Types].xml file. * * @return Sheet[] Sheets within the XLSX file */ public function getSheets() { $sheets = []; $sheetIndex = 0; $activeSheetIndex = 0; // By default, the first sheet is active $xmlReader = new XMLReader(); if ($xmlReader->openFileInZip($this->filePath, self::WORKBOOK_XML_FILE_PATH)) { while ($xmlReader->read()) { if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_WORKBOOK_VIEW)) { // The "workbookView" node is located before "sheet" nodes, ensuring that // the active sheet is known before parsing sheets data. $activeSheetIndex = (int) $xmlReader->getAttribute(self::XML_ATTRIBUTE_ACTIVE_TAB); } else if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_SHEET)) { $isSheetActive = ($sheetIndex === $activeSheetIndex); $sheets[] = $this->getSheetFromSheetXMLNode($xmlReader, $sheetIndex, $isSheetActive); $sheetIndex++; } else if ($xmlReader->isPositionedOnEndingNode(self::XML_NODE_SHEETS)) { // stop reading once all sheets have been read break; } } $xmlReader->close(); } return $sheets; } /** * Returns an instance of a sheet, given the XML node describing the sheet - from "workbook.xml". * We can find the XML file path describing the sheet inside "workbook.xml.res", by mapping with the sheet ID * ("r:id" in "workbook.xml", "Id" in "workbook.xml.res"). * * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReaderOnSheetNode XML Reader instance, pointing on the node describing the sheet, as defined in "workbook.xml" * @param int $sheetIndexZeroBased Index of the sheet, based on order of appearance in the workbook (zero-based) * @param bool $isSheetActive Whether this sheet was defined as active * @return \Box\Spout\Reader\XLSX\Sheet Sheet instance */ protected function getSheetFromSheetXMLNode($xmlReaderOnSheetNode, $sheetIndexZeroBased, $isSheetActive) { $sheetId = $xmlReaderOnSheetNode->getAttribute(self::XML_ATTRIBUTE_R_ID); $escapedSheetName = $xmlReaderOnSheetNode->getAttribute(self::XML_ATTRIBUTE_NAME); /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ $escaper = \Box\Spout\Common\Escaper\XLSX::getInstance(); $sheetName = $escaper->unescape($escapedSheetName); $sheetDataXMLFilePath = $this->getSheetDataXMLFilePathForSheetId($sheetId); return new Sheet( $this->filePath, $sheetDataXMLFilePath, $sheetIndexZeroBased, $sheetName, $isSheetActive, $this->options, $this->sharedStringsHelper ); } /** * @param string $sheetId The sheet ID, as defined in "workbook.xml" * @return string The XML file path describing the sheet inside "workbook.xml.res", for the given sheet ID */ protected function getSheetDataXMLFilePathForSheetId($sheetId) { $sheetDataXMLFilePath = ''; // find the file path of the sheet, by looking at the "workbook.xml.res" file $xmlReader = new XMLReader(); if ($xmlReader->openFileInZip($this->filePath, self::WORKBOOK_XML_RELS_FILE_PATH)) { while ($xmlReader->read()) { if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_RELATIONSHIP)) { $relationshipSheetId = $xmlReader->getAttribute(self::XML_ATTRIBUTE_ID); if ($relationshipSheetId === $sheetId) { // In workbook.xml.rels, it is only "worksheets/sheet1.xml" // In [Content_Types].xml, the path is "/xl/worksheets/sheet1.xml" $sheetDataXMLFilePath = $xmlReader->getAttribute(self::XML_ATTRIBUTE_TARGET); // sometimes, the sheet data file path already contains "/xl/"... if (strpos($sheetDataXMLFilePath, '/xl/') !== 0) { $sheetDataXMLFilePath = '/xl/' . $sheetDataXMLFilePath; break; } } } } $xmlReader->close(); } return $sheetDataXMLFilePath; } } Reader/XLSX/Helper/CellHelper.php 0000644 00000010174 15073227061 0012521 0 ustar 00 <?php namespace Box\Spout\Reader\XLSX\Helper; use Box\Spout\Common\Exception\InvalidArgumentException; /** * Class CellHelper * This class provides helper functions when working with cells * * @package Box\Spout\Reader\XLSX\Helper */ class CellHelper { // Using ord() is super slow... Using a pre-computed hash table instead. private static $columnLetterToIndexMapping = [ 'A' => 0, 'B' => 1, 'C' => 2, 'D' => 3, 'E' => 4, 'F' => 5, 'G' => 6, 'H' => 7, 'I' => 8, 'J' => 9, 'K' => 10, 'L' => 11, 'M' => 12, 'N' => 13, 'O' => 14, 'P' => 15, 'Q' => 16, 'R' => 17, 'S' => 18, 'T' => 19, 'U' => 20, 'V' => 21, 'W' => 22, 'X' => 23, 'Y' => 24, 'Z' => 25, ]; /** * Fills the missing indexes of an array with a given value. * For instance, $dataArray = []; $a[1] = 1; $a[3] = 3; * Calling fillMissingArrayIndexes($dataArray, 'FILL') will return this array: ['FILL', 1, 'FILL', 3] * * @param array $dataArray The array to fill * @param string|void $fillValue optional * @return array */ public static function fillMissingArrayIndexes($dataArray, $fillValue = '') { if (empty($dataArray)) { return []; } $existingIndexes = array_keys($dataArray); $newIndexes = array_fill_keys(range(0, max($existingIndexes)), $fillValue); $dataArray += $newIndexes; ksort($dataArray); return $dataArray; } /** * Returns the base 10 column index associated to the cell index (base 26). * Excel uses A to Z letters for column indexing, where A is the 1st column, * Z is the 26th and AA is the 27th. * The mapping is zero based, so that A1 maps to 0, B2 maps to 1, Z13 to 25 and AA4 to 26. * * @param string $cellIndex The Excel cell index ('A1', 'BC13', ...) * @return int * @throws \Box\Spout\Common\Exception\InvalidArgumentException When the given cell index is invalid */ public static function getColumnIndexFromCellIndex($cellIndex) { if (!self::isValidCellIndex($cellIndex)) { throw new InvalidArgumentException('Cannot get column index from an invalid cell index.'); } $columnIndex = 0; // Remove row information $columnLetters = preg_replace('/\d/', '', $cellIndex); // strlen() is super slow too... Using isset() is way faster and not too unreadable, // since we checked before that there are between 1 and 3 letters. $columnLength = isset($columnLetters[1]) ? (isset($columnLetters[2]) ? 3 : 2) : 1; // Looping over the different letters of the column is slower than this method. // Also, not using the pow() function because it's slooooow... switch ($columnLength) { case 1: $columnIndex = (self::$columnLetterToIndexMapping[$columnLetters]); break; case 2: $firstLetterIndex = (self::$columnLetterToIndexMapping[$columnLetters[0]] + 1) * 26; $secondLetterIndex = self::$columnLetterToIndexMapping[$columnLetters[1]]; $columnIndex = $firstLetterIndex + $secondLetterIndex; break; case 3: $firstLetterIndex = (self::$columnLetterToIndexMapping[$columnLetters[0]] + 1) * 676; $secondLetterIndex = (self::$columnLetterToIndexMapping[$columnLetters[1]] + 1) * 26; $thirdLetterIndex = self::$columnLetterToIndexMapping[$columnLetters[2]]; $columnIndex = $firstLetterIndex + $secondLetterIndex + $thirdLetterIndex; break; } return $columnIndex; } /** * Returns whether a cell index is valid, in an Excel world. * To be valid, the cell index should start with capital letters and be followed by numbers. * There can only be 3 letters, as there can only be 16,384 rows, which is equivalent to 'XFE'. * * @param string $cellIndex The Excel cell index ('A1', 'BC13', ...) * @return bool */ protected static function isValidCellIndex($cellIndex) { return (preg_match('/^[A-Z]{1,3}\d+$/', $cellIndex) === 1); } } Reader/XLSX/Helper/StyleHelper.php 0000644 00000030037 15073227061 0012742 0 ustar 00 <?php namespace Box\Spout\Reader\XLSX\Helper; use Box\Spout\Reader\Wrapper\XMLReader; /** * Class StyleHelper * This class provides helper functions related to XLSX styles * * @package Box\Spout\Reader\XLSX\Helper */ class StyleHelper { /** Paths of XML files relative to the XLSX file root */ const STYLES_XML_FILE_PATH = 'xl/styles.xml'; /** Nodes used to find relevant information in the styles XML file */ const XML_NODE_NUM_FMTS = 'numFmts'; const XML_NODE_NUM_FMT = 'numFmt'; const XML_NODE_CELL_XFS = 'cellXfs'; const XML_NODE_XF = 'xf'; /** Attributes used to find relevant information in the styles XML file */ const XML_ATTRIBUTE_NUM_FMT_ID = 'numFmtId'; const XML_ATTRIBUTE_FORMAT_CODE = 'formatCode'; const XML_ATTRIBUTE_APPLY_NUMBER_FORMAT = 'applyNumberFormat'; /** By convention, default style ID is 0 */ const DEFAULT_STYLE_ID = 0; const NUMBER_FORMAT_GENERAL = 'General'; /** * @see https://msdn.microsoft.com/en-us/library/ff529597(v=office.12).aspx * @var array Mapping between built-in numFmtId and the associated format - for dates only */ protected static $builtinNumFmtIdToNumFormatMapping = [ 14 => 'm/d/yyyy', // @NOTE: ECMA spec is 'mm-dd-yy' 15 => 'd-mmm-yy', 16 => 'd-mmm', 17 => 'mmm-yy', 18 => 'h:mm AM/PM', 19 => 'h:mm:ss AM/PM', 20 => 'h:mm', 21 => 'h:mm:ss', 22 => 'm/d/yyyy h:mm', // @NOTE: ECMA spec is 'm/d/yy h:mm', 45 => 'mm:ss', 46 => '[h]:mm:ss', 47 => 'mm:ss.0', // @NOTE: ECMA spec is 'mmss.0', ]; /** @var string Path of the XLSX file being read */ protected $filePath; /** @var array Array containing the IDs of built-in number formats indicating a date */ protected $builtinNumFmtIdIndicatingDates; /** @var array Array containing a mapping NUM_FMT_ID => FORMAT_CODE */ protected $customNumberFormats; /** @var array Array containing a mapping STYLE_ID => [STYLE_ATTRIBUTES] */ protected $stylesAttributes; /** @var array Cache containing a mapping NUM_FMT_ID => IS_DATE_FORMAT. Used to avoid lots of recalculations */ protected $numFmtIdToIsDateFormatCache = []; /** * @param string $filePath Path of the XLSX file being read */ public function __construct($filePath) { $this->filePath = $filePath; $this->builtinNumFmtIdIndicatingDates = array_keys(self::$builtinNumFmtIdToNumFormatMapping); } /** * Returns whether the style with the given ID should consider * numeric values as timestamps and format the cell as a date. * * @param int $styleId Zero-based style ID * @return bool Whether the cell with the given cell should display a date instead of a numeric value */ public function shouldFormatNumericValueAsDate($styleId) { $stylesAttributes = $this->getStylesAttributes(); // Default style (0) does not format numeric values as timestamps. Only custom styles do. // Also if the style ID does not exist in the styles.xml file, format as numeric value. // Using isset here because it is way faster than array_key_exists... if ($styleId === self::DEFAULT_STYLE_ID || !isset($stylesAttributes[$styleId])) { return false; } $styleAttributes = $stylesAttributes[$styleId]; return $this->doesStyleIndicateDate($styleAttributes); } /** * Reads the styles.xml file and extract the relevant information from the file. * * @return void */ protected function extractRelevantInfo() { $this->customNumberFormats = []; $this->stylesAttributes = []; $xmlReader = new XMLReader(); if ($xmlReader->openFileInZip($this->filePath, self::STYLES_XML_FILE_PATH)) { while ($xmlReader->read()) { if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_NUM_FMTS)) { $this->extractNumberFormats($xmlReader); } else if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_CELL_XFS)) { $this->extractStyleAttributes($xmlReader); } } $xmlReader->close(); } } /** * Extracts number formats from the "numFmt" nodes. * For simplicity, the styles attributes are kept in memory. This is possible thanks * to the reuse of formats. So 1 million cells should not use 1 million formats. * * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XML Reader positioned on the "numFmts" node * @return void */ protected function extractNumberFormats($xmlReader) { while ($xmlReader->read()) { if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_NUM_FMT)) { $numFmtId = intval($xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_FMT_ID)); $formatCode = $xmlReader->getAttribute(self::XML_ATTRIBUTE_FORMAT_CODE); $this->customNumberFormats[$numFmtId] = $formatCode; } else if ($xmlReader->isPositionedOnEndingNode(self::XML_NODE_NUM_FMTS)) { // Once done reading "numFmts" node's children break; } } } /** * Extracts style attributes from the "xf" nodes, inside the "cellXfs" section. * For simplicity, the styles attributes are kept in memory. This is possible thanks * to the reuse of styles. So 1 million cells should not use 1 million styles. * * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XML Reader positioned on the "cellXfs" node * @return void */ protected function extractStyleAttributes($xmlReader) { while ($xmlReader->read()) { if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_XF)) { $numFmtId = $xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_FMT_ID); $normalizedNumFmtId = ($numFmtId !== null) ? intval($numFmtId) : null; $applyNumberFormat = $xmlReader->getAttribute(self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT); $normalizedApplyNumberFormat = ($applyNumberFormat !== null) ? !!$applyNumberFormat : null; $this->stylesAttributes[] = [ self::XML_ATTRIBUTE_NUM_FMT_ID => $normalizedNumFmtId, self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT => $normalizedApplyNumberFormat, ]; } else if ($xmlReader->isPositionedOnEndingNode(self::XML_NODE_CELL_XFS)) { // Once done reading "cellXfs" node's children break; } } } /** * @return array The custom number formats */ protected function getCustomNumberFormats() { if (!isset($this->customNumberFormats)) { $this->extractRelevantInfo(); } return $this->customNumberFormats; } /** * @return array The styles attributes */ protected function getStylesAttributes() { if (!isset($this->stylesAttributes)) { $this->extractRelevantInfo(); } return $this->stylesAttributes; } /** * @param array $styleAttributes Array containing the style attributes (2 keys: "applyNumberFormat" and "numFmtId") * @return bool Whether the style with the given attributes indicates that the number is a date */ protected function doesStyleIndicateDate($styleAttributes) { $applyNumberFormat = $styleAttributes[self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT]; $numFmtId = $styleAttributes[self::XML_ATTRIBUTE_NUM_FMT_ID]; // A style may apply a date format if it has: // - "applyNumberFormat" attribute not set to "false" // - "numFmtId" attribute set // This is a preliminary check, as having "numFmtId" set just means the style should apply a specific number format, // but this is not necessarily a date. if ($applyNumberFormat === false || $numFmtId === null) { return false; } return $this->doesNumFmtIdIndicateDate($numFmtId); } /** * Returns whether the number format ID indicates that the number is a date. * The result is cached to avoid recomputing the same thing over and over, as * "numFmtId" attributes can be shared between multiple styles. * * @param int $numFmtId * @return bool Whether the number format ID indicates that the number is a date */ protected function doesNumFmtIdIndicateDate($numFmtId) { if (!isset($this->numFmtIdToIsDateFormatCache[$numFmtId])) { $formatCode = $this->getFormatCodeForNumFmtId($numFmtId); $this->numFmtIdToIsDateFormatCache[$numFmtId] = ( $this->isNumFmtIdBuiltInDateFormat($numFmtId) || $this->isFormatCodeCustomDateFormat($formatCode) ); } return $this->numFmtIdToIsDateFormatCache[$numFmtId]; } /** * @param int $numFmtId * @return string|null The custom number format or NULL if none defined for the given numFmtId */ protected function getFormatCodeForNumFmtId($numFmtId) { $customNumberFormats = $this->getCustomNumberFormats(); // Using isset here because it is way faster than array_key_exists... return (isset($customNumberFormats[$numFmtId])) ? $customNumberFormats[$numFmtId] : null; } /** * @param int $numFmtId * @return bool Whether the number format ID indicates that the number is a date */ protected function isNumFmtIdBuiltInDateFormat($numFmtId) { return in_array($numFmtId, $this->builtinNumFmtIdIndicatingDates); } /** * @param string|null $formatCode * @return bool Whether the given format code indicates that the number is a date */ protected function isFormatCodeCustomDateFormat($formatCode) { // if no associated format code or if using the default "General" format if ($formatCode === null || strcasecmp($formatCode, self::NUMBER_FORMAT_GENERAL) === 0) { return false; } return $this->isFormatCodeMatchingDateFormatPattern($formatCode); } /** * @param string $formatCode * @return bool Whether the given format code matches a date format pattern */ protected function isFormatCodeMatchingDateFormatPattern($formatCode) { // Remove extra formatting (what's between [ ], the brackets should not be preceded by a "\") $pattern = '((?<!\\\)\[.+?(?<!\\\)\])'; $formatCode = preg_replace($pattern, '', $formatCode); // custom date formats contain specific characters to represent the date: // e - yy - m - d - h - s // and all of their variants (yyyy - mm - dd...) $dateFormatCharacters = ['e', 'yy', 'm', 'd', 'h', 's']; $hasFoundDateFormatCharacter = false; foreach ($dateFormatCharacters as $dateFormatCharacter) { // character not preceded by "\" (case insensitive) $pattern = '/(?<!\\\)' . $dateFormatCharacter . '/i'; if (preg_match($pattern, $formatCode)) { $hasFoundDateFormatCharacter = true; break; } } return $hasFoundDateFormatCharacter; } /** * Returns the format as defined in "styles.xml" of the given style. * NOTE: It is assumed that the style DOES have a number format associated to it. * * @param int $styleId Zero-based style ID * @return string The number format code associated with the given style */ public function getNumberFormatCode($styleId) { $stylesAttributes = $this->getStylesAttributes(); $styleAttributes = $stylesAttributes[$styleId]; $numFmtId = $styleAttributes[self::XML_ATTRIBUTE_NUM_FMT_ID]; if ($this->isNumFmtIdBuiltInDateFormat($numFmtId)) { $numberFormatCode = self::$builtinNumFmtIdToNumFormatMapping[$numFmtId]; } else { $customNumberFormats = $this->getCustomNumberFormats(); $numberFormatCode = $customNumberFormats[$numFmtId]; } return $numberFormatCode; } } Reader/XLSX/Helper/SharedStringsHelper.php 0000644 00000021356 15073227061 0014426 0 ustar 00 <?php namespace Box\Spout\Reader\XLSX\Helper; use Box\Spout\Common\Exception\IOException; use Box\Spout\Reader\Exception\XMLProcessingException; use Box\Spout\Reader\Wrapper\XMLReader; use Box\Spout\Reader\XLSX\Helper\SharedStringsCaching\CachingStrategyFactory; use Box\Spout\Reader\XLSX\Helper\SharedStringsCaching\CachingStrategyInterface; /** * Class SharedStringsHelper * This class provides helper functions for reading sharedStrings XML file * * @package Box\Spout\Reader\XLSX\Helper */ class SharedStringsHelper { /** Path of sharedStrings XML file inside the XLSX file */ const SHARED_STRINGS_XML_FILE_PATH = 'xl/sharedStrings.xml'; /** Main namespace for the sharedStrings.xml file */ const MAIN_NAMESPACE_FOR_SHARED_STRINGS_XML = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'; /** Definition of XML nodes names used to parse data */ const XML_NODE_SST = 'sst'; const XML_NODE_SI = 'si'; const XML_NODE_R = 'r'; const XML_NODE_T = 't'; /** Definition of XML attributes used to parse data */ const XML_ATTRIBUTE_COUNT = 'count'; const XML_ATTRIBUTE_UNIQUE_COUNT = 'uniqueCount'; const XML_ATTRIBUTE_XML_SPACE = 'xml:space'; const XML_ATTRIBUTE_VALUE_PRESERVE = 'preserve'; /** @var string Path of the XLSX file being read */ protected $filePath; /** @var string Temporary folder where the temporary files to store shared strings will be stored */ protected $tempFolder; /** @var CachingStrategyInterface The best caching strategy for storing shared strings */ protected $cachingStrategy; /** * @param string $filePath Path of the XLSX file being read * @param string|null|void $tempFolder Temporary folder where the temporary files to store shared strings will be stored */ public function __construct($filePath, $tempFolder = null) { $this->filePath = $filePath; $this->tempFolder = $tempFolder; } /** * Returns whether the XLSX file contains a shared strings XML file * * @return bool */ public function hasSharedStrings() { $hasSharedStrings = false; $zip = new \ZipArchive(); if ($zip->open($this->filePath) === true) { $hasSharedStrings = ($zip->locateName(self::SHARED_STRINGS_XML_FILE_PATH) !== false); $zip->close(); } return $hasSharedStrings; } /** * Builds an in-memory array containing all the shared strings of the sheet. * All the strings are stored in a XML file, located at 'xl/sharedStrings.xml'. * It is then accessed by the sheet data, via the string index in the built table. * * More documentation available here: http://msdn.microsoft.com/en-us/library/office/gg278314.aspx * * The XML file can be really big with sheets containing a lot of data. That is why * we need to use a XML reader that provides streaming like the XMLReader library. * * @return void * @throws \Box\Spout\Common\Exception\IOException If sharedStrings.xml can't be read */ public function extractSharedStrings() { $xmlReader = new XMLReader(); $sharedStringIndex = 0; if ($xmlReader->openFileInZip($this->filePath, self::SHARED_STRINGS_XML_FILE_PATH) === false) { throw new IOException('Could not open "' . self::SHARED_STRINGS_XML_FILE_PATH . '".'); } try { $sharedStringsUniqueCount = $this->getSharedStringsUniqueCount($xmlReader); $this->cachingStrategy = $this->getBestSharedStringsCachingStrategy($sharedStringsUniqueCount); $xmlReader->readUntilNodeFound(self::XML_NODE_SI); while ($xmlReader->getCurrentNodeName() === self::XML_NODE_SI) { $this->processSharedStringsItem($xmlReader, $sharedStringIndex); $sharedStringIndex++; // jump to the next '<si>' tag $xmlReader->next(self::XML_NODE_SI); } $this->cachingStrategy->closeCache(); } catch (XMLProcessingException $exception) { throw new IOException("The sharedStrings.xml file is invalid and cannot be read. [{$exception->getMessage()}]"); } $xmlReader->close(); } /** * Returns the shared strings unique count, as specified in <sst> tag. * * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader instance * @return int|null Number of unique shared strings in the sharedStrings.xml file * @throws \Box\Spout\Common\Exception\IOException If sharedStrings.xml is invalid and can't be read */ protected function getSharedStringsUniqueCount($xmlReader) { $xmlReader->next(self::XML_NODE_SST); // Iterate over the "sst" elements to get the actual "sst ELEMENT" (skips any DOCTYPE) while ($xmlReader->getCurrentNodeName() === self::XML_NODE_SST && $xmlReader->nodeType !== XMLReader::ELEMENT) { $xmlReader->read(); } $uniqueCount = $xmlReader->getAttribute(self::XML_ATTRIBUTE_UNIQUE_COUNT); // some software do not add the "uniqueCount" attribute but only use the "count" one // @see https://github.com/box/spout/issues/254 if ($uniqueCount === null) { $uniqueCount = $xmlReader->getAttribute(self::XML_ATTRIBUTE_COUNT); } return ($uniqueCount !== null) ? intval($uniqueCount) : null; } /** * Returns the best shared strings caching strategy. * * @param int|null $sharedStringsUniqueCount Number of unique shared strings (NULL if unknown) * @return CachingStrategyInterface */ protected function getBestSharedStringsCachingStrategy($sharedStringsUniqueCount) { return CachingStrategyFactory::getInstance() ->getBestCachingStrategy($sharedStringsUniqueCount, $this->tempFolder); } /** * Processes the shared strings item XML node which the given XML reader is positioned on. * * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XML Reader positioned on a "<si>" node * @param int $sharedStringIndex Index of the processed shared strings item * @return void */ protected function processSharedStringsItem($xmlReader, $sharedStringIndex) { $sharedStringValue = ''; // NOTE: expand() will automatically decode all XML entities of the child nodes $siNode = $xmlReader->expand(); $textNodes = $siNode->getElementsByTagName(self::XML_NODE_T); foreach ($textNodes as $textNode) { if ($this->shouldExtractTextNodeValue($textNode)) { $textNodeValue = $textNode->nodeValue; $shouldPreserveWhitespace = $this->shouldPreserveWhitespace($textNode); $sharedStringValue .= ($shouldPreserveWhitespace) ? $textNodeValue : trim($textNodeValue); } } $this->cachingStrategy->addStringForIndex($sharedStringValue, $sharedStringIndex); } /** * Not all text nodes' values must be extracted. * Some text nodes are part of a node describing the pronunciation for instance. * We'll only consider the nodes whose parents are "<si>" or "<r>". * * @param \DOMElement $textNode Text node to check * @return bool Whether the given text node's value must be extracted */ protected function shouldExtractTextNodeValue($textNode) { $parentTagName = $textNode->parentNode->localName; return ($parentTagName === self::XML_NODE_SI || $parentTagName === self::XML_NODE_R); } /** * If the text node has the attribute 'xml:space="preserve"', then preserve whitespace. * * @param \DOMElement $textNode The text node element (<t>) whose whitespace may be preserved * @return bool Whether whitespace should be preserved */ protected function shouldPreserveWhitespace($textNode) { $spaceValue = $textNode->getAttribute(self::XML_ATTRIBUTE_XML_SPACE); return ($spaceValue === self::XML_ATTRIBUTE_VALUE_PRESERVE); } /** * Returns the shared string at the given index, using the previously chosen caching strategy. * * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file * @return string The shared string at the given index * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If no shared string found for the given index */ public function getStringAtIndex($sharedStringIndex) { return $this->cachingStrategy->getStringAtIndex($sharedStringIndex); } /** * Destroys the cache, freeing memory and removing any created artifacts * * @return void */ public function cleanup() { if ($this->cachingStrategy) { $this->cachingStrategy->clearCache(); } } } Reader/XLSX/Reader.php 0000644 00000006163 15073227061 0010470 0 ustar 00 <?php namespace Box\Spout\Reader\XLSX; use Box\Spout\Common\Exception\IOException; use Box\Spout\Reader\AbstractReader; use Box\Spout\Reader\XLSX\Helper\SharedStringsHelper; /** * Class Reader * This class provides support to read data from a XLSX file * * @package Box\Spout\Reader\XLSX */ class Reader extends AbstractReader { /** @var \ZipArchive */ protected $zip; /** @var \Box\Spout\Reader\XLSX\Helper\SharedStringsHelper Helper to work with shared strings */ protected $sharedStringsHelper; /** @var SheetIterator To iterator over the XLSX sheets */ protected $sheetIterator; /** * Returns the reader's current options * * @return ReaderOptions */ protected function getOptions() { if (!isset($this->options)) { $this->options = new ReaderOptions(); } return $this->options; } /** * @param string $tempFolder Temporary folder where the temporary files will be created * @return Reader */ public function setTempFolder($tempFolder) { $this->getOptions()->setTempFolder($tempFolder); return $this; } /** * Returns whether stream wrappers are supported * * @return bool */ protected function doesSupportStreamWrapper() { return false; } /** * Opens the file at the given file path to make it ready to be read. * It also parses the sharedStrings.xml file to get all the shared strings available in memory * and fetches all the available sheets. * * @param string $filePath Path of the file to be read * @return void * @throws \Box\Spout\Common\Exception\IOException If the file at the given path or its content cannot be read * @throws \Box\Spout\Reader\Exception\NoSheetsFoundException If there are no sheets in the file */ protected function openReader($filePath) { $this->zip = new \ZipArchive(); if ($this->zip->open($filePath) === true) { $this->sharedStringsHelper = new SharedStringsHelper($filePath, $this->getOptions()->getTempFolder()); if ($this->sharedStringsHelper->hasSharedStrings()) { // Extracts all the strings from the sheets for easy access in the future $this->sharedStringsHelper->extractSharedStrings(); } $this->sheetIterator = new SheetIterator($filePath, $this->getOptions(), $this->sharedStringsHelper, $this->globalFunctionsHelper); } else { throw new IOException("Could not open $filePath for reading."); } } /** * Returns an iterator to iterate over sheets. * * @return SheetIterator To iterate over sheets */ protected function getConcreteSheetIterator() { return $this->sheetIterator; } /** * Closes the reader. To be used after reading the file. * * @return void */ protected function closeReader() { if ($this->zip) { $this->zip->close(); } if ($this->sharedStringsHelper) { $this->sharedStringsHelper->cleanup(); } } } Reader/XLSX/Sheet.php 0000644 00000004172 15073227061 0010334 0 ustar 00 <?php namespace Box\Spout\Reader\XLSX; use Box\Spout\Reader\SheetInterface; /** * Class Sheet * Represents a sheet within a XLSX file * * @package Box\Spout\Reader\XLSX */ class Sheet implements SheetInterface { /** @var \Box\Spout\Reader\XLSX\RowIterator To iterate over sheet's rows */ protected $rowIterator; /** @var int Index of the sheet, based on order in the workbook (zero-based) */ protected $index; /** @var string Name of the sheet */ protected $name; /** @var bool Whether the sheet was the active one */ protected $isActive; /** * @param string $filePath Path of the XLSX file being read * @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml * @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based) * @param string $sheetName Name of the sheet * @param bool $isSheetActive Whether the sheet was defined as active * @param \Box\Spout\Reader\XLSX\ReaderOptions $options Reader's current options * @param Helper\SharedStringsHelper Helper to work with shared strings */ public function __construct($filePath, $sheetDataXMLFilePath, $sheetIndex, $sheetName, $isSheetActive, $options, $sharedStringsHelper) { $this->rowIterator = new RowIterator($filePath, $sheetDataXMLFilePath, $options, $sharedStringsHelper); $this->index = $sheetIndex; $this->name = $sheetName; $this->isActive = $isSheetActive; } /** * @api * @return \Box\Spout\Reader\XLSX\RowIterator */ public function getRowIterator() { return $this->rowIterator; } /** * @api * @return int Index of the sheet, based on order in the workbook (zero-based) */ public function getIndex() { return $this->index; } /** * @api * @return string Name of the sheet */ public function getName() { return $this->name; } /** * @api * @return bool Whether the sheet was defined as active */ public function isActive() { return $this->isActive; } } Reader/XLSX/ReaderOptions.php 0000644 00000001450 15073227061 0012036 0 ustar 00 <?php namespace Box\Spout\Reader\XLSX; /** * Class ReaderOptions * This class is used to customize the reader's behavior * * @package Box\Spout\Reader\XLSX */ class ReaderOptions extends \Box\Spout\Reader\Common\ReaderOptions { /** @var string|null Temporary folder where the temporary files will be created */ protected $tempFolder = null; /** * @return string|null Temporary folder where the temporary files will be created */ public function getTempFolder() { return $this->tempFolder; } /** * @param string|null $tempFolder Temporary folder where the temporary files will be created * @return ReaderOptions */ public function setTempFolder($tempFolder) { $this->tempFolder = $tempFolder; return $this; } } Reader/XLSX/SheetIterator.php 0000644 00000006212 15073227061 0012043 0 ustar 00 <?php namespace Box\Spout\Reader\XLSX; use Box\Spout\Reader\IteratorInterface; use Box\Spout\Reader\XLSX\Helper\SheetHelper; use Box\Spout\Reader\Exception\NoSheetsFoundException; /** * Class SheetIterator * Iterate over XLSX sheet. * * @package Box\Spout\Reader\XLSX */ class SheetIterator implements IteratorInterface { /** @var \Box\Spout\Reader\XLSX\Sheet[] The list of sheet present in the file */ protected $sheets; /** @var int The index of the sheet being read (zero-based) */ protected $currentSheetIndex; /** * @param string $filePath Path of the file to be read * @param \Box\Spout\Reader\XLSX\ReaderOptions $options Reader's current options * @param \Box\Spout\Reader\XLSX\Helper\SharedStringsHelper $sharedStringsHelper * @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper * @throws \Box\Spout\Reader\Exception\NoSheetsFoundException If there are no sheets in the file */ public function __construct($filePath, $options, $sharedStringsHelper, $globalFunctionsHelper) { // Fetch all available sheets $sheetHelper = new SheetHelper($filePath, $options, $sharedStringsHelper, $globalFunctionsHelper); $this->sheets = $sheetHelper->getSheets(); if (count($this->sheets) === 0) { throw new NoSheetsFoundException('The file must contain at least one sheet.'); } } /** * Rewind the Iterator to the first element * @link http://php.net/manual/en/iterator.rewind.php * * @return void */ public function rewind() { $this->currentSheetIndex = 0; } /** * Checks if current position is valid * @link http://php.net/manual/en/iterator.valid.php * * @return bool */ public function valid() { return ($this->currentSheetIndex < count($this->sheets)); } /** * Move forward to next element * @link http://php.net/manual/en/iterator.next.php * * @return void */ public function next() { // Using isset here because it is way faster than array_key_exists... if (isset($this->sheets[$this->currentSheetIndex])) { $currentSheet = $this->sheets[$this->currentSheetIndex]; $currentSheet->getRowIterator()->end(); $this->currentSheetIndex++; } } /** * Return the current element * @link http://php.net/manual/en/iterator.current.php * * @return \Box\Spout\Reader\XLSX\Sheet */ public function current() { return $this->sheets[$this->currentSheetIndex]; } /** * Return the key of the current element * @link http://php.net/manual/en/iterator.key.php * * @return int */ public function key() { return $this->currentSheetIndex + 1; } /** * Cleans up what was created to iterate over the object. * * @return void */ public function end() { // make sure we are not leaking memory in case the iteration stopped before the end foreach ($this->sheets as $sheet) { $sheet->getRowIterator()->end(); } } } Reader/AbstractReader.php 0000644 00000016462 15073227061 0011361 0 ustar 00 <?php namespace Box\Spout\Reader; use Box\Spout\Common\Exception\IOException; use Box\Spout\Reader\Exception\ReaderNotOpenedException; /** * Class AbstractReader * * @package Box\Spout\Reader * @abstract */ abstract class AbstractReader implements ReaderInterface { /** @var bool Indicates whether the stream is currently open */ protected $isStreamOpened = false; /** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */ protected $globalFunctionsHelper; /** @var \Box\Spout\Reader\Common\ReaderOptions Reader's customized options */ protected $options; /** * Returns the reader's current options * * @return \Box\Spout\Reader\Common\ReaderOptions */ abstract protected function getOptions(); /** * Returns whether stream wrappers are supported * * @return bool */ abstract protected function doesSupportStreamWrapper(); /** * Opens the file at the given file path to make it ready to be read * * @param string $filePath Path of the file to be read * @return void */ abstract protected function openReader($filePath); /** * Returns an iterator to iterate over sheets. * * @return \Iterator To iterate over sheets */ abstract protected function getConcreteSheetIterator(); /** * Closes the reader. To be used after reading the file. * * @return AbstractReader */ abstract protected function closeReader(); /** * @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper * @return AbstractReader */ public function setGlobalFunctionsHelper($globalFunctionsHelper) { $this->globalFunctionsHelper = $globalFunctionsHelper; return $this; } /** * Sets whether date/time values should be returned as PHP objects or be formatted as strings. * * @api * @param bool $shouldFormatDates * @return AbstractReader */ public function setShouldFormatDates($shouldFormatDates) { $this->getOptions()->setShouldFormatDates($shouldFormatDates); return $this; } /** * Sets whether empty rows should be returned or skipped. * * @api * @param bool $shouldPreserveEmptyRows * @return AbstractReader */ public function setShouldPreserveEmptyRows($shouldPreserveEmptyRows) { $this->getOptions()->setShouldPreserveEmptyRows($shouldPreserveEmptyRows); return $this; } /** * Prepares the reader to read the given file. It also makes sure * that the file exists and is readable. * * @api * @param string $filePath Path of the file to be read * @return void * @throws \Box\Spout\Common\Exception\IOException If the file at the given path does not exist, is not readable or is corrupted */ public function open($filePath) { if ($this->isStreamWrapper($filePath) && (!$this->doesSupportStreamWrapper() || !$this->isSupportedStreamWrapper($filePath))) { throw new IOException("Could not open $filePath for reading! Stream wrapper used is not supported for this type of file."); } if (!$this->isPhpStream($filePath)) { // we skip the checks if the provided file path points to a PHP stream if (!$this->globalFunctionsHelper->file_exists($filePath)) { throw new IOException("Could not open $filePath for reading! File does not exist."); } else if (!$this->globalFunctionsHelper->is_readable($filePath)) { throw new IOException("Could not open $filePath for reading! File is not readable."); } } try { $fileRealPath = $this->getFileRealPath($filePath); $this->openReader($fileRealPath); $this->isStreamOpened = true; } catch (\Exception $exception) { throw new IOException("Could not open $filePath for reading! ({$exception->getMessage()})"); } } /** * Returns the real path of the given path. * If the given path is a valid stream wrapper, returns the path unchanged. * * @param string $filePath * @return string */ protected function getFileRealPath($filePath) { if ($this->isSupportedStreamWrapper($filePath)) { return $filePath; } // Need to use realpath to fix "Can't open file" on some Windows setup return realpath($filePath); } /** * Returns the scheme of the custom stream wrapper, if the path indicates a stream wrapper is used. * For example, php://temp => php, s3://path/to/file => s3... * * @param string $filePath Path of the file to be read * @return string|null The stream wrapper scheme or NULL if not a stream wrapper */ protected function getStreamWrapperScheme($filePath) { $streamScheme = null; if (preg_match('/^(\w+):\/\//', $filePath, $matches)) { $streamScheme = $matches[1]; } return $streamScheme; } /** * Checks if the given path is an unsupported stream wrapper * (like local path, php://temp, mystream://foo/bar...). * * @param string $filePath Path of the file to be read * @return bool Whether the given path is an unsupported stream wrapper */ protected function isStreamWrapper($filePath) { return ($this->getStreamWrapperScheme($filePath) !== null); } /** * Checks if the given path is an supported stream wrapper * (like php://temp, mystream://foo/bar...). * If the given path is a local path, returns true. * * @param string $filePath Path of the file to be read * @return bool Whether the given path is an supported stream wrapper */ protected function isSupportedStreamWrapper($filePath) { $streamScheme = $this->getStreamWrapperScheme($filePath); return ($streamScheme !== null) ? in_array($streamScheme, $this->globalFunctionsHelper->stream_get_wrappers()) : true; } /** * Checks if a path is a PHP stream (like php://output, php://memory, ...) * * @param string $filePath Path of the file to be read * @return bool Whether the given path maps to a PHP stream */ protected function isPhpStream($filePath) { $streamScheme = $this->getStreamWrapperScheme($filePath); return ($streamScheme === 'php'); } /** * Returns an iterator to iterate over sheets. * * @api * @return \Iterator To iterate over sheets * @throws \Box\Spout\Reader\Exception\ReaderNotOpenedException If called before opening the reader */ public function getSheetIterator() { if (!$this->isStreamOpened) { throw new ReaderNotOpenedException('Reader should be opened first.'); } return $this->getConcreteSheetIterator(); } /** * Closes the reader, preventing any additional reading * * @api * @return void */ public function close() { if ($this->isStreamOpened) { $this->closeReader(); $sheetIterator = $this->getConcreteSheetIterator(); if ($sheetIterator) { $sheetIterator->end(); } $this->isStreamOpened = false; } } } Reader/Exception/XMLProcessingException.php 0000644 00000000271 15073227061 0014774 0 ustar 00 <?php namespace Box\Spout\Reader\Exception; /** * Class XMLProcessingException * * @package Box\Spout\Reader\Exception */ class XMLProcessingException extends ReaderException { } Reader/Exception/ReaderException.php 0000644 00000000360 15073227061 0013500 0 ustar 00 <?php namespace Box\Spout\Reader\Exception; use Box\Spout\Common\Exception\SpoutException; /** * Class ReaderException * * @package Box\Spout\Reader\Exception * @abstract */ abstract class ReaderException extends SpoutException { } Reader/Exception/NoSheetsFoundException.php 0000644 00000000301 15073227061 0015015 0 ustar 00 <?php namespace Box\Spout\Reader\Exception; /** * Class NoSheetsFoundException * * @api * @package Box\Spout\Reader\Exception */ class NoSheetsFoundException extends ReaderException { } Reader/Exception/SharedStringNotFoundException.php 0000644 00000000317 15073227061 0016352 0 ustar 00 <?php namespace Box\Spout\Reader\Exception; /** * Class SharedStringNotFoundException * * @api * @package Box\Spout\Reader\Exception */ class SharedStringNotFoundException extends ReaderException { } Reader/Exception/ReaderNotOpenedException.php 0000644 00000000305 15073227061 0015313 0 ustar 00 <?php namespace Box\Spout\Reader\Exception; /** * Class ReaderNotOpenedException * * @api * @package Box\Spout\Reader\Exception */ class ReaderNotOpenedException extends ReaderException { } Reader/Exception/IteratorNotRewindableException.php 0000644 00000000321 15073227061 0016542 0 ustar 00 <?php namespace Box\Spout\Reader\Exception; /** * Class IteratorNotRewindableException * * @api * @package Box\Spout\Reader\Exception */ class IteratorNotRewindableException extends ReaderException { } Reader/ReaderInterface.php 0000644 00000001515 15073227061 0011507 0 ustar 00 <?php namespace Box\Spout\Reader; /** * Interface ReaderInterface * * @package Box\Spout\Reader */ interface ReaderInterface { /** * Prepares the reader to read the given file. It also makes sure * that the file exists and is readable. * * @param string $filePath Path of the file to be read * @return void * @throws \Box\Spout\Common\Exception\IOException */ public function open($filePath); /** * Returns an iterator to iterate over sheets. * * @return \Iterator To iterate over sheets * @throws \Box\Spout\Reader\Exception\ReaderNotOpenedException If called before opening the reader */ public function getSheetIterator(); /** * Closes the reader, preventing any additional reading * * @return void */ public function close(); } Reader/Wrapper/XMLInternalErrorsHelper.php 0000644 00000004216 15073227061 0014577 0 ustar 00 <?php namespace Box\Spout\Reader\Wrapper; use Box\Spout\Reader\Exception\XMLProcessingException; /** * Trait XMLInternalErrorsHelper * * @package Box\Spout\Reader\Wrapper */ trait XMLInternalErrorsHelper { /** @var bool Stores whether XML errors were initially stored internally - used to reset */ protected $initialUseInternalErrorsValue; /** * To avoid displaying lots of warning/error messages on screen, * stores errors internally instead. * * @return void */ protected function useXMLInternalErrors() { libxml_clear_errors(); $this->initialUseInternalErrorsValue = libxml_use_internal_errors(true); } /** * Throws an XMLProcessingException if an error occured. * It also always resets the "libxml_use_internal_errors" setting back to its initial value. * * @return void * @throws \Box\Spout\Reader\Exception\XMLProcessingException */ protected function resetXMLInternalErrorsSettingAndThrowIfXMLErrorOccured() { if ($this->hasXMLErrorOccured()) { $this->resetXMLInternalErrorsSetting(); throw new XMLProcessingException($this->getLastXMLErrorMessage()); } $this->resetXMLInternalErrorsSetting(); } /** * Returns whether the a XML error has occured since the last time errors were cleared. * * @return bool TRUE if an error occured, FALSE otherwise */ private function hasXMLErrorOccured() { return (libxml_get_last_error() !== false); } /** * Returns the error message for the last XML error that occured. * @see libxml_get_last_error * * @return String|null Last XML error message or null if no error */ private function getLastXMLErrorMessage() { $errorMessage = null; $error = libxml_get_last_error(); if ($error !== false) { $errorMessage = trim($error->message); } return $errorMessage; } /** * @return void */ protected function resetXMLInternalErrorsSetting() { libxml_use_internal_errors($this->initialUseInternalErrorsValue); } } Reader/Wrapper/XMLReader.php 0000644 00000013224 15073227061 0011667 0 ustar 00 <?php namespace Box\Spout\Reader\Wrapper; use DOMNode; /** * Class XMLReader * Wrapper around the built-in XMLReader * @see \XMLReader * * @package Box\Spout\Reader\Wrapper */ class XMLReader extends \XMLReader { use XMLInternalErrorsHelper; const ZIP_WRAPPER = 'zip://'; /** * Opens the XML Reader to read a file located inside a ZIP file. * * @param string $zipFilePath Path to the ZIP file * @param string $fileInsideZipPath Relative or absolute path of the file inside the zip * @return bool TRUE on success or FALSE on failure */ public function openFileInZip($zipFilePath, $fileInsideZipPath) { $wasOpenSuccessful = false; $realPathURI = $this->getRealPathURIForFileInZip($zipFilePath, $fileInsideZipPath); // We need to check first that the file we are trying to read really exist because: // - PHP emits a warning when trying to open a file that does not exist. // - HHVM does not check if file exists within zip file (@link https://github.com/facebook/hhvm/issues/5779) if ($this->fileExistsWithinZip($realPathURI)) { $wasOpenSuccessful = $this->open($realPathURI, null, LIBXML_NONET); } return $wasOpenSuccessful; } /** * Returns the real path for the given path components. * This is useful to avoid issues on some Windows setup. * * @param string $zipFilePath Path to the ZIP file * @param string $fileInsideZipPath Relative or absolute path of the file inside the zip * @return string The real path URI */ public function getRealPathURIForFileInZip($zipFilePath, $fileInsideZipPath) { return (self::ZIP_WRAPPER . realpath($zipFilePath) . '#' . $fileInsideZipPath); } /** * Returns whether the file at the given location exists * * @param string $zipStreamURI URI of a zip stream, e.g. "zip://file.zip#path/inside.xml" * @return bool TRUE if the file exists, FALSE otherwise */ protected function fileExistsWithinZip($zipStreamURI) { $doesFileExists = false; $pattern = '/zip:\/\/([^#]+)#(.*)/'; if (preg_match($pattern, $zipStreamURI, $matches)) { $zipFilePath = $matches[1]; $innerFilePath = $matches[2]; $zip = new \ZipArchive(); if ($zip->open($zipFilePath) === true) { $doesFileExists = ($zip->locateName($innerFilePath) !== false); $zip->close(); } } return $doesFileExists; } /** * Move to next node in document * @see \XMLReader::read * * @return bool TRUE on success or FALSE on failure * @throws \Box\Spout\Reader\Exception\XMLProcessingException If an error/warning occurred */ public function read() { $this->useXMLInternalErrors(); $wasReadSuccessful = parent::read(); $this->resetXMLInternalErrorsSettingAndThrowIfXMLErrorOccured(); return $wasReadSuccessful; } /** * Read until the element with the given name is found, or the end of the file. * * @param string $nodeName Name of the node to find * @return bool TRUE on success or FALSE on failure * @throws \Box\Spout\Reader\Exception\XMLProcessingException If an error/warning occurred */ public function readUntilNodeFound($nodeName) { do { $wasReadSuccessful = $this->read(); $isNotPositionedOnStartingNode = !$this->isPositionedOnStartingNode($nodeName); } while ($wasReadSuccessful && $isNotPositionedOnStartingNode); return $wasReadSuccessful; } /** * Move cursor to next node skipping all subtrees * @see \XMLReader::next * * @param string|void $localName The name of the next node to move to * @return bool TRUE on success or FALSE on failure * @throws \Box\Spout\Reader\Exception\XMLProcessingException If an error/warning occurred */ public function next($localName = null) { $this->useXMLInternalErrors(); $wasNextSuccessful = parent::next($localName); $this->resetXMLInternalErrorsSettingAndThrowIfXMLErrorOccured(); return $wasNextSuccessful; } /** * @param string $nodeName * @return bool Whether the XML Reader is currently positioned on the starting node with given name */ public function isPositionedOnStartingNode($nodeName) { return $this->isPositionedOnNode($nodeName, XMLReader::ELEMENT); } /** * @param string $nodeName * @return bool Whether the XML Reader is currently positioned on the ending node with given name */ public function isPositionedOnEndingNode($nodeName) { return $this->isPositionedOnNode($nodeName, XMLReader::END_ELEMENT); } /** * @param string $nodeName * @param int $nodeType * @return bool Whether the XML Reader is currently positioned on the node with given name and type */ private function isPositionedOnNode($nodeName, $nodeType) { // In some cases, the node has a prefix (for instance, "<sheet>" can also be "<x:sheet>"). // So if the given node name does not have a prefix, we need to look at the unprefixed name ("localName"). // @see https://github.com/box/spout/issues/233 $hasPrefix = (strpos($nodeName, ':') !== false); $currentNodeName = ($hasPrefix) ? $this->name : $this->localName; return ($this->nodeType === $nodeType && $currentNodeName === $nodeName); } /** * @return string The name of the current node, un-prefixed */ public function getCurrentNodeName() { return $this->localName; } } Reader/Common/XMLProcessor.php 0000644 00000013557 15073227061 0012265 0 ustar 00 <?php namespace Box\Spout\Reader\Common; use Box\Spout\Reader\Wrapper\XMLReader; /** * Class XMLProcessor * Helps process XML files * * @package Box\Spout\Reader\Common */ class XMLProcessor { /* Node types */ const NODE_TYPE_START = XMLReader::ELEMENT; const NODE_TYPE_END = XMLReader::END_ELEMENT; /* Keys associated to reflection attributes to invoke a callback */ const CALLBACK_REFLECTION_METHOD = 'reflectionMethod'; const CALLBACK_REFLECTION_OBJECT = 'reflectionObject'; /* Values returned by the callbacks to indicate what the processor should do next */ const PROCESSING_CONTINUE = 1; const PROCESSING_STOP = 2; /** @var \Box\Spout\Reader\Wrapper\XMLReader The XMLReader object that will help read sheet's XML data */ protected $xmlReader; /** @var array Registered callbacks */ private $callbacks = []; /** * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object */ public function __construct($xmlReader) { $this->xmlReader = $xmlReader; } /** * @param string $nodeName A callback may be triggered when a node with this name is read * @param int $nodeType Type of the node [NODE_TYPE_START || NODE_TYPE_END] * @param callable $callback Callback to execute when the read node has the given name and type * @return XMLProcessor */ public function registerCallback($nodeName, $nodeType, $callback) { $callbackKey = $this->getCallbackKey($nodeName, $nodeType); $this->callbacks[$callbackKey] = $this->getInvokableCallbackData($callback); return $this; } /** * @param string $nodeName Name of the node * @param int $nodeType Type of the node [NODE_TYPE_START || NODE_TYPE_END] * @return string Key used to store the associated callback */ private function getCallbackKey($nodeName, $nodeType) { return "$nodeName$nodeType"; } /** * Because the callback can be a "protected" function, we don't want to use call_user_func() directly * but instead invoke the callback using Reflection. This allows the invocation of "protected" functions. * Since some functions can be called a lot, we pre-process the callback to only return the elements that * will be needed to invoke the callback later. * * @param callable $callback Array reference to a callback: [OBJECT, METHOD_NAME] * @return array Associative array containing the elements needed to invoke the callback using Reflection */ private function getInvokableCallbackData($callback) { $callbackObject = $callback[0]; $callbackMethodName = $callback[1]; $reflectionMethod = new \ReflectionMethod(get_class($callbackObject), $callbackMethodName); $reflectionMethod->setAccessible(true); return [ self::CALLBACK_REFLECTION_METHOD => $reflectionMethod, self::CALLBACK_REFLECTION_OBJECT => $callbackObject, ]; } /** * Resumes the reading of the XML file where it was left off. * Stops whenever a callback indicates that reading should stop or at the end of the file. * * @return void * @throws \Box\Spout\Reader\Exception\XMLProcessingException */ public function readUntilStopped() { while ($this->xmlReader->read()) { $nodeType = $this->xmlReader->nodeType; $nodeNamePossiblyWithPrefix = $this->xmlReader->name; $nodeNameWithoutPrefix = $this->xmlReader->localName; $callbackData = $this->getRegisteredCallbackData($nodeNamePossiblyWithPrefix, $nodeNameWithoutPrefix, $nodeType); if ($callbackData !== null) { $callbackResponse = $this->invokeCallback($callbackData, [$this->xmlReader]); if ($callbackResponse === self::PROCESSING_STOP) { // stop reading break; } } } } /** * @param string $nodeNamePossiblyWithPrefix Name of the node, possibly prefixed * @param string $nodeNameWithoutPrefix Name of the same node, un-prefixed * @param int $nodeType Type of the node [NODE_TYPE_START || NODE_TYPE_END] * @return array|null Callback data to be used for execution when a node of the given name/type is read or NULL if none found */ private function getRegisteredCallbackData($nodeNamePossiblyWithPrefix, $nodeNameWithoutPrefix, $nodeType) { // With prefixed nodes, we should match if (by order of preference): // 1. the callback was registered with the prefixed node name (e.g. "x:worksheet") // 2. the callback was registered with the un-prefixed node name (e.g. "worksheet") $callbackKeyForPossiblyPrefixedName = $this->getCallbackKey($nodeNamePossiblyWithPrefix, $nodeType); $callbackKeyForUnPrefixedName = $this->getCallbackKey($nodeNameWithoutPrefix, $nodeType); $hasPrefix = ($nodeNamePossiblyWithPrefix !== $nodeNameWithoutPrefix); $callbackKeyToUse = $callbackKeyForUnPrefixedName; if ($hasPrefix && isset($this->callbacks[$callbackKeyForPossiblyPrefixedName])) { $callbackKeyToUse = $callbackKeyForPossiblyPrefixedName; } // Using isset here because it is way faster than array_key_exists... return isset($this->callbacks[$callbackKeyToUse]) ? $this->callbacks[$callbackKeyToUse] : null; } /** * @param array $callbackData Associative array containing data to invoke the callback using Reflection * @param array $args Arguments to pass to the callback * @return int Callback response */ private function invokeCallback($callbackData, $args) { $reflectionMethod = $callbackData[self::CALLBACK_REFLECTION_METHOD]; $callbackObject = $callbackData[self::CALLBACK_REFLECTION_OBJECT]; return $reflectionMethod->invokeArgs($callbackObject, $args); } } Reader/Common/ReaderOptions.php 0000644 00000002752 15073227061 0012476 0 ustar 00 <?php namespace Box\Spout\Reader\Common; /** * Class ReaderOptions * Readers' common options * * @package Box\Spout\Reader\Common */ class ReaderOptions { /** @var bool Whether date/time values should be returned as PHP objects or be formatted as strings */ protected $shouldFormatDates = false; /** @var bool Whether empty rows should be returned or skipped */ protected $shouldPreserveEmptyRows = false; /** * @return bool Whether date/time values should be returned as PHP objects or be formatted as strings. */ public function shouldFormatDates() { return $this->shouldFormatDates; } /** * Sets whether date/time values should be returned as PHP objects or be formatted as strings. * * @param bool $shouldFormatDates * @return ReaderOptions */ public function setShouldFormatDates($shouldFormatDates) { $this->shouldFormatDates = $shouldFormatDates; return $this; } /** * @return bool Whether empty rows should be returned or skipped. */ public function shouldPreserveEmptyRows() { return $this->shouldPreserveEmptyRows; } /** * Sets whether empty rows should be returned or skipped. * * @param bool $shouldPreserveEmptyRows * @return ReaderOptions */ public function setShouldPreserveEmptyRows($shouldPreserveEmptyRows) { $this->shouldPreserveEmptyRows = $shouldPreserveEmptyRows; return $this; } } Reader/CSV/RowIterator.php 0000644 00000020737 15073227061 0011407 0 ustar 00 <?php namespace Box\Spout\Reader\CSV; use Box\Spout\Reader\IteratorInterface; use Box\Spout\Common\Helper\EncodingHelper; /** * Class RowIterator * Iterate over CSV rows. * * @package Box\Spout\Reader\CSV */ class RowIterator implements IteratorInterface { /** * Value passed to fgetcsv. 0 means "unlimited" (slightly slower but accomodates for very long lines). */ const MAX_READ_BYTES_PER_LINE = 0; /** @var resource Pointer to the CSV file to read */ protected $filePointer; /** @var int Number of read rows */ protected $numReadRows = 0; /** @var array|null Buffer used to store the row data, while checking if there are more rows to read */ protected $rowDataBuffer = null; /** @var bool Indicates whether all rows have been read */ protected $hasReachedEndOfFile = false; /** @var string Defines the character used to delimit fields (one character only) */ protected $fieldDelimiter; /** @var string Defines the character used to enclose fields (one character only) */ protected $fieldEnclosure; /** @var string Encoding of the CSV file to be read */ protected $encoding; /** @var string End of line delimiter, given by the user as input. */ protected $inputEOLDelimiter; /** @var bool Whether empty rows should be returned or skipped */ protected $shouldPreserveEmptyRows; /** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */ protected $globalFunctionsHelper; /** @var \Box\Spout\Common\Helper\EncodingHelper Helper to work with different encodings */ protected $encodingHelper; /** @var string End of line delimiter, encoded using the same encoding as the CSV */ protected $encodedEOLDelimiter; /** * @param resource $filePointer Pointer to the CSV file to read * @param \Box\Spout\Reader\CSV\ReaderOptions $options * @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper */ public function __construct($filePointer, $options, $globalFunctionsHelper) { $this->filePointer = $filePointer; $this->fieldDelimiter = $options->getFieldDelimiter(); $this->fieldEnclosure = $options->getFieldEnclosure(); $this->encoding = $options->getEncoding(); $this->inputEOLDelimiter = $options->getEndOfLineCharacter(); $this->shouldPreserveEmptyRows = $options->shouldPreserveEmptyRows(); $this->globalFunctionsHelper = $globalFunctionsHelper; $this->encodingHelper = new EncodingHelper($globalFunctionsHelper); } /** * Rewind the Iterator to the first element * @link http://php.net/manual/en/iterator.rewind.php * * @return void */ public function rewind() { $this->rewindAndSkipBom(); $this->numReadRows = 0; $this->rowDataBuffer = null; $this->next(); } /** * This rewinds and skips the BOM if inserted at the beginning of the file * by moving the file pointer after it, so that it is not read. * * @return void */ protected function rewindAndSkipBom() { $byteOffsetToSkipBom = $this->encodingHelper->getBytesOffsetToSkipBOM($this->filePointer, $this->encoding); // sets the cursor after the BOM (0 means no BOM, so rewind it) $this->globalFunctionsHelper->fseek($this->filePointer, $byteOffsetToSkipBom); } /** * Checks if current position is valid * @link http://php.net/manual/en/iterator.valid.php * * @return bool */ public function valid() { return ($this->filePointer && !$this->hasReachedEndOfFile); } /** * Move forward to next element. Reads data for the next unprocessed row. * @link http://php.net/manual/en/iterator.next.php * * @return void * @throws \Box\Spout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8 */ public function next() { $this->hasReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer); if (!$this->hasReachedEndOfFile) { $this->readDataForNextRow(); } } /** * @return void * @throws \Box\Spout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8 */ protected function readDataForNextRow() { do { $rowData = $this->getNextUTF8EncodedRow(); } while ($this->shouldReadNextRow($rowData)); if ($rowData !== false) { // str_replace will replace NULL values by empty strings $this->rowDataBuffer = str_replace(null, null, $rowData); $this->numReadRows++; } else { // If we reach this point, it means end of file was reached. // This happens when the last lines are empty lines. $this->hasReachedEndOfFile = true; } } /** * @param array|bool $currentRowData * @return bool Whether the data for the current row can be returned or if we need to keep reading */ protected function shouldReadNextRow($currentRowData) { $hasSuccessfullyFetchedRowData = ($currentRowData !== false); $hasNowReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer); $isEmptyLine = $this->isEmptyLine($currentRowData); return ( (!$hasSuccessfullyFetchedRowData && !$hasNowReachedEndOfFile) || (!$this->shouldPreserveEmptyRows && $isEmptyLine) ); } /** * Returns the next row, converted if necessary to UTF-8. * As fgetcsv() does not manage correctly encoding for non UTF-8 data, * we remove manually whitespace with ltrim or rtrim (depending on the order of the bytes) * * @return array|false The row for the current file pointer, encoded in UTF-8 or FALSE if nothing to read * @throws \Box\Spout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8 */ protected function getNextUTF8EncodedRow() { $encodedRowData = $this->globalFunctionsHelper->fgetcsv($this->filePointer, self::MAX_READ_BYTES_PER_LINE, $this->fieldDelimiter, $this->fieldEnclosure); if ($encodedRowData === false) { return false; } foreach ($encodedRowData as $cellIndex => $cellValue) { switch($this->encoding) { case EncodingHelper::ENCODING_UTF16_LE: case EncodingHelper::ENCODING_UTF32_LE: // remove whitespace from the beginning of a string as fgetcsv() add extra whitespace when it try to explode non UTF-8 data $cellValue = ltrim($cellValue); break; case EncodingHelper::ENCODING_UTF16_BE: case EncodingHelper::ENCODING_UTF32_BE: // remove whitespace from the end of a string as fgetcsv() add extra whitespace when it try to explode non UTF-8 data $cellValue = rtrim($cellValue); break; } $encodedRowData[$cellIndex] = $this->encodingHelper->attemptConversionToUTF8($cellValue, $this->encoding); } return $encodedRowData; } /** * Returns the end of line delimiter, encoded using the same encoding as the CSV. * The return value is cached. * * @return string */ protected function getEncodedEOLDelimiter() { if (!isset($this->encodedEOLDelimiter)) { $this->encodedEOLDelimiter = $this->encodingHelper->attemptConversionFromUTF8($this->inputEOLDelimiter, $this->encoding); } return $this->encodedEOLDelimiter; } /** * @param array|bool $lineData Array containing the cells value for the line * @return bool Whether the given line is empty */ protected function isEmptyLine($lineData) { return (is_array($lineData) && count($lineData) === 1 && $lineData[0] === null); } /** * Return the current element from the buffer * @link http://php.net/manual/en/iterator.current.php * * @return array|null */ public function current() { return $this->rowDataBuffer; } /** * Return the key of the current element * @link http://php.net/manual/en/iterator.key.php * * @return int */ public function key() { return $this->numReadRows; } /** * Cleans up what was created to iterate over the object. * * @return void */ public function end() { // do nothing } } Reader/CSV/Reader.php 0000644 00000007625 15073227061 0010331 0 ustar 00 <?php namespace Box\Spout\Reader\CSV; use Box\Spout\Reader\AbstractReader; use Box\Spout\Common\Exception\IOException; /** * Class Reader * This class provides support to read data from a CSV file. * * @package Box\Spout\Reader\CSV */ class Reader extends AbstractReader { /** @var resource Pointer to the file to be written */ protected $filePointer; /** @var SheetIterator To iterator over the CSV unique "sheet" */ protected $sheetIterator; /** @var string Original value for the "auto_detect_line_endings" INI value */ protected $originalAutoDetectLineEndings; /** * Returns the reader's current options * * @return ReaderOptions */ protected function getOptions() { if (!isset($this->options)) { $this->options = new ReaderOptions(); } return $this->options; } /** * Sets the field delimiter for the CSV. * Needs to be called before opening the reader. * * @param string $fieldDelimiter Character that delimits fields * @return Reader */ public function setFieldDelimiter($fieldDelimiter) { $this->getOptions()->setFieldDelimiter($fieldDelimiter); return $this; } /** * Sets the field enclosure for the CSV. * Needs to be called before opening the reader. * * @param string $fieldEnclosure Character that enclose fields * @return Reader */ public function setFieldEnclosure($fieldEnclosure) { $this->getOptions()->setFieldEnclosure($fieldEnclosure); return $this; } /** * Sets the encoding of the CSV file to be read. * Needs to be called before opening the reader. * * @param string $encoding Encoding of the CSV file to be read * @return Reader */ public function setEncoding($encoding) { $this->getOptions()->setEncoding($encoding); return $this; } /** * Sets the EOL for the CSV. * Needs to be called before opening the reader. * * @param string $endOfLineCharacter used to properly get lines from the CSV file. * @return Reader */ public function setEndOfLineCharacter($endOfLineCharacter) { $this->getOptions()->setEndOfLineCharacter($endOfLineCharacter); return $this; } /** * Returns whether stream wrappers are supported * * @return bool */ protected function doesSupportStreamWrapper() { return true; } /** * Opens the file at the given path to make it ready to be read. * If setEncoding() was not called, it assumes that the file is encoded in UTF-8. * * @param string $filePath Path of the CSV file to be read * @return void * @throws \Box\Spout\Common\Exception\IOException */ protected function openReader($filePath) { $this->originalAutoDetectLineEndings = ini_get('auto_detect_line_endings'); ini_set('auto_detect_line_endings', '1'); $this->filePointer = $this->globalFunctionsHelper->fopen($filePath, 'r'); if (!$this->filePointer) { throw new IOException("Could not open file $filePath for reading."); } $this->sheetIterator = new SheetIterator( $this->filePointer, $this->getOptions(), $this->globalFunctionsHelper ); } /** * Returns an iterator to iterate over sheets. * * @return SheetIterator To iterate over sheets */ protected function getConcreteSheetIterator() { return $this->sheetIterator; } /** * Closes the reader. To be used after reading the file. * * @return void */ protected function closeReader() { if ($this->filePointer) { $this->globalFunctionsHelper->fclose($this->filePointer); } ini_set('auto_detect_line_endings', $this->originalAutoDetectLineEndings); } } Reader/CSV/Sheet.php 0000644 00000002451 15073227061 0010167 0 ustar 00 <?php namespace Box\Spout\Reader\CSV; use Box\Spout\Reader\SheetInterface; /** * Class Sheet * * @package Box\Spout\Reader\CSV */ class Sheet implements SheetInterface { /** @var \Box\Spout\Reader\CSV\RowIterator To iterate over the CSV's rows */ protected $rowIterator; /** * @param resource $filePointer Pointer to the CSV file to read * @param \Box\Spout\Reader\CSV\ReaderOptions $options * @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper */ public function __construct($filePointer, $options, $globalFunctionsHelper) { $this->rowIterator = new RowIterator($filePointer, $options, $globalFunctionsHelper); } /** * @api * @return \Box\Spout\Reader\CSV\RowIterator */ public function getRowIterator() { return $this->rowIterator; } /** * @api * @return int Index of the sheet */ public function getIndex() { return 0; } /** * @api * @return string Name of the sheet - empty string since CSV does not support that */ public function getName() { return ''; } /** * @api * @return bool Always TRUE as there is only one sheet */ public function isActive() { return true; } } Reader/CSV/ReaderOptions.php 0000644 00000005215 15073227061 0011676 0 ustar 00 <?php namespace Box\Spout\Reader\CSV; use Box\Spout\Common\Helper\EncodingHelper; /** * Class ReaderOptions * This class is used to customize the reader's behavior * * @package Box\Spout\Reader\CSV */ class ReaderOptions extends \Box\Spout\Reader\Common\ReaderOptions { /** @var string Defines the character used to delimit fields (one character only) */ protected $fieldDelimiter = ','; /** @var string Defines the character used to enclose fields (one character only) */ protected $fieldEnclosure = '"'; /** @var string Encoding of the CSV file to be read */ protected $encoding = EncodingHelper::ENCODING_UTF8; /** @var string Defines the End of line */ protected $endOfLineCharacter = "\n"; /** * @return string */ public function getFieldDelimiter() { return $this->fieldDelimiter; } /** * Sets the field delimiter for the CSV. * Needs to be called before opening the reader. * * @param string $fieldDelimiter Character that delimits fields * @return ReaderOptions */ public function setFieldDelimiter($fieldDelimiter) { $this->fieldDelimiter = $fieldDelimiter; return $this; } /** * @return string */ public function getFieldEnclosure() { return $this->fieldEnclosure; } /** * Sets the field enclosure for the CSV. * Needs to be called before opening the reader. * * @param string $fieldEnclosure Character that enclose fields * @return ReaderOptions */ public function setFieldEnclosure($fieldEnclosure) { $this->fieldEnclosure = $fieldEnclosure; return $this; } /** * @return string */ public function getEncoding() { return $this->encoding; } /** * Sets the encoding of the CSV file to be read. * Needs to be called before opening the reader. * * @param string $encoding Encoding of the CSV file to be read * @return ReaderOptions */ public function setEncoding($encoding) { $this->encoding = $encoding; return $this; } /** * @return string EOL for the CSV */ public function getEndOfLineCharacter() { return $this->endOfLineCharacter; } /** * Sets the EOL for the CSV. * Needs to be called before opening the reader. * * @param string $endOfLineCharacter used to properly get lines from the CSV file. * @return ReaderOptions */ public function setEndOfLineCharacter($endOfLineCharacter) { $this->endOfLineCharacter = $endOfLineCharacter; return $this; } } Reader/CSV/SheetIterator.php 0000644 00000004046 15073227061 0011703 0 ustar 00 <?php namespace Box\Spout\Reader\CSV; use Box\Spout\Reader\IteratorInterface; /** * Class SheetIterator * Iterate over CSV unique "sheet". * * @package Box\Spout\Reader\CSV */ class SheetIterator implements IteratorInterface { /** @var \Box\Spout\Reader\CSV\Sheet The CSV unique "sheet" */ protected $sheet; /** @var bool Whether the unique "sheet" has already been read */ protected $hasReadUniqueSheet = false; /** * @param resource $filePointer * @param \Box\Spout\Reader\CSV\ReaderOptions $options * @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper */ public function __construct($filePointer, $options, $globalFunctionsHelper) { $this->sheet = new Sheet($filePointer, $options, $globalFunctionsHelper); } /** * Rewind the Iterator to the first element * @link http://php.net/manual/en/iterator.rewind.php * * @return void */ public function rewind() { $this->hasReadUniqueSheet = false; } /** * Checks if current position is valid * @link http://php.net/manual/en/iterator.valid.php * * @return bool */ public function valid() { return (!$this->hasReadUniqueSheet); } /** * Move forward to next element * @link http://php.net/manual/en/iterator.next.php * * @return void */ public function next() { $this->hasReadUniqueSheet = true; } /** * Return the current element * @link http://php.net/manual/en/iterator.current.php * * @return \Box\Spout\Reader\CSV\Sheet */ public function current() { return $this->sheet; } /** * Return the key of the current element * @link http://php.net/manual/en/iterator.key.php * * @return int */ public function key() { return 1; } /** * Cleans up what was created to iterate over the object. * * @return void */ public function end() { // do nothing } } Reader/ODS/RowIterator.php 0000644 00000032663 15073227061 0011402 0 ustar 00 <?php namespace Box\Spout\Reader\ODS; use Box\Spout\Common\Exception\IOException; use Box\Spout\Reader\Exception\IteratorNotRewindableException; use Box\Spout\Reader\Exception\XMLProcessingException; use Box\Spout\Reader\IteratorInterface; use Box\Spout\Reader\ODS\Helper\CellValueFormatter; use Box\Spout\Reader\Wrapper\XMLReader; use Box\Spout\Reader\Common\XMLProcessor; /** * Class RowIterator * * @package Box\Spout\Reader\ODS */ class RowIterator implements IteratorInterface { /** Definition of XML nodes names used to parse data */ const XML_NODE_TABLE = 'table:table'; const XML_NODE_ROW = 'table:table-row'; const XML_NODE_CELL = 'table:table-cell'; const MAX_COLUMNS_EXCEL = 16384; /** Definition of XML attribute used to parse data */ const XML_ATTRIBUTE_NUM_ROWS_REPEATED = 'table:number-rows-repeated'; const XML_ATTRIBUTE_NUM_COLUMNS_REPEATED = 'table:number-columns-repeated'; /** @var \Box\Spout\Reader\Wrapper\XMLReader The XMLReader object that will help read sheet's XML data */ protected $xmlReader; /** @var \Box\Spout\Reader\Common\XMLProcessor Helper Object to process XML nodes */ protected $xmlProcessor; /** @var bool Whether empty rows should be returned or skipped */ protected $shouldPreserveEmptyRows; /** @var Helper\CellValueFormatter Helper to format cell values */ protected $cellValueFormatter; /** @var bool Whether the iterator has already been rewound once */ protected $hasAlreadyBeenRewound = false; /** @var array Contains the data for the currently processed row (key = cell index, value = cell value) */ protected $currentlyProcessedRowData = []; /** @var array|null Buffer used to store the row data, while checking if there are more rows to read */ protected $rowDataBuffer = null; /** @var bool Indicates whether all rows have been read */ protected $hasReachedEndOfFile = false; /** @var int Last row index processed (one-based) */ protected $lastRowIndexProcessed = 0; /** @var int Row index to be processed next (one-based) */ protected $nextRowIndexToBeProcessed = 1; /** @var mixed|null Value of the last processed cell (because when reading cell at column N+1, cell N is processed) */ protected $lastProcessedCellValue = null; /** @var int Number of times the last processed row should be repeated */ protected $numRowsRepeated = 1; /** @var int Number of times the last cell value should be copied to the cells on its right */ protected $numColumnsRepeated = 1; /** @var bool Whether at least one cell has been read for the row currently being processed */ protected $hasAlreadyReadOneCellInCurrentRow = false; /** * @param XMLReader $xmlReader XML Reader, positioned on the "<table:table>" element * @param \Box\Spout\Reader\ODS\ReaderOptions $options Reader's current options */ public function __construct($xmlReader, $options) { $this->xmlReader = $xmlReader; $this->shouldPreserveEmptyRows = $options->shouldPreserveEmptyRows(); $this->cellValueFormatter = new CellValueFormatter($options->shouldFormatDates()); // Register all callbacks to process different nodes when reading the XML file $this->xmlProcessor = new XMLProcessor($this->xmlReader); $this->xmlProcessor->registerCallback(self::XML_NODE_ROW, XMLProcessor::NODE_TYPE_START, [$this, 'processRowStartingNode']); $this->xmlProcessor->registerCallback(self::XML_NODE_CELL, XMLProcessor::NODE_TYPE_START, [$this, 'processCellStartingNode']); $this->xmlProcessor->registerCallback(self::XML_NODE_ROW, XMLProcessor::NODE_TYPE_END, [$this, 'processRowEndingNode']); $this->xmlProcessor->registerCallback(self::XML_NODE_TABLE, XMLProcessor::NODE_TYPE_END, [$this, 'processTableEndingNode']); } /** * Rewind the Iterator to the first element. * NOTE: It can only be done once, as it is not possible to read an XML file backwards. * @link http://php.net/manual/en/iterator.rewind.php * * @return void * @throws \Box\Spout\Reader\Exception\IteratorNotRewindableException If the iterator is rewound more than once */ public function rewind() { // Because sheet and row data is located in the file, we can't rewind both the // sheet iterator and the row iterator, as XML file cannot be read backwards. // Therefore, rewinding the row iterator has been disabled. if ($this->hasAlreadyBeenRewound) { throw new IteratorNotRewindableException(); } $this->hasAlreadyBeenRewound = true; $this->lastRowIndexProcessed = 0; $this->nextRowIndexToBeProcessed = 1; $this->rowDataBuffer = null; $this->hasReachedEndOfFile = false; $this->next(); } /** * Checks if current position is valid * @link http://php.net/manual/en/iterator.valid.php * * @return bool */ public function valid() { return (!$this->hasReachedEndOfFile); } /** * Move forward to next element. Empty rows will be skipped. * @link http://php.net/manual/en/iterator.next.php * * @return void * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If a shared string was not found * @throws \Box\Spout\Common\Exception\IOException If unable to read the sheet data XML */ public function next() { if ($this->doesNeedDataForNextRowToBeProcessed()) { $this->readDataForNextRow(); } $this->lastRowIndexProcessed++; } /** * Returns whether we need data for the next row to be processed. * We DO need to read data if: * - we have not read any rows yet * OR * - the next row to be processed immediately follows the last read row * * @return bool Whether we need data for the next row to be processed. */ protected function doesNeedDataForNextRowToBeProcessed() { $hasReadAtLeastOneRow = ($this->lastRowIndexProcessed !== 0); return ( !$hasReadAtLeastOneRow || $this->lastRowIndexProcessed === $this->nextRowIndexToBeProcessed - 1 ); } /** * @return void * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If a shared string was not found * @throws \Box\Spout\Common\Exception\IOException If unable to read the sheet data XML */ protected function readDataForNextRow() { $this->currentlyProcessedRowData = []; try { $this->xmlProcessor->readUntilStopped(); } catch (XMLProcessingException $exception) { throw new IOException("The sheet's data cannot be read. [{$exception->getMessage()}]"); } $this->rowDataBuffer = $this->currentlyProcessedRowData; } /** * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<table:table-row>" starting node * @return int A return code that indicates what action should the processor take next */ protected function processRowStartingNode($xmlReader) { // Reset data from current row $this->hasAlreadyReadOneCellInCurrentRow = false; $this->lastProcessedCellValue = null; $this->numColumnsRepeated = 1; $this->numRowsRepeated = $this->getNumRowsRepeatedForCurrentNode($xmlReader); return XMLProcessor::PROCESSING_CONTINUE; } /** * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<table:table-cell>" starting node * @return int A return code that indicates what action should the processor take next */ protected function processCellStartingNode($xmlReader) { $currentNumColumnsRepeated = $this->getNumColumnsRepeatedForCurrentNode($xmlReader); // NOTE: expand() will automatically decode all XML entities of the child nodes $node = $xmlReader->expand(); $currentCellValue = $this->getCellValue($node); // process cell N only after having read cell N+1 (see below why) if ($this->hasAlreadyReadOneCellInCurrentRow) { for ($i = 0; $i < $this->numColumnsRepeated; $i++) { $this->currentlyProcessedRowData[] = $this->lastProcessedCellValue; } } $this->hasAlreadyReadOneCellInCurrentRow = true; $this->lastProcessedCellValue = $currentCellValue; $this->numColumnsRepeated = $currentNumColumnsRepeated; return XMLProcessor::PROCESSING_CONTINUE; } /** * @return int A return code that indicates what action should the processor take next */ protected function processRowEndingNode() { $isEmptyRow = $this->isEmptyRow($this->currentlyProcessedRowData, $this->lastProcessedCellValue); // if the fetched row is empty and we don't want to preserve it... if (!$this->shouldPreserveEmptyRows && $isEmptyRow) { // ... skip it return XMLProcessor::PROCESSING_CONTINUE; } // if the row is empty, we don't want to return more than one cell $actualNumColumnsRepeated = (!$isEmptyRow) ? $this->numColumnsRepeated : 1; // Only add the value if the last read cell is not a trailing empty cell repeater in Excel. // The current count of read columns is determined by counting the values in "$this->currentlyProcessedRowData". // This is to avoid creating a lot of empty cells, as Excel adds a last empty "<table:table-cell>" // with a number-columns-repeated value equals to the number of (supported columns - used columns). // In Excel, the number of supported columns is 16384, but we don't want to returns rows with // always 16384 cells. if ((count($this->currentlyProcessedRowData) + $actualNumColumnsRepeated) !== self::MAX_COLUMNS_EXCEL) { for ($i = 0; $i < $actualNumColumnsRepeated; $i++) { $this->currentlyProcessedRowData[] = $this->lastProcessedCellValue; } } // If we are processing row N and the row is repeated M times, // then the next row to be processed will be row (N+M). $this->nextRowIndexToBeProcessed += $this->numRowsRepeated; // at this point, we have all the data we need for the row // so that we can populate the buffer return XMLProcessor::PROCESSING_STOP; } /** * @return int A return code that indicates what action should the processor take next */ protected function processTableEndingNode() { // The closing "</table:table>" marks the end of the file $this->hasReachedEndOfFile = true; return XMLProcessor::PROCESSING_STOP; } /** * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<table:table-row>" starting node * @return int The value of "table:number-rows-repeated" attribute of the current node, or 1 if attribute missing */ protected function getNumRowsRepeatedForCurrentNode($xmlReader) { $numRowsRepeated = $xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_ROWS_REPEATED); return ($numRowsRepeated !== null) ? intval($numRowsRepeated) : 1; } /** * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "<table:table-cell>" starting node * @return int The value of "table:number-columns-repeated" attribute of the current node, or 1 if attribute missing */ protected function getNumColumnsRepeatedForCurrentNode($xmlReader) { $numColumnsRepeated = $xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_COLUMNS_REPEATED); return ($numColumnsRepeated !== null) ? intval($numColumnsRepeated) : 1; } /** * Returns the (unescaped) correctly marshalled, cell value associated to the given XML node. * * @param \DOMNode $node * @return string|int|float|bool|\DateTime|\DateInterval|null The value associated with the cell, empty string if cell's type is void/undefined, null on error */ protected function getCellValue($node) { return $this->cellValueFormatter->extractAndFormatNodeValue($node); } /** * After finishing processing each cell, a row is considered empty if it contains * no cells or if the value of the last read cell is an empty string. * After finishing processing each cell, the last read cell is not part of the * row data yet (as we still need to apply the "num-columns-repeated" attribute). * * @param array $rowData * @param string|int|float|bool|\DateTime|\DateInterval|null The value of the last read cell * @return bool Whether the row is empty */ protected function isEmptyRow($rowData, $lastReadCellValue) { return ( count($rowData) === 0 && (!isset($lastReadCellValue) || trim($lastReadCellValue) === '') ); } /** * Return the current element, from the buffer. * @link http://php.net/manual/en/iterator.current.php * * @return array|null */ public function current() { return $this->rowDataBuffer; } /** * Return the key of the current element * @link http://php.net/manual/en/iterator.key.php * * @return int */ public function key() { return $this->lastRowIndexProcessed; } /** * Cleans up what was created to iterate over the object. * * @return void */ public function end() { $this->xmlReader->close(); } } Reader/ODS/Helper/CellValueFormatter.php 0000644 00000020414 15073227061 0014067 0 ustar 00 <?php namespace Box\Spout\Reader\ODS\Helper; /** * Class CellValueFormatter * This class provides helper functions to format cell values * * @package Box\Spout\Reader\ODS\Helper */ class CellValueFormatter { /** Definition of all possible cell types */ const CELL_TYPE_STRING = 'string'; const CELL_TYPE_FLOAT = 'float'; const CELL_TYPE_BOOLEAN = 'boolean'; const CELL_TYPE_DATE = 'date'; const CELL_TYPE_TIME = 'time'; const CELL_TYPE_CURRENCY = 'currency'; const CELL_TYPE_PERCENTAGE = 'percentage'; const CELL_TYPE_VOID = 'void'; /** Definition of XML nodes names used to parse data */ const XML_NODE_P = 'p'; const XML_NODE_S = 'text:s'; const XML_NODE_A = 'text:a'; const XML_NODE_SPAN = 'text:span'; /** Definition of XML attributes used to parse data */ const XML_ATTRIBUTE_TYPE = 'office:value-type'; const XML_ATTRIBUTE_VALUE = 'office:value'; const XML_ATTRIBUTE_BOOLEAN_VALUE = 'office:boolean-value'; const XML_ATTRIBUTE_DATE_VALUE = 'office:date-value'; const XML_ATTRIBUTE_TIME_VALUE = 'office:time-value'; const XML_ATTRIBUTE_CURRENCY = 'office:currency'; const XML_ATTRIBUTE_C = 'text:c'; /** @var bool Whether date/time values should be returned as PHP objects or be formatted as strings */ protected $shouldFormatDates; /** @var \Box\Spout\Common\Escaper\ODS Used to unescape XML data */ protected $escaper; /** * @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings */ public function __construct($shouldFormatDates) { $this->shouldFormatDates = $shouldFormatDates; /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ $this->escaper = \Box\Spout\Common\Escaper\ODS::getInstance(); } /** * Returns the (unescaped) correctly marshalled, cell value associated to the given XML node. * @see http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#refTable13 * * @param \DOMNode $node * @return string|int|float|bool|\DateTime|\DateInterval|null The value associated with the cell, empty string if cell's type is void/undefined, null on error */ public function extractAndFormatNodeValue($node) { $cellType = $node->getAttribute(self::XML_ATTRIBUTE_TYPE); switch ($cellType) { case self::CELL_TYPE_STRING: return $this->formatStringCellValue($node); case self::CELL_TYPE_FLOAT: return $this->formatFloatCellValue($node); case self::CELL_TYPE_BOOLEAN: return $this->formatBooleanCellValue($node); case self::CELL_TYPE_DATE: return $this->formatDateCellValue($node); case self::CELL_TYPE_TIME: return $this->formatTimeCellValue($node); case self::CELL_TYPE_CURRENCY: return $this->formatCurrencyCellValue($node); case self::CELL_TYPE_PERCENTAGE: return $this->formatPercentageCellValue($node); case self::CELL_TYPE_VOID: default: return ''; } } /** * Returns the cell String value. * * @param \DOMNode $node * @return string The value associated with the cell */ protected function formatStringCellValue($node) { $pNodeValues = []; $pNodes = $node->getElementsByTagName(self::XML_NODE_P); foreach ($pNodes as $pNode) { $currentPValue = ''; foreach ($pNode->childNodes as $childNode) { if ($childNode instanceof \DOMText) { $currentPValue .= $childNode->nodeValue; } else if ($childNode->nodeName === self::XML_NODE_S) { $spaceAttribute = $childNode->getAttribute(self::XML_ATTRIBUTE_C); $numSpaces = (!empty($spaceAttribute)) ? intval($spaceAttribute) : 1; $currentPValue .= str_repeat(' ', $numSpaces); } else if ($childNode->nodeName === self::XML_NODE_A || $childNode->nodeName === self::XML_NODE_SPAN) { $currentPValue .= $childNode->nodeValue; } } $pNodeValues[] = $currentPValue; } $escapedCellValue = implode("\n", $pNodeValues); $cellValue = $this->escaper->unescape($escapedCellValue); return $cellValue; } /** * Returns the cell Numeric value from the given node. * * @param \DOMNode $node * @return int|float The value associated with the cell */ protected function formatFloatCellValue($node) { $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_VALUE); $nodeIntValue = intval($nodeValue); // The "==" is intentionally not a "===" because only the value matters, not the type $cellValue = ($nodeIntValue == $nodeValue) ? $nodeIntValue : floatval($nodeValue); return $cellValue; } /** * Returns the cell Boolean value from the given node. * * @param \DOMNode $node * @return bool The value associated with the cell */ protected function formatBooleanCellValue($node) { $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_BOOLEAN_VALUE); // !! is similar to boolval() $cellValue = !!$nodeValue; return $cellValue; } /** * Returns the cell Date value from the given node. * * @param \DOMNode $node * @return \DateTime|string|null The value associated with the cell or NULL if invalid date value */ protected function formatDateCellValue($node) { // The XML node looks like this: // <table:table-cell calcext:value-type="date" office:date-value="2016-05-19T16:39:00" office:value-type="date"> // <text:p>05/19/16 04:39 PM</text:p> // </table:table-cell> if ($this->shouldFormatDates) { // The date is already formatted in the "p" tag $nodeWithValueAlreadyFormatted = $node->getElementsByTagName(self::XML_NODE_P)->item(0); return $nodeWithValueAlreadyFormatted->nodeValue; } else { // otherwise, get it from the "date-value" attribute try { $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_DATE_VALUE); return new \DateTime($nodeValue); } catch (\Exception $e) { return null; } } } /** * Returns the cell Time value from the given node. * * @param \DOMNode $node * @return \DateInterval|string|null The value associated with the cell or NULL if invalid time value */ protected function formatTimeCellValue($node) { // The XML node looks like this: // <table:table-cell calcext:value-type="time" office:time-value="PT13H24M00S" office:value-type="time"> // <text:p>01:24:00 PM</text:p> // </table:table-cell> if ($this->shouldFormatDates) { // The date is already formatted in the "p" tag $nodeWithValueAlreadyFormatted = $node->getElementsByTagName(self::XML_NODE_P)->item(0); return $nodeWithValueAlreadyFormatted->nodeValue; } else { // otherwise, get it from the "time-value" attribute try { $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_TIME_VALUE); return new \DateInterval($nodeValue); } catch (\Exception $e) { return null; } } } /** * Returns the cell Currency value from the given node. * * @param \DOMNode $node * @return string The value associated with the cell (e.g. "100 USD" or "9.99 EUR") */ protected function formatCurrencyCellValue($node) { $value = $node->getAttribute(self::XML_ATTRIBUTE_VALUE); $currency = $node->getAttribute(self::XML_ATTRIBUTE_CURRENCY); return "$value $currency"; } /** * Returns the cell Percentage value from the given node. * * @param \DOMNode $node * @return int|float The value associated with the cell */ protected function formatPercentageCellValue($node) { // percentages are formatted like floats return $this->formatFloatCellValue($node); } } Reader/ODS/Helper/SettingsHelper.php 0000644 00000003030 15073227061 0013262 0 ustar 00 <?php namespace Box\Spout\Reader\ODS\Helper; use Box\Spout\Reader\Exception\XMLProcessingException; use Box\Spout\Reader\Wrapper\XMLReader; /** * Class SettingsHelper * This class provides helper functions to extract data from the "settings.xml" file. * * @package Box\Spout\Reader\ODS\Helper */ class SettingsHelper { const SETTINGS_XML_FILE_PATH = 'settings.xml'; /** Definition of XML nodes name and attribute used to parse settings data */ const XML_NODE_CONFIG_ITEM = 'config:config-item'; const XML_ATTRIBUTE_CONFIG_NAME = 'config:name'; const XML_ATTRIBUTE_VALUE_ACTIVE_TABLE = 'ActiveTable'; /** * @param string $filePath Path of the file to be read * @return string|null Name of the sheet that was defined as active or NULL if none found */ public function getActiveSheetName($filePath) { $xmlReader = new XMLReader(); if ($xmlReader->openFileInZip($filePath, self::SETTINGS_XML_FILE_PATH) === false) { return null; } $activeSheetName = null; try { while ($xmlReader->readUntilNodeFound(self::XML_NODE_CONFIG_ITEM)) { if ($xmlReader->getAttribute(self::XML_ATTRIBUTE_CONFIG_NAME) === self::XML_ATTRIBUTE_VALUE_ACTIVE_TABLE) { $activeSheetName = $xmlReader->readString(); break; } } } catch (XMLProcessingException $exception) { // do nothing } $xmlReader->close(); return $activeSheetName; } } Reader/ODS/Reader.php 0000644 00000004014 15073227061 0010310 0 ustar 00 <?php namespace Box\Spout\Reader\ODS; use Box\Spout\Common\Exception\IOException; use Box\Spout\Reader\AbstractReader; /** * Class Reader * This class provides support to read data from a ODS file * * @package Box\Spout\Reader\ODS */ class Reader extends AbstractReader { /** @var \ZipArchive */ protected $zip; /** @var SheetIterator To iterator over the ODS sheets */ protected $sheetIterator; /** * Returns the reader's current options * * @return ReaderOptions */ protected function getOptions() { if (!isset($this->options)) { $this->options = new ReaderOptions(); } return $this->options; } /** * Returns whether stream wrappers are supported * * @return bool */ protected function doesSupportStreamWrapper() { return false; } /** * Opens the file at the given file path to make it ready to be read. * * @param string $filePath Path of the file to be read * @return void * @throws \Box\Spout\Common\Exception\IOException If the file at the given path or its content cannot be read * @throws \Box\Spout\Reader\Exception\NoSheetsFoundException If there are no sheets in the file */ protected function openReader($filePath) { $this->zip = new \ZipArchive(); if ($this->zip->open($filePath) === true) { $this->sheetIterator = new SheetIterator($filePath, $this->getOptions()); } else { throw new IOException("Could not open $filePath for reading."); } } /** * Returns an iterator to iterate over sheets. * * @return SheetIterator To iterate over sheets */ protected function getConcreteSheetIterator() { return $this->sheetIterator; } /** * Closes the reader. To be used after reading the file. * * @return void */ protected function closeReader() { if ($this->zip) { $this->zip->close(); } } } Reader/ODS/Sheet.php 0000644 00000003744 15073227061 0010167 0 ustar 00 <?php namespace Box\Spout\Reader\ODS; use Box\Spout\Reader\SheetInterface; use Box\Spout\Reader\Wrapper\XMLReader; /** * Class Sheet * Represents a sheet within a ODS file * * @package Box\Spout\Reader\ODS */ class Sheet implements SheetInterface { /** @var \Box\Spout\Reader\ODS\RowIterator To iterate over sheet's rows */ protected $rowIterator; /** @var int ID of the sheet */ protected $id; /** @var int Index of the sheet, based on order in the workbook (zero-based) */ protected $index; /** @var string Name of the sheet */ protected $name; /** @var bool Whether the sheet was the active one */ protected $isActive; /** * @param XMLReader $xmlReader XML Reader, positioned on the "<table:table>" element * @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based) * @param string $sheetName Name of the sheet * @param bool $isSheetActive Whether the sheet was defined as active * @param \Box\Spout\Reader\ODS\ReaderOptions $options Reader's current options */ public function __construct($xmlReader, $sheetIndex, $sheetName, $isSheetActive, $options) { $this->rowIterator = new RowIterator($xmlReader, $options); $this->index = $sheetIndex; $this->name = $sheetName; $this->isActive = $isSheetActive; } /** * @api * @return \Box\Spout\Reader\ODS\RowIterator */ public function getRowIterator() { return $this->rowIterator; } /** * @api * @return int Index of the sheet, based on order in the workbook (zero-based) */ public function getIndex() { return $this->index; } /** * @api * @return string Name of the sheet */ public function getName() { return $this->name; } /** * @api * @return bool Whether the sheet was defined as active */ public function isActive() { return $this->isActive; } } Reader/ODS/ReaderOptions.php 0000644 00000000403 15073227061 0011662 0 ustar 00 <?php namespace Box\Spout\Reader\ODS; /** * Class ReaderOptions * This class is used to customize the reader's behavior * * @package Box\Spout\Reader\ODS */ class ReaderOptions extends \Box\Spout\Reader\Common\ReaderOptions { // No extra options } Reader/ODS/SheetIterator.php 0000644 00000012367 15073227061 0011702 0 ustar 00 <?php namespace Box\Spout\Reader\ODS; use Box\Spout\Common\Exception\IOException; use Box\Spout\Reader\Exception\XMLProcessingException; use Box\Spout\Reader\IteratorInterface; use Box\Spout\Reader\ODS\Helper\SettingsHelper; use Box\Spout\Reader\Wrapper\XMLReader; /** * Class SheetIterator * Iterate over ODS sheet. * * @package Box\Spout\Reader\ODS */ class SheetIterator implements IteratorInterface { const CONTENT_XML_FILE_PATH = 'content.xml'; /** Definition of XML nodes name and attribute used to parse sheet data */ const XML_NODE_TABLE = 'table:table'; const XML_ATTRIBUTE_TABLE_NAME = 'table:name'; /** @var string $filePath Path of the file to be read */ protected $filePath; /** @var \Box\Spout\Reader\ODS\ReaderOptions Reader's current options */ protected $options; /** @var XMLReader The XMLReader object that will help read sheet's XML data */ protected $xmlReader; /** @var \Box\Spout\Common\Escaper\ODS Used to unescape XML data */ protected $escaper; /** @var bool Whether there are still at least a sheet to be read */ protected $hasFoundSheet; /** @var int The index of the sheet being read (zero-based) */ protected $currentSheetIndex; /** @var string The name of the sheet that was defined as active */ protected $activeSheetName; /** * @param string $filePath Path of the file to be read * @param \Box\Spout\Reader\ODS\ReaderOptions $options Reader's current options * @throws \Box\Spout\Reader\Exception\NoSheetsFoundException If there are no sheets in the file */ public function __construct($filePath, $options) { $this->filePath = $filePath; $this->options = $options; $this->xmlReader = new XMLReader(); /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ $this->escaper = \Box\Spout\Common\Escaper\ODS::getInstance(); $settingsHelper = new SettingsHelper(); $this->activeSheetName = $settingsHelper->getActiveSheetName($filePath); } /** * Rewind the Iterator to the first element * @link http://php.net/manual/en/iterator.rewind.php * * @return void * @throws \Box\Spout\Common\Exception\IOException If unable to open the XML file containing sheets' data */ public function rewind() { $this->xmlReader->close(); if ($this->xmlReader->openFileInZip($this->filePath, self::CONTENT_XML_FILE_PATH) === false) { $contentXmlFilePath = $this->filePath . '#' . self::CONTENT_XML_FILE_PATH; throw new IOException("Could not open \"{$contentXmlFilePath}\"."); } try { $this->hasFoundSheet = $this->xmlReader->readUntilNodeFound(self::XML_NODE_TABLE); } catch (XMLProcessingException $exception) { throw new IOException("The content.xml file is invalid and cannot be read. [{$exception->getMessage()}]"); } $this->currentSheetIndex = 0; } /** * Checks if current position is valid * @link http://php.net/manual/en/iterator.valid.php * * @return bool */ public function valid() { return $this->hasFoundSheet; } /** * Move forward to next element * @link http://php.net/manual/en/iterator.next.php * * @return void */ public function next() { $this->hasFoundSheet = $this->xmlReader->readUntilNodeFound(self::XML_NODE_TABLE); if ($this->hasFoundSheet) { $this->currentSheetIndex++; } } /** * Return the current element * @link http://php.net/manual/en/iterator.current.php * * @return \Box\Spout\Reader\ODS\Sheet */ public function current() { $escapedSheetName = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_TABLE_NAME); $sheetName = $this->escaper->unescape($escapedSheetName); $isActiveSheet = $this->isActiveSheet($sheetName, $this->currentSheetIndex, $this->activeSheetName); return new Sheet($this->xmlReader, $this->currentSheetIndex, $sheetName, $isActiveSheet, $this->options); } /** * Returns whether the current sheet was defined as the active one * * @param string $sheetName Name of the current sheet * @param int $sheetIndex Index of the current sheet * @param string|null Name of the sheet that was defined as active or NULL if none defined * @return bool Whether the current sheet was defined as the active one */ private function isActiveSheet($sheetName, $sheetIndex, $activeSheetName) { // The given sheet is active if its name matches the defined active sheet's name // or if no information about the active sheet was found, it defaults to the first sheet. return ( ($activeSheetName === null && $sheetIndex === 0) || ($activeSheetName === $sheetName) ); } /** * Return the key of the current element * @link http://php.net/manual/en/iterator.key.php * * @return int */ public function key() { return $this->currentSheetIndex + 1; } /** * Cleans up what was created to iterate over the object. * * @return void */ public function end() { $this->xmlReader->close(); } } Reader/ReaderFactory.php 0000644 00000002447 15073227061 0011223 0 ustar 00 <?php namespace Box\Spout\Reader; use Box\Spout\Common\Exception\UnsupportedTypeException; use Box\Spout\Common\Helper\GlobalFunctionsHelper; use Box\Spout\Common\Type; /** * Class ReaderFactory * This factory is used to create readers, based on the type of the file to be read. * It supports CSV and XLSX formats. * * @package Box\Spout\Reader */ class ReaderFactory { /** * This creates an instance of the appropriate reader, given the type of the file to be read * * @api * @param string $readerType Type of the reader to instantiate * @return ReaderInterface * @throws \Box\Spout\Common\Exception\UnsupportedTypeException */ public static function create($readerType) { $reader = null; switch ($readerType) { case Type::CSV: $reader = new CSV\Reader(); break; case Type::XLSX: $reader = new XLSX\Reader(); break; case Type::ODS: $reader = new ODS\Reader(); break; default: throw new UnsupportedTypeException('No readers supporting the given type: ' . $readerType); } $reader->setGlobalFunctionsHelper(new GlobalFunctionsHelper()); return $reader; } } Writer/XLSX/Helper/FileSystemHelper.php 0000644 00000032653 15073227061 0014006 0 ustar 00 <?php namespace Box\Spout\Writer\XLSX\Helper; use Box\Spout\Writer\Common\Helper\ZipHelper; use Box\Spout\Writer\XLSX\Internal\Worksheet; /** * Class FileSystemHelper * This class provides helper functions to help with the file system operations * like files/folders creation & deletion for XLSX files * * @package Box\Spout\Writer\XLSX\Helper */ class FileSystemHelper extends \Box\Spout\Common\Helper\FileSystemHelper { const APP_NAME = 'Spout'; const RELS_FOLDER_NAME = '_rels'; const DOC_PROPS_FOLDER_NAME = 'docProps'; const XL_FOLDER_NAME = 'xl'; const WORKSHEETS_FOLDER_NAME = 'worksheets'; const RELS_FILE_NAME = '.rels'; const APP_XML_FILE_NAME = 'app.xml'; const CORE_XML_FILE_NAME = 'core.xml'; const CONTENT_TYPES_XML_FILE_NAME = '[Content_Types].xml'; const WORKBOOK_XML_FILE_NAME = 'workbook.xml'; const WORKBOOK_RELS_XML_FILE_NAME = 'workbook.xml.rels'; const STYLES_XML_FILE_NAME = 'styles.xml'; /** @var string Path to the root folder inside the temp folder where the files to create the XLSX will be stored */ protected $rootFolder; /** @var string Path to the "_rels" folder inside the root folder */ protected $relsFolder; /** @var string Path to the "docProps" folder inside the root folder */ protected $docPropsFolder; /** @var string Path to the "xl" folder inside the root folder */ protected $xlFolder; /** @var string Path to the "_rels" folder inside the "xl" folder */ protected $xlRelsFolder; /** @var string Path to the "worksheets" folder inside the "xl" folder */ protected $xlWorksheetsFolder; /** * @return string */ public function getRootFolder() { return $this->rootFolder; } /** * @return string */ public function getXlFolder() { return $this->xlFolder; } /** * @return string */ public function getXlWorksheetsFolder() { return $this->xlWorksheetsFolder; } /** * Creates all the folders needed to create a XLSX file, as well as the files that won't change. * * @return void * @throws \Box\Spout\Common\Exception\IOException If unable to create at least one of the base folders */ public function createBaseFilesAndFolders() { $this ->createRootFolder() ->createRelsFolderAndFile() ->createDocPropsFolderAndFiles() ->createXlFolderAndSubFolders(); } /** * Creates the folder that will be used as root * * @return FileSystemHelper * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder */ protected function createRootFolder() { $this->rootFolder = $this->createFolder($this->baseFolderRealPath, uniqid('xlsx', true)); return $this; } /** * Creates the "_rels" folder under the root folder as well as the ".rels" file in it * * @return FileSystemHelper * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder or the ".rels" file */ protected function createRelsFolderAndFile() { $this->relsFolder = $this->createFolder($this->rootFolder, self::RELS_FOLDER_NAME); $this->createRelsFile(); return $this; } /** * Creates the ".rels" file under the "_rels" folder (under root) * * @return FileSystemHelper * @throws \Box\Spout\Common\Exception\IOException If unable to create the file */ protected function createRelsFile() { $relsFileContents = <<<EOD <?xml version="1.0" encoding="UTF-8"?> <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"> <Relationship Id="rIdWorkbook" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/> <Relationship Id="rIdCore" Type="http://schemas.openxmlformats.org/officedocument/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/> <Relationship Id="rIdApp" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/> </Relationships> EOD; $this->createFileWithContents($this->relsFolder, self::RELS_FILE_NAME, $relsFileContents); return $this; } /** * Creates the "docProps" folder under the root folder as well as the "app.xml" and "core.xml" files in it * * @return FileSystemHelper * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder or one of the files */ protected function createDocPropsFolderAndFiles() { $this->docPropsFolder = $this->createFolder($this->rootFolder, self::DOC_PROPS_FOLDER_NAME); $this->createAppXmlFile(); $this->createCoreXmlFile(); return $this; } /** * Creates the "app.xml" file under the "docProps" folder * * @return FileSystemHelper * @throws \Box\Spout\Common\Exception\IOException If unable to create the file */ protected function createAppXmlFile() { $appName = self::APP_NAME; $appXmlFileContents = <<<EOD <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"> <Application>$appName</Application> <TotalTime>0</TotalTime> </Properties> EOD; $this->createFileWithContents($this->docPropsFolder, self::APP_XML_FILE_NAME, $appXmlFileContents); return $this; } /** * Creates the "core.xml" file under the "docProps" folder * * @return FileSystemHelper * @throws \Box\Spout\Common\Exception\IOException If unable to create the file */ protected function createCoreXmlFile() { $createdDate = (new \DateTime())->format(\DateTime::W3C); $coreXmlFileContents = <<<EOD <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <dcterms:created xsi:type="dcterms:W3CDTF">$createdDate</dcterms:created> <dcterms:modified xsi:type="dcterms:W3CDTF">$createdDate</dcterms:modified> <cp:revision>0</cp:revision> </cp:coreProperties> EOD; $this->createFileWithContents($this->docPropsFolder, self::CORE_XML_FILE_NAME, $coreXmlFileContents); return $this; } /** * Creates the "xl" folder under the root folder as well as its subfolders * * @return FileSystemHelper * @throws \Box\Spout\Common\Exception\IOException If unable to create at least one of the folders */ protected function createXlFolderAndSubFolders() { $this->xlFolder = $this->createFolder($this->rootFolder, self::XL_FOLDER_NAME); $this->createXlRelsFolder(); $this->createXlWorksheetsFolder(); return $this; } /** * Creates the "_rels" folder under the "xl" folder * * @return FileSystemHelper * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder */ protected function createXlRelsFolder() { $this->xlRelsFolder = $this->createFolder($this->xlFolder, self::RELS_FOLDER_NAME); return $this; } /** * Creates the "worksheets" folder under the "xl" folder * * @return FileSystemHelper * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder */ protected function createXlWorksheetsFolder() { $this->xlWorksheetsFolder = $this->createFolder($this->xlFolder, self::WORKSHEETS_FOLDER_NAME); return $this; } /** * Creates the "[Content_Types].xml" file under the root folder * * @param Worksheet[] $worksheets * @return FileSystemHelper */ public function createContentTypesFile($worksheets) { $contentTypesXmlFileContents = <<<EOD <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"> <Default ContentType="application/xml" Extension="xml"/> <Default ContentType="application/vnd.openxmlformats-package.relationships+xml" Extension="rels"/> <Override ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml" PartName="/xl/workbook.xml"/> EOD; /** @var Worksheet $worksheet */ foreach ($worksheets as $worksheet) { $contentTypesXmlFileContents .= '<Override ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml" PartName="/xl/worksheets/sheet' . $worksheet->getId() . '.xml"/>'; } $contentTypesXmlFileContents .= <<<EOD <Override ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml" PartName="/xl/styles.xml"/> <Override ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml" PartName="/xl/sharedStrings.xml"/> <Override ContentType="application/vnd.openxmlformats-package.core-properties+xml" PartName="/docProps/core.xml"/> <Override ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml" PartName="/docProps/app.xml"/> </Types> EOD; $this->createFileWithContents($this->rootFolder, self::CONTENT_TYPES_XML_FILE_NAME, $contentTypesXmlFileContents); return $this; } /** * Creates the "workbook.xml" file under the "xl" folder * * @param Worksheet[] $worksheets * @return FileSystemHelper */ public function createWorkbookFile($worksheets) { $workbookXmlFileContents = <<<EOD <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"> <sheets> EOD; /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ $escaper = \Box\Spout\Common\Escaper\XLSX::getInstance(); /** @var Worksheet $worksheet */ foreach ($worksheets as $worksheet) { $worksheetName = $worksheet->getExternalSheet()->getName(); $worksheetId = $worksheet->getId(); $workbookXmlFileContents .= '<sheet name="' . $escaper->escape($worksheetName) . '" sheetId="' . $worksheetId . '" r:id="rIdSheet' . $worksheetId . '"/>'; } $workbookXmlFileContents .= <<<EOD </sheets> </workbook> EOD; $this->createFileWithContents($this->xlFolder, self::WORKBOOK_XML_FILE_NAME, $workbookXmlFileContents); return $this; } /** * Creates the "workbook.xml.res" file under the "xl/_res" folder * * @param Worksheet[] $worksheets * @return FileSystemHelper */ public function createWorkbookRelsFile($worksheets) { $workbookRelsXmlFileContents = <<<EOD <?xml version="1.0" encoding="UTF-8"?> <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"> <Relationship Id="rIdStyles" Target="styles.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles"/> <Relationship Id="rIdSharedStrings" Target="sharedStrings.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings"/> EOD; /** @var Worksheet $worksheet */ foreach ($worksheets as $worksheet) { $worksheetId = $worksheet->getId(); $workbookRelsXmlFileContents .= '<Relationship Id="rIdSheet' . $worksheetId . '" Target="worksheets/sheet' . $worksheetId . '.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"/>'; } $workbookRelsXmlFileContents .= '</Relationships>'; $this->createFileWithContents($this->xlRelsFolder, self::WORKBOOK_RELS_XML_FILE_NAME, $workbookRelsXmlFileContents); return $this; } /** * Creates the "styles.xml" file under the "xl" folder * * @param StyleHelper $styleHelper * @return FileSystemHelper */ public function createStylesFile($styleHelper) { $stylesXmlFileContents = $styleHelper->getStylesXMLFileContent(); $this->createFileWithContents($this->xlFolder, self::STYLES_XML_FILE_NAME, $stylesXmlFileContents); return $this; } /** * Zips the root folder and streams the contents of the zip into the given stream * * @param resource $streamPointer Pointer to the stream to copy the zip * @return void */ public function zipRootFolderAndCopyToStream($streamPointer) { $zipHelper = new ZipHelper($this->rootFolder); // In order to have the file's mime type detected properly, files need to be added // to the zip file in a particular order. // "[Content_Types].xml" then at least 2 files located in "xl" folder should be zipped first. $zipHelper->addFileToArchive($this->rootFolder, self::CONTENT_TYPES_XML_FILE_NAME); $zipHelper->addFileToArchive($this->rootFolder, self::XL_FOLDER_NAME . '/' . self::WORKBOOK_XML_FILE_NAME); $zipHelper->addFileToArchive($this->rootFolder, self::XL_FOLDER_NAME . '/' . self::STYLES_XML_FILE_NAME); $zipHelper->addFolderToArchive($this->rootFolder, ZipHelper::EXISTING_FILES_SKIP); $zipHelper->closeArchiveAndCopyToStream($streamPointer); // once the zip is copied, remove it $this->deleteFile($zipHelper->getZipFilePath()); } } Writer/XLSX/Helper/StyleHelper.php 0000644 00000024704 15073227061 0013020 0 ustar 00 <?php namespace Box\Spout\Writer\XLSX\Helper; use Box\Spout\Writer\Common\Helper\AbstractStyleHelper; use Box\Spout\Writer\Style\Color; use Box\Spout\Writer\Style\Style; /** * Class StyleHelper * This class provides helper functions to manage styles * * @package Box\Spout\Writer\XLSX\Helper */ class StyleHelper extends AbstractStyleHelper { /** * @var array */ protected $registeredFills = []; /** * @var array [STYLE_ID] => [FILL_ID] maps a style to a fill declaration */ protected $styleIdToFillMappingTable = []; /** * Excel preserves two default fills with index 0 and 1 * Since Excel is the dominant vendor - we play along here * * @var int The fill index counter for custom fills. */ protected $fillIndex = 2; /** * @var array */ protected $registeredBorders = []; /** * @var array [STYLE_ID] => [BORDER_ID] maps a style to a border declaration */ protected $styleIdToBorderMappingTable = []; /** * XLSX specific operations on the registered styles * * @param \Box\Spout\Writer\Style\Style $style * @return \Box\Spout\Writer\Style\Style */ public function registerStyle($style) { $registeredStyle = parent::registerStyle($style); $this->registerFill($registeredStyle); $this->registerBorder($registeredStyle); return $registeredStyle; } /** * Register a fill definition * * @param \Box\Spout\Writer\Style\Style $style */ protected function registerFill($style) { $styleId = $style->getId(); // Currently - only solid backgrounds are supported // so $backgroundColor is a scalar value (RGB Color) $backgroundColor = $style->getBackgroundColor(); if ($backgroundColor) { $isBackgroundColorRegistered = isset($this->registeredFills[$backgroundColor]); // We need to track the already registered background definitions if ($isBackgroundColorRegistered) { $registeredStyleId = $this->registeredFills[$backgroundColor]; $registeredFillId = $this->styleIdToFillMappingTable[$registeredStyleId]; $this->styleIdToFillMappingTable[$styleId] = $registeredFillId; } else { $this->registeredFills[$backgroundColor] = $styleId; $this->styleIdToFillMappingTable[$styleId] = $this->fillIndex++; } } else { // The fillId maps a style to a fill declaration // When there is no background color definition - we default to 0 $this->styleIdToFillMappingTable[$styleId] = 0; } } /** * Register a border definition * * @param \Box\Spout\Writer\Style\Style $style */ protected function registerBorder($style) { $styleId = $style->getId(); if ($style->shouldApplyBorder()) { $border = $style->getBorder(); $serializedBorder = serialize($border); $isBorderAlreadyRegistered = isset($this->registeredBorders[$serializedBorder]); if ($isBorderAlreadyRegistered) { $registeredStyleId = $this->registeredBorders[$serializedBorder]; $registeredBorderId = $this->styleIdToBorderMappingTable[$registeredStyleId]; $this->styleIdToBorderMappingTable[$styleId] = $registeredBorderId; } else { $this->registeredBorders[$serializedBorder] = $styleId; $this->styleIdToBorderMappingTable[$styleId] = count($this->registeredBorders); } } else { // If no border should be applied - the mapping is the default border: 0 $this->styleIdToBorderMappingTable[$styleId] = 0; } } /** * For empty cells, we can specify a style or not. If no style are specified, * then the software default will be applied. But sometimes, it may be useful * to override this default style, for instance if the cell should have a * background color different than the default one or some borders * (fonts property don't really matter here). * * @param int $styleId * @return bool Whether the cell should define a custom style */ public function shouldApplyStyleOnEmptyCell($styleId) { $hasStyleCustomFill = (isset($this->styleIdToFillMappingTable[$styleId]) && $this->styleIdToFillMappingTable[$styleId] !== 0); $hasStyleCustomBorders = (isset($this->styleIdToBorderMappingTable[$styleId]) && $this->styleIdToBorderMappingTable[$styleId] !== 0); return ($hasStyleCustomFill || $hasStyleCustomBorders); } /** * Returns the content of the "styles.xml" file, given a list of styles. * * @return string */ public function getStylesXMLFileContent() { $content = <<<EOD <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"> EOD; $content .= $this->getFontsSectionContent(); $content .= $this->getFillsSectionContent(); $content .= $this->getBordersSectionContent(); $content .= $this->getCellStyleXfsSectionContent(); $content .= $this->getCellXfsSectionContent(); $content .= $this->getCellStylesSectionContent(); $content .= <<<EOD </styleSheet> EOD; return $content; } /** * Returns the content of the "<fonts>" section. * * @return string */ protected function getFontsSectionContent() { $content = '<fonts count="' . count($this->styleIdToStyleMappingTable) . '">'; /** @var \Box\Spout\Writer\Style\Style $style */ foreach ($this->getRegisteredStyles() as $style) { $content .= '<font>'; $content .= '<sz val="' . $style->getFontSize() . '"/>'; $content .= '<color rgb="' . Color::toARGB($style->getFontColor()) . '"/>'; $content .= '<name val="' . $style->getFontName() . '"/>'; if ($style->isFontBold()) { $content .= '<b/>'; } if ($style->isFontItalic()) { $content .= '<i/>'; } if ($style->isFontUnderline()) { $content .= '<u/>'; } if ($style->isFontStrikethrough()) { $content .= '<strike/>'; } $content .= '</font>'; } $content .= '</fonts>'; return $content; } /** * Returns the content of the "<fills>" section. * * @return string */ protected function getFillsSectionContent() { // Excel reserves two default fills $fillsCount = count($this->registeredFills) + 2; $content = sprintf('<fills count="%d">', $fillsCount); $content .= '<fill><patternFill patternType="none"/></fill>'; $content .= '<fill><patternFill patternType="gray125"/></fill>'; // The other fills are actually registered by setting a background color foreach ($this->registeredFills as $styleId) { /** @var Style $style */ $style = $this->styleIdToStyleMappingTable[$styleId]; $backgroundColor = $style->getBackgroundColor(); $content .= sprintf( '<fill><patternFill patternType="solid"><fgColor rgb="%s"/></patternFill></fill>', $backgroundColor ); } $content .= '</fills>'; return $content; } /** * Returns the content of the "<borders>" section. * * @return string */ protected function getBordersSectionContent() { // There is one default border with index 0 $borderCount = count($this->registeredBorders) + 1; $content = '<borders count="' . $borderCount . '">'; // Default border starting at index 0 $content .= '<border><left/><right/><top/><bottom/></border>'; foreach ($this->registeredBorders as $styleId) { /** @var \Box\Spout\Writer\Style\Style $style */ $style = $this->styleIdToStyleMappingTable[$styleId]; $border = $style->getBorder(); $content .= '<border>'; // @link https://github.com/box/spout/issues/271 $sortOrder = ['left', 'right', 'top', 'bottom']; foreach ($sortOrder as $partName) { if ($border->hasPart($partName)) { /** @var $part \Box\Spout\Writer\Style\BorderPart */ $part = $border->getPart($partName); $content .= BorderHelper::serializeBorderPart($part); } } $content .= '</border>'; } $content .= '</borders>'; return $content; } /** * Returns the content of the "<cellStyleXfs>" section. * * @return string */ protected function getCellStyleXfsSectionContent() { return <<<EOD <cellStyleXfs count="1"> <xf borderId="0" fillId="0" fontId="0" numFmtId="0"/> </cellStyleXfs> EOD; } /** * Returns the content of the "<cellXfs>" section. * * @return string */ protected function getCellXfsSectionContent() { $registeredStyles = $this->getRegisteredStyles(); $content = '<cellXfs count="' . count($registeredStyles) . '">'; foreach ($registeredStyles as $style) { $styleId = $style->getId(); $fillId = $this->styleIdToFillMappingTable[$styleId]; $borderId = $this->styleIdToBorderMappingTable[$styleId]; $content .= '<xf numFmtId="0" fontId="' . $styleId . '" fillId="' . $fillId . '" borderId="' . $borderId . '" xfId="0"'; if ($style->shouldApplyFont()) { $content .= ' applyFont="1"'; } $content .= sprintf(' applyBorder="%d"', $style->shouldApplyBorder() ? 1 : 0); if ($style->shouldWrapText()) { $content .= ' applyAlignment="1">'; $content .= '<alignment wrapText="1"/>'; $content .= '</xf>'; } else { $content .= '/>'; } } $content .= '</cellXfs>'; return $content; } /** * Returns the content of the "<cellStyles>" section. * * @return string */ protected function getCellStylesSectionContent() { return <<<EOD <cellStyles count="1"> <cellStyle builtinId="0" name="Normal" xfId="0"/> </cellStyles> EOD; } } Writer/XLSX/Helper/BorderHelper.php 0000644 00000003626 15073227061 0013135 0 ustar 00 <?php namespace Box\Spout\Writer\XLSX\Helper; use Box\Spout\Writer\Style\Border; use Box\Spout\Writer\Style\BorderPart; class BorderHelper { public static $xlsxStyleMap = [ Border::STYLE_SOLID => [ Border::WIDTH_THIN => 'thin', Border::WIDTH_MEDIUM => 'medium', Border::WIDTH_THICK => 'thick' ], Border::STYLE_DOTTED => [ Border::WIDTH_THIN => 'dotted', Border::WIDTH_MEDIUM => 'dotted', Border::WIDTH_THICK => 'dotted', ], Border::STYLE_DASHED => [ Border::WIDTH_THIN => 'dashed', Border::WIDTH_MEDIUM => 'mediumDashed', Border::WIDTH_THICK => 'mediumDashed', ], Border::STYLE_DOUBLE => [ Border::WIDTH_THIN => 'double', Border::WIDTH_MEDIUM => 'double', Border::WIDTH_THICK => 'double', ], Border::STYLE_NONE => [ Border::WIDTH_THIN => 'none', Border::WIDTH_MEDIUM => 'none', Border::WIDTH_THICK => 'none', ], ]; /** * @param BorderPart $borderPart * @return string */ public static function serializeBorderPart(BorderPart $borderPart) { $borderStyle = self::getBorderStyle($borderPart); $colorEl = $borderPart->getColor() ? sprintf('<color rgb="%s"/>', $borderPart->getColor()) : ''; $partEl = sprintf( '<%s style="%s">%s</%s>', $borderPart->getName(), $borderStyle, $colorEl, $borderPart->getName() ); return $partEl . PHP_EOL; } /** * Get the style definition from the style map * * @param BorderPart $borderPart * @return string */ protected static function getBorderStyle(BorderPart $borderPart) { return self::$xlsxStyleMap[$borderPart->getStyle()][$borderPart->getWidth()]; } } Writer/XLSX/Helper/SharedStringsHelper.php 0000644 00000007561 15073227061 0014502 0 ustar 00 <?php namespace Box\Spout\Writer\XLSX\Helper; use Box\Spout\Common\Exception\IOException; /** * Class SharedStringsHelper * This class provides helper functions to write shared strings * * @package Box\Spout\Writer\XLSX\Helper */ class SharedStringsHelper { const SHARED_STRINGS_FILE_NAME = 'sharedStrings.xml'; const SHARED_STRINGS_XML_FILE_FIRST_PART_HEADER = <<<EOD <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" EOD; /** * This number must be really big so that the no generated file will have more strings than that. * If the strings number goes above, characters will be overwritten in an unwanted way and will corrupt the file. */ const DEFAULT_STRINGS_COUNT_PART = 'count="9999999999999" uniqueCount="9999999999999"'; /** @var resource Pointer to the sharedStrings.xml file */ protected $sharedStringsFilePointer; /** @var int Number of shared strings already written */ protected $numSharedStrings = 0; /** @var \Box\Spout\Common\Escaper\XLSX Strings escaper */ protected $stringsEscaper; /** * @param string $xlFolder Path to the "xl" folder */ public function __construct($xlFolder) { $sharedStringsFilePath = $xlFolder . '/' . self::SHARED_STRINGS_FILE_NAME; $this->sharedStringsFilePointer = fopen($sharedStringsFilePath, 'w'); $this->throwIfSharedStringsFilePointerIsNotAvailable(); // the headers is split into different parts so that we can fseek and put in the correct count and uniqueCount later $header = self::SHARED_STRINGS_XML_FILE_FIRST_PART_HEADER . ' ' . self::DEFAULT_STRINGS_COUNT_PART . '>'; fwrite($this->sharedStringsFilePointer, $header); /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ $this->stringsEscaper = \Box\Spout\Common\Escaper\XLSX::getInstance(); } /** * 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 throwIfSharedStringsFilePointerIsNotAvailable() { if (!$this->sharedStringsFilePointer) { throw new IOException('Unable to open shared strings file for writing.'); } } /** * Writes the given string into the sharedStrings.xml file. * Starting and ending whitespaces are preserved. * * @param string $string * @return int ID of the written shared string */ public function writeString($string) { fwrite($this->sharedStringsFilePointer, '<si><t xml:space="preserve">' . $this->stringsEscaper->escape($string) . '</t></si>'); $this->numSharedStrings++; // Shared string ID is zero-based return ($this->numSharedStrings - 1); } /** * Finishes writing the data in the sharedStrings.xml file and closes the file. * * @return void */ public function close() { if (!is_resource($this->sharedStringsFilePointer)) { return; } fwrite($this->sharedStringsFilePointer, '</sst>'); // Replace the default strings count with the actual number of shared strings in the file header $firstPartHeaderLength = strlen(self::SHARED_STRINGS_XML_FILE_FIRST_PART_HEADER); $defaultStringsCountPartLength = strlen(self::DEFAULT_STRINGS_COUNT_PART); // Adding 1 to take into account the space between the last xml attribute and "count" fseek($this->sharedStringsFilePointer, $firstPartHeaderLength + 1); fwrite($this->sharedStringsFilePointer, sprintf("%-{$defaultStringsCountPartLength}s", 'count="' . $this->numSharedStrings . '" uniqueCount="' . $this->numSharedStrings . '"')); fclose($this->sharedStringsFilePointer); } } Writer/XLSX/Internal/Worksheet.php 0000644 00000024403 15073227061 0013064 0 ustar 00 <?php namespace Box\Spout\Writer\XLSX\Internal; use Box\Spout\Common\Exception\InvalidArgumentException; use Box\Spout\Common\Exception\IOException; use Box\Spout\Common\Helper\StringHelper; use Box\Spout\Writer\Common\Helper\CellHelper; use Box\Spout\Writer\Common\Internal\WorksheetInterface; /** * Class Worksheet * Represents a worksheet within a XLSX file. The difference with the Sheet object is * that this class provides an interface to write data * * @package Box\Spout\Writer\XLSX\Internal */ class Worksheet implements WorksheetInterface { /** * Maximum number of characters a cell can contain * @see https://support.office.com/en-us/article/Excel-specifications-and-limits-16c69c74-3d6a-4aaf-ba35-e6eb276e8eaa [Excel 2007] * @see https://support.office.com/en-us/article/Excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3 [Excel 2010] * @see https://support.office.com/en-us/article/Excel-specifications-and-limits-ca36e2dc-1f09-4620-b726-67c00b05040f [Excel 2013/2016] */ const MAX_CHARACTERS_PER_CELL = 32767; const SHEET_XML_FILE_HEADER = <<<EOD <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"> 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 \Box\Spout\Common\Escaper\XLSX Strings escaper */ protected $stringsEscaper; /** @var \Box\Spout\Common\Helper\StringHelper String helper */ protected $stringHelper; /** @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; /** * @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 bool $shouldUseInlineStrings Whether inline or shared strings should be used * @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing */ public function __construct($externalSheet, $worksheetFilesFolder, $sharedStringsHelper, $styleHelper, $shouldUseInlineStrings) { $this->externalSheet = $externalSheet; $this->sharedStringsHelper = $sharedStringsHelper; $this->styleHelper = $styleHelper; $this->shouldUseInlineStrings = $shouldUseInlineStrings; /** @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 * * @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(); fwrite($this->sheetFilePointer, self::SHEET_XML_FILE_HEADER); fwrite($this->sheetFilePointer, '<sheetData>'); } /** * 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 */ protected 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 */ protected function addNonEmptyRow($dataRow, $style) { $cellNumber = 0; $rowIndex = $this->lastWrittenRowIndex + 1; $numCells = count($dataRow); $rowXML = '<row r="' . $rowIndex . '" spans="1:' . $numCells . '">'; foreach($dataRow as $cellValue) { $rowXML .= $this->getCellXML($rowIndex, $cellNumber, $cellValue, $style->getId()); $cellNumber++; } $rowXML .= '</row>'; $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 int $styleId * @return string * @throws InvalidArgumentException If the given value cannot be processed */ protected function getCellXML($rowIndex, $cellNumber, $cellValue, $styleId) { $columnIndex = CellHelper::getCellIndexFromColumnIndex($cellNumber); $cellXML = '<c r="' . $columnIndex . $rowIndex . '"'; $cellXML .= ' s="' . $styleId . '"'; if (CellHelper::isNonEmptyString($cellValue)) { $cellXML .= $this->getCellXMLFragmentForNonEmptyString($cellValue); } else if (CellHelper::isBoolean($cellValue)) { $cellXML .= ' t="b"><v>' . intval($cellValue) . '</v></c>'; } else if (CellHelper::isNumeric($cellValue)) { $cellXML .= '><v>' . $cellValue . '</v></c>'; } else if (empty($cellValue)) { if ($this->styleHelper->shouldApplyStyleOnEmptyCell($styleId)) { $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)); } 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 */ protected 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"><is><t>' . $this->stringsEscaper->escape($cellValue) . '</t></is></c>'; } else { $sharedStringId = $this->sharedStringsHelper->writeString($cellValue); $cellXMLFragment = ' t="s"><v>' . $sharedStringId . '</v></c>'; } return $cellXMLFragment; } /** * Closes the worksheet * * @return void */ public function close() { if (!is_resource($this->sheetFilePointer)) { return; } fwrite($this->sheetFilePointer, '</sheetData>'); fwrite($this->sheetFilePointer, '</worksheet>'); fclose($this->sheetFilePointer); } } Writer/XLSX/Internal/Workbook.php 0000644 00000011052 15073227061 0012702 0 ustar 00 <?php namespace Box\Spout\Writer\XLSX\Internal; use Box\Spout\Writer\Common\Internal\AbstractWorkbook; use Box\Spout\Writer\XLSX\Helper\FileSystemHelper; use Box\Spout\Writer\XLSX\Helper\SharedStringsHelper; use Box\Spout\Writer\XLSX\Helper\StyleHelper; use Box\Spout\Writer\Common\Sheet; /** * Class Workbook * Represents a workbook within a XLSX file. * It provides the functions to work with worksheets. * * @package Box\Spout\Writer\XLSX\Internal */ class Workbook extends AbstractWorkbook { /** * Maximum number of rows a XLSX sheet can contain * @see http://office.microsoft.com/en-us/excel-help/excel-specifications-and-limits-HP010073849.aspx */ protected static $maxRowsPerWorksheet = 1048576; /** @var bool Whether inline or shared strings should be used */ protected $shouldUseInlineStrings; /** @var \Box\Spout\Writer\XLSX\Helper\FileSystemHelper Helper to perform file system operations */ protected $fileSystemHelper; /** @var \Box\Spout\Writer\XLSX\Helper\SharedStringsHelper Helper to write shared strings */ protected $sharedStringsHelper; /** @var \Box\Spout\Writer\XLSX\Helper\StyleHelper Helper to apply styles */ protected $styleHelper; /** * @param string $tempFolder * @param bool $shouldUseInlineStrings * @param bool $shouldCreateNewSheetsAutomatically * @param \Box\Spout\Writer\Style\Style $defaultRowStyle * @throws \Box\Spout\Common\Exception\IOException If unable to create at least one of the base folders */ public function __construct($tempFolder, $shouldUseInlineStrings, $shouldCreateNewSheetsAutomatically, $defaultRowStyle) { parent::__construct($shouldCreateNewSheetsAutomatically, $defaultRowStyle); $this->shouldUseInlineStrings = $shouldUseInlineStrings; $this->fileSystemHelper = new FileSystemHelper($tempFolder); $this->fileSystemHelper->createBaseFilesAndFolders(); $this->styleHelper = new StyleHelper($defaultRowStyle); // This helper will be shared by all sheets $xlFolder = $this->fileSystemHelper->getXlFolder(); $this->sharedStringsHelper = new SharedStringsHelper($xlFolder); } /** * @return \Box\Spout\Writer\XLSX\Helper\StyleHelper Helper to apply styles to XLSX files */ protected function getStyleHelper() { return $this->styleHelper; } /** * @return int Maximum number of rows/columns a sheet can contain */ protected function getMaxRowsPerWorksheet() { return self::$maxRowsPerWorksheet; } /** * Creates a new sheet in the workbook. The current sheet remains unchanged. * * @return Worksheet The created sheet * @throws \Box\Spout\Common\Exception\IOException If unable to open the sheet for writing */ public function addNewSheet() { $newSheetIndex = count($this->worksheets); $sheet = new Sheet($newSheetIndex, $this->internalId); $worksheetFilesFolder = $this->fileSystemHelper->getXlWorksheetsFolder(); $worksheet = new Worksheet($sheet, $worksheetFilesFolder, $this->sharedStringsHelper, $this->styleHelper, $this->shouldUseInlineStrings); $this->worksheets[] = $worksheet; return $worksheet; } /** * Closes the workbook and all its associated sheets. * All the necessary files are written to disk and zipped together to create the XLSX file. * All the temporary files are then deleted. * * @param resource $finalFilePointer Pointer to the XLSX that will be created * @return void */ public function close($finalFilePointer) { /** @var Worksheet[] $worksheets */ $worksheets = $this->worksheets; foreach ($worksheets as $worksheet) { $worksheet->close(); } $this->sharedStringsHelper->close(); // Finish creating all the necessary files before zipping everything together $this->fileSystemHelper ->createContentTypesFile($worksheets) ->createWorkbookFile($worksheets) ->createWorkbookRelsFile($worksheets) ->createStylesFile($this->styleHelper) ->zipRootFolderAndCopyToStream($finalFilePointer); $this->cleanupTempFolder(); } /** * Deletes the root folder created in the temp folder and all its contents. * * @return void */ protected function cleanupTempFolder() { $xlsxRootFolder = $this->fileSystemHelper->getRootFolder(); $this->fileSystemHelper->deleteFolderRecursively($xlsxRootFolder); } } Writer/XLSX/Writer.php 0000644 00000010567 15073227061 0010617 0 ustar 00 <?php namespace Box\Spout\Writer\XLSX; use Box\Spout\Writer\AbstractMultiSheetsWriter; use Box\Spout\Writer\Style\StyleBuilder; use Box\Spout\Writer\XLSX\Internal\Workbook; /** * Class Writer * This class provides base support to write data to XLSX files * * @package Box\Spout\Writer\XLSX */ class Writer extends AbstractMultiSheetsWriter { /** Default style font values */ const DEFAULT_FONT_SIZE = 12; const DEFAULT_FONT_NAME = 'Calibri'; /** @var string Content-Type value for the header */ protected static $headerContentType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; /** @var string Temporary folder where the files to create the XLSX will be stored */ protected $tempFolder; /** @var bool Whether inline or shared strings should be used - inline string is more memory efficient */ protected $shouldUseInlineStrings = true; /** @var Internal\Workbook The workbook for the XLSX file */ protected $book; /** * Sets a custom temporary folder for creating intermediate files/folders. * This must be set before opening the writer. * * @api * @param string $tempFolder Temporary folder where the files to create the XLSX will be stored * @return Writer * @throws \Box\Spout\Writer\Exception\WriterAlreadyOpenedException If the writer was already opened */ public function setTempFolder($tempFolder) { $this->throwIfWriterAlreadyOpened('Writer must be configured before opening it.'); $this->tempFolder = $tempFolder; return $this; } /** * Use inline string to be more memory efficient. If set to false, it will use shared strings. * This must be set before opening the writer. * * @api * @param bool $shouldUseInlineStrings Whether inline or shared strings should be used * @return Writer * @throws \Box\Spout\Writer\Exception\WriterAlreadyOpenedException If the writer was already opened */ public function setShouldUseInlineStrings($shouldUseInlineStrings) { $this->throwIfWriterAlreadyOpened('Writer must be configured before opening it.'); $this->shouldUseInlineStrings = $shouldUseInlineStrings; return $this; } /** * Configures the write and sets the current sheet pointer to a new sheet. * * @return void * @throws \Box\Spout\Common\Exception\IOException If unable to open the file for writing */ protected function openWriter() { if (!$this->book) { $tempFolder = ($this->tempFolder) ? : sys_get_temp_dir(); $this->book = new Workbook($tempFolder, $this->shouldUseInlineStrings, $this->shouldCreateNewSheetsAutomatically, $this->defaultRowStyle); $this->book->addNewSheetAndMakeItCurrent(); } } /** * @return Internal\Workbook The workbook representing the file to be written */ protected function getWorkbook() { return $this->book; } /** * Adds data to the currently opened writer. * If shouldCreateNewSheetsAutomatically option is set to true, it will handle pagination * with the creation of new worksheets if one worksheet has reached its maximum capicity. * * @param array $dataRow Array containing data to be written. * Example $dataRow = ['data1', 1234, null, '', 'data5']; * @param \Box\Spout\Writer\Style\Style $style Style to be applied to the row. * @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, $style) { $this->throwIfBookIsNotAvailable(); $this->book->addRowToCurrentWorksheet($dataRow, $style); } /** * Returns the default style to be applied to rows. * * @return \Box\Spout\Writer\Style\Style */ protected function getDefaultRowStyle() { return (new StyleBuilder()) ->setFontSize(self::DEFAULT_FONT_SIZE) ->setFontName(self::DEFAULT_FONT_NAME) ->build(); } /** * Closes the writer, preventing any additional writing. * * @return void */ protected function closeWriter() { if ($this->book) { $this->book->close($this->filePointer); } } } Writer/AbstractWriter.php 0000644 00000031704 15073227061 0011501 0 ustar 00 <?php namespace Box\Spout\Writer; use Box\Spout\Common\Exception\IOException; use Box\Spout\Common\Exception\InvalidArgumentException; use Box\Spout\Common\Exception\SpoutException; use Box\Spout\Common\Helper\FileSystemHelper; use Box\Spout\Writer\Exception\WriterAlreadyOpenedException; use Box\Spout\Writer\Exception\WriterNotOpenedException; use Box\Spout\Writer\Style\StyleBuilder; /** * Class AbstractWriter * * @package Box\Spout\Writer * @abstract */ abstract class AbstractWriter implements WriterInterface { /** @var string Path to the output file */ protected $outputFilePath; /** @var resource Pointer to the file/stream we will write to */ protected $filePointer; /** @var bool Indicates whether the writer has been opened or not */ protected $isWriterOpened = false; /** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */ protected $globalFunctionsHelper; /** @var Style\Style Style to be applied to the next written row(s) */ protected $rowStyle; /** @var Style\Style Default row style. Each writer can have its own default style */ protected $defaultRowStyle; /** @var string Content-Type value for the header - to be defined by child class */ protected static $headerContentType; /** * Opens the streamer and makes it ready to accept data. * * @return void * @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened */ abstract protected function openWriter(); /** * Adds data to the currently openned writer. * * @param array $dataRow Array containing data to be streamed. * Example $dataRow = ['data1', 1234, null, '', 'data5']; * @param Style\Style $style Style to be applied to the written row * @return void */ abstract protected function addRowToWriter(array $dataRow, $style); /** * Closes the streamer, preventing any additional writing. * * @return void */ abstract protected function closeWriter(); /** * */ public function __construct() { $this->defaultRowStyle = $this->getDefaultRowStyle(); $this->resetRowStyleToDefault(); } /** * Sets the default styles for all rows added with "addRow". * Overriding the default style instead of using "addRowWithStyle" improves performance by 20%. * @see https://github.com/box/spout/issues/272 * * @param Style\Style $defaultStyle * @return AbstractWriter */ public function setDefaultRowStyle($defaultStyle) { $this->defaultRowStyle = $defaultStyle; $this->resetRowStyleToDefault(); return $this; } /** * @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper * @return AbstractWriter */ public function setGlobalFunctionsHelper($globalFunctionsHelper) { $this->globalFunctionsHelper = $globalFunctionsHelper; return $this; } /** * Inits the writer and opens it to accept data. * By using this method, the data will be written to a file. * * @api * @param string $outputFilePath Path of the output file that will contain the data * @return AbstractWriter * @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened or if the given path is not writable */ public function openToFile($outputFilePath) { $this->outputFilePath = $outputFilePath; $this->filePointer = $this->globalFunctionsHelper->fopen($this->outputFilePath, 'wb+'); $this->throwIfFilePointerIsNotAvailable(); $this->openWriter(); $this->isWriterOpened = true; return $this; } /** * Inits the writer and opens it to accept data. * By using this method, the data will be outputted directly to the browser. * * @codeCoverageIgnore * * @api * @param string $outputFileName Name of the output file that will contain the data. If a path is passed in, only the file name will be kept * @return AbstractWriter * @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened */ public function openToBrowser($outputFileName) { $this->outputFilePath = $this->globalFunctionsHelper->basename($outputFileName); $this->filePointer = $this->globalFunctionsHelper->fopen('php://output', 'w'); $this->throwIfFilePointerIsNotAvailable(); // Clear any previous output (otherwise the generated file will be corrupted) // @see https://github.com/box/spout/issues/241 $this->globalFunctionsHelper->ob_end_clean(); // Set headers $this->globalFunctionsHelper->header('Content-Type: ' . static::$headerContentType); $this->globalFunctionsHelper->header('Content-Disposition: attachment; filename="' . $this->outputFilePath . '"'); /* * When forcing the download of a file over SSL,IE8 and lower browsers fail * if the Cache-Control and Pragma headers are not set. * * @see http://support.microsoft.com/KB/323308 * @see https://github.com/liuggio/ExcelBundle/issues/45 */ $this->globalFunctionsHelper->header('Cache-Control: max-age=0'); $this->globalFunctionsHelper->header('Pragma: public'); $this->openWriter(); $this->isWriterOpened = true; return $this; } /** * Checks if the pointer to the file/stream to write to is available. * Will throw an exception if not available. * * @return void * @throws \Box\Spout\Common\Exception\IOException If the pointer is not available */ protected function throwIfFilePointerIsNotAvailable() { if (!$this->filePointer) { throw new IOException('File pointer has not be opened'); } } /** * Checks if the writer has already been opened, since some actions must be done before it gets opened. * Throws an exception if already opened. * * @param string $message Error message * @return void * @throws \Box\Spout\Writer\Exception\WriterAlreadyOpenedException If the writer was already opened and must not be. */ protected function throwIfWriterAlreadyOpened($message) { if ($this->isWriterOpened) { throw new WriterAlreadyOpenedException($message); } } /** * Write given data to the output. New data will be appended to end of stream. * * @param array $dataRow Array containing data to be streamed. * If empty, no data is added (i.e. not even as a blank row) * Example: $dataRow = ['data1', 1234, null, '', 'data5', false]; * @api * @return 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 * @throws \Box\Spout\Common\Exception\SpoutException If anything else goes wrong while writing data */ public function addRow(array $dataRow) { if ($this->isWriterOpened) { // empty $dataRow should not add an empty line if (!empty($dataRow)) { try { $this->addRowToWriter($dataRow, $this->rowStyle); } catch (SpoutException $e) { // if an exception occurs while writing data, // close the writer and remove all files created so far. $this->closeAndAttemptToCleanupAllFiles(); // re-throw the exception to alert developers of the error throw $e; } } } else { throw new WriterNotOpenedException('The writer needs to be opened before adding row.'); } return $this; } /** * Write given data to the output and apply the given style. * @see addRow * * @api * @param array $dataRow Array of array containing data to be streamed. * @param Style\Style $style Style to be applied to the row. * @return AbstractWriter * @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid * @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 addRowWithStyle(array $dataRow, $style) { if (!$style instanceof Style\Style) { throw new InvalidArgumentException('The "$style" argument must be a Style instance and cannot be NULL.'); } $this->setRowStyle($style); $this->addRow($dataRow); $this->resetRowStyleToDefault(); return $this; } /** * Write given data to the output. New data will be appended to end of stream. * * @api * @param array $dataRows Array of array containing data to be streamed. * If a row is empty, it won't be added (i.e. not even as a blank row) * Example: $dataRows = [ * ['data11', 12, , '', 'data13'], * ['data21', 'data22', null, false], * ]; * @return AbstractWriter * @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid * @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 addRows(array $dataRows) { if (!empty($dataRows)) { $firstRow = reset($dataRows); if (!is_array($firstRow)) { throw new InvalidArgumentException('The input should be an array of arrays'); } foreach ($dataRows as $dataRow) { $this->addRow($dataRow); } } return $this; } /** * Write given data to the output and apply the given style. * @see addRows * * @api * @param array $dataRows Array of array containing data to be streamed. * @param Style\Style $style Style to be applied to the rows. * @return AbstractWriter * @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid * @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 addRowsWithStyle(array $dataRows, $style) { if (!$style instanceof Style\Style) { throw new InvalidArgumentException('The "$style" argument must be a Style instance and cannot be NULL.'); } $this->setRowStyle($style); $this->addRows($dataRows); $this->resetRowStyleToDefault(); return $this; } /** * Returns the default style to be applied to rows. * Can be overriden by children to have a custom style. * * @return Style\Style */ protected function getDefaultRowStyle() { return (new StyleBuilder())->build(); } /** * Sets the style to be applied to the next written rows * until it is changed or reset. * * @param Style\Style $style * @return void */ private function setRowStyle($style) { // Merge given style with the default one to inherit custom properties $this->rowStyle = $style->mergeWith($this->defaultRowStyle); } /** * Resets the style to be applied to the next written rows. * * @return void */ private function resetRowStyleToDefault() { $this->rowStyle = $this->defaultRowStyle; } /** * Closes the writer. This will close the streamer as well, preventing new data * to be written to the file. * * @api * @return void */ public function close() { if (!$this->isWriterOpened) { return; } $this->closeWriter(); if (is_resource($this->filePointer)) { $this->globalFunctionsHelper->fclose($this->filePointer); } $this->isWriterOpened = false; } /** * Closes the writer and attempts to cleanup all files that were * created during the writing process (temp files & final file). * * @return void */ private function closeAndAttemptToCleanupAllFiles() { // close the writer, which should remove all temp files $this->close(); // remove output file if it was created if ($this->globalFunctionsHelper->file_exists($this->outputFilePath)) { $outputFolderPath = dirname($this->outputFilePath); $fileSystemHelper = new FileSystemHelper($outputFolderPath); $fileSystemHelper->deleteFile($this->outputFilePath); } } } Writer/Style/Color.php 0000644 00000005045 15073227061 0010716 0 ustar 00 <?php namespace Box\Spout\Writer\Style; use Box\Spout\Writer\Exception\InvalidColorException; /** * Class Color * This class provides constants and functions to work with colors * * @package Box\Spout\Writer\Style */ class Color { /** Standard colors - based on Office Online */ const BLACK = '000000'; const WHITE = 'FFFFFF'; const RED = 'FF0000'; const DARK_RED = 'C00000'; const ORANGE = 'FFC000'; const YELLOW = 'FFFF00'; const LIGHT_GREEN = '92D040'; const GREEN = '00B050'; const LIGHT_BLUE = '00B0E0'; const BLUE = '0070C0'; const DARK_BLUE = '002060'; const PURPLE = '7030A0'; /** * Returns an RGB color from R, G and B values * * @api * @param int $red Red component, 0 - 255 * @param int $green Green component, 0 - 255 * @param int $blue Blue component, 0 - 255 * @return string RGB color */ public static function rgb($red, $green, $blue) { self::throwIfInvalidColorComponentValue($red); self::throwIfInvalidColorComponentValue($green); self::throwIfInvalidColorComponentValue($blue); return strtoupper( self::convertColorComponentToHex($red) . self::convertColorComponentToHex($green) . self::convertColorComponentToHex($blue) ); } /** * Throws an exception is the color component value is outside of bounds (0 - 255) * * @param int $colorComponent * @return void * @throws \Box\Spout\Writer\Exception\InvalidColorException */ protected static function throwIfInvalidColorComponentValue($colorComponent) { if (!is_int($colorComponent) || $colorComponent < 0 || $colorComponent > 255) { throw new InvalidColorException("The RGB components must be between 0 and 255. Received: $colorComponent"); } } /** * Converts the color component to its corresponding hexadecimal value * * @param int $colorComponent Color component, 0 - 255 * @return string Corresponding hexadecimal value, with a leading 0 if needed. E.g "0f", "2d" */ protected static function convertColorComponentToHex($colorComponent) { return str_pad(dechex($colorComponent), 2, '0', STR_PAD_LEFT); } /** * Returns the ARGB color of the given RGB color, * assuming that alpha value is always 1. * * @param string $rgbColor RGB color like "FF08B2" * @return string ARGB color */ public static function toARGB($rgbColor) { return 'FF' . $rgbColor; } } Writer/Style/Border.php 0000644 00000003205 15073227061 0011051 0 ustar 00 <?php namespace Box\Spout\Writer\Style; /** * Class Border */ class Border { const LEFT = 'left'; const RIGHT = 'right'; const TOP = 'top'; const BOTTOM = 'bottom'; const STYLE_NONE = 'none'; const STYLE_SOLID = 'solid'; const STYLE_DASHED = 'dashed'; const STYLE_DOTTED = 'dotted'; const STYLE_DOUBLE = 'double'; const WIDTH_THIN = 'thin'; const WIDTH_MEDIUM = 'medium'; const WIDTH_THICK = 'thick'; /** * @var array A list of BorderPart objects for this border. */ protected $parts = []; /** * @param array|void $borderParts */ public function __construct(array $borderParts = []) { $this->setParts($borderParts); } /** * @param $name The name of the border part * @return null|BorderPart */ public function getPart($name) { return $this->hasPart($name) ? $this->parts[$name] : null; } /** * @param $name The name of the border part * @return bool */ public function hasPart($name) { return isset($this->parts[$name]); } /** * @return array */ public function getParts() { return $this->parts; } /** * Set BorderParts * @param array $parts */ public function setParts($parts) { unset($this->parts); foreach ($parts as $part) { $this->addPart($part); } } /** * @param BorderPart $borderPart * @return self */ public function addPart(BorderPart $borderPart) { $this->parts[$borderPart->getName()] = $borderPart; return $this; } } Writer/Style/BorderPart.php 0000644 00000007510 15073227061 0011703 0 ustar 00 <?php namespace Box\Spout\Writer\Style; use Box\Spout\Writer\Exception\Border\InvalidNameException; use Box\Spout\Writer\Exception\Border\InvalidStyleException; use Box\Spout\Writer\Exception\Border\InvalidWidthException; /** * Class BorderPart */ class BorderPart { /** * @var string The style of this border part. */ protected $style; /** * @var string The name of this border part. */ protected $name; /** * @var string The color of this border part. */ protected $color; /** * @var string The width of this border part. */ protected $width; /** * @var array Allowed style constants for parts. */ protected static $allowedStyles = [ 'none', 'solid', 'dashed', 'dotted', 'double' ]; /** * @var array Allowed names constants for border parts. */ protected static $allowedNames = [ 'left', 'right', 'top', 'bottom', ]; /** * @var array Allowed width constants for border parts. */ protected static $allowedWidths = [ 'thin', 'medium', 'thick', ]; /** * @param string $name @see BorderPart::$allowedNames * @param string $color A RGB color code * @param string $width @see BorderPart::$allowedWidths * @param string $style @see BorderPart::$allowedStyles * @throws InvalidNameException * @throws InvalidStyleException * @throws InvalidWidthException */ public function __construct($name, $color = Color::BLACK, $width = Border::WIDTH_MEDIUM, $style = Border::STYLE_SOLID) { $this->setName($name); $this->setColor($color); $this->setWidth($width); $this->setStyle($style); } /** * @return string */ public function getName() { return $this->name; } /** * @param string $name The name of the border part @see BorderPart::$allowedNames * @throws InvalidNameException * @return void */ public function setName($name) { if (!in_array($name, self::$allowedNames)) { throw new InvalidNameException($name); } $this->name = $name; } /** * @return string */ public function getStyle() { return $this->style; } /** * @param string $style The style of the border part @see BorderPart::$allowedStyles * @throws InvalidStyleException * @return void */ public function setStyle($style) { if (!in_array($style, self::$allowedStyles)) { throw new InvalidStyleException($style); } $this->style = $style; } /** * @return string */ public function getColor() { return $this->color; } /** * @param string $color The color of the border part @see Color::rgb() * @return void */ public function setColor($color) { $this->color = $color; } /** * @return string */ public function getWidth() { return $this->width; } /** * @param string $width The width of the border part @see BorderPart::$allowedWidths * @throws InvalidWidthException * @return void */ public function setWidth($width) { if (!in_array($width, self::$allowedWidths)) { throw new InvalidWidthException($width); } $this->width = $width; } /** * @return array */ public static function getAllowedStyles() { return self::$allowedStyles; } /** * @return array */ public static function getAllowedNames() { return self::$allowedNames; } /** * @return array */ public static function getAllowedWidths() { return self::$allowedWidths; } } Writer/Style/Style.php 0000644 00000024663 15073227061 0010747 0 ustar 00 <?php namespace Box\Spout\Writer\Style; /** * Class Style * Represents a style to be applied to a cell * * @package Box\Spout\Writer\Style */ class Style { /** Default font values */ const DEFAULT_FONT_SIZE = 11; const DEFAULT_FONT_COLOR = Color::BLACK; const DEFAULT_FONT_NAME = 'Arial'; /** @var int|null Style ID */ protected $id = null; /** @var bool Whether the font should be bold */ protected $fontBold = false; /** @var bool Whether the bold property was set */ protected $hasSetFontBold = false; /** @var bool Whether the font should be italic */ protected $fontItalic = false; /** @var bool Whether the italic property was set */ protected $hasSetFontItalic = false; /** @var bool Whether the font should be underlined */ protected $fontUnderline = false; /** @var bool Whether the underline property was set */ protected $hasSetFontUnderline = false; /** @var bool Whether the font should be struck through */ protected $fontStrikethrough = false; /** @var bool Whether the strikethrough property was set */ protected $hasSetFontStrikethrough = false; /** @var int Font size */ protected $fontSize = self::DEFAULT_FONT_SIZE; /** @var bool Whether the font size property was set */ protected $hasSetFontSize = false; /** @var string Font color */ protected $fontColor = self::DEFAULT_FONT_COLOR; /** @var bool Whether the font color property was set */ protected $hasSetFontColor = false; /** @var string Font name */ protected $fontName = self::DEFAULT_FONT_NAME; /** @var bool Whether the font name property was set */ protected $hasSetFontName = false; /** @var bool Whether specific font properties should be applied */ protected $shouldApplyFont = false; /** @var bool Whether the text should wrap in the cell (useful for long or multi-lines text) */ protected $shouldWrapText = false; /** @var bool Whether the wrap text property was set */ protected $hasSetWrapText = false; /** * @var Border */ protected $border = null; /** * @var bool Whether border properties should be applied */ protected $shouldApplyBorder = false; /** @var string Background color */ protected $backgroundColor = null; /** @var bool */ protected $hasSetBackgroundColor = false; /** * @return int|null */ public function getId() { return $this->id; } /** * @param int $id * @return Style */ public function setId($id) { $this->id = $id; return $this; } /** * @return Border */ public function getBorder() { return $this->border; } /** * @param Border $border * @return Style */ public function setBorder(Border $border) { $this->shouldApplyBorder = true; $this->border = $border; return $this; } /** * @return bool */ public function shouldApplyBorder() { return $this->shouldApplyBorder; } /** * @return bool */ public function isFontBold() { return $this->fontBold; } /** * @return Style */ public function setFontBold() { $this->fontBold = true; $this->hasSetFontBold = true; $this->shouldApplyFont = true; return $this; } /** * @return bool */ public function isFontItalic() { return $this->fontItalic; } /** * @return Style */ public function setFontItalic() { $this->fontItalic = true; $this->hasSetFontItalic = true; $this->shouldApplyFont = true; return $this; } /** * @return bool */ public function isFontUnderline() { return $this->fontUnderline; } /** * @return Style */ public function setFontUnderline() { $this->fontUnderline = true; $this->hasSetFontUnderline = true; $this->shouldApplyFont = true; return $this; } /** * @return bool */ public function isFontStrikethrough() { return $this->fontStrikethrough; } /** * @return Style */ public function setFontStrikethrough() { $this->fontStrikethrough = true; $this->hasSetFontStrikethrough = true; $this->shouldApplyFont = true; return $this; } /** * @return int */ public function getFontSize() { return $this->fontSize; } /** * @param int $fontSize Font size, in pixels * @return Style */ public function setFontSize($fontSize) { $this->fontSize = $fontSize; $this->hasSetFontSize = true; $this->shouldApplyFont = true; return $this; } /** * @return string */ public function getFontColor() { return $this->fontColor; } /** * Sets the font color. * * @param string $fontColor ARGB color (@see Color) * @return Style */ public function setFontColor($fontColor) { $this->fontColor = $fontColor; $this->hasSetFontColor = true; $this->shouldApplyFont = true; return $this; } /** * @return string */ public function getFontName() { return $this->fontName; } /** * @param string $fontName Name of the font to use * @return Style */ public function setFontName($fontName) { $this->fontName = $fontName; $this->hasSetFontName = true; $this->shouldApplyFont = true; return $this; } /** * @return bool */ public function shouldWrapText() { return $this->shouldWrapText; } /** * @param bool|void $shouldWrap Should the text be wrapped * @return Style */ public function setShouldWrapText($shouldWrap = true) { $this->shouldWrapText = $shouldWrap; $this->hasSetWrapText = true; return $this; } /** * @return bool */ public function hasSetWrapText() { return $this->hasSetWrapText; } /** * @return bool Whether specific font properties should be applied */ public function shouldApplyFont() { return $this->shouldApplyFont; } /** * Sets the background color * @param string $color ARGB color (@see Color) * @return Style */ public function setBackgroundColor($color) { $this->hasSetBackgroundColor = true; $this->backgroundColor = $color; return $this; } /** * @return string */ public function getBackgroundColor() { return $this->backgroundColor; } /** * * @return bool Whether the background color should be applied */ public function shouldApplyBackgroundColor() { return $this->hasSetBackgroundColor; } /** * Serializes the style for future comparison with other styles. * The ID is excluded from the comparison, as we only care about * actual style properties. * * @return string The serialized style */ public function serialize() { // In order to be able to properly compare style, set static ID value $currentId = $this->id; $this->setId(0); $serializedStyle = serialize($this); $this->setId($currentId); return $serializedStyle; } /** * Merges the current style with the given style, using the given style as a base. This means that: * - if current style and base style both have property A set, use current style property's value * - if current style has property A set but base style does not, use current style property's value * - if base style has property A set but current style does not, use base style property's value * * @NOTE: This function returns a new style. * * @param Style $baseStyle * @return Style New style corresponding to the merge of the 2 styles */ public function mergeWith($baseStyle) { $mergedStyle = clone $this; $this->mergeFontStyles($mergedStyle, $baseStyle); $this->mergeOtherFontProperties($mergedStyle, $baseStyle); $this->mergeCellProperties($mergedStyle, $baseStyle); return $mergedStyle; } /** * @param Style $styleToUpdate (passed as reference) * @param Style $baseStyle * @return void */ private function mergeFontStyles($styleToUpdate, $baseStyle) { if (!$this->hasSetFontBold && $baseStyle->isFontBold()) { $styleToUpdate->setFontBold(); } if (!$this->hasSetFontItalic && $baseStyle->isFontItalic()) { $styleToUpdate->setFontItalic(); } if (!$this->hasSetFontUnderline && $baseStyle->isFontUnderline()) { $styleToUpdate->setFontUnderline(); } if (!$this->hasSetFontStrikethrough && $baseStyle->isFontStrikethrough()) { $styleToUpdate->setFontStrikethrough(); } } /** * @param Style $styleToUpdate Style to update (passed as reference) * @param Style $baseStyle * @return void */ private function mergeOtherFontProperties($styleToUpdate, $baseStyle) { if (!$this->hasSetFontSize && $baseStyle->getFontSize() !== self::DEFAULT_FONT_SIZE) { $styleToUpdate->setFontSize($baseStyle->getFontSize()); } if (!$this->hasSetFontColor && $baseStyle->getFontColor() !== self::DEFAULT_FONT_COLOR) { $styleToUpdate->setFontColor($baseStyle->getFontColor()); } if (!$this->hasSetFontName && $baseStyle->getFontName() !== self::DEFAULT_FONT_NAME) { $styleToUpdate->setFontName($baseStyle->getFontName()); } } /** * @param Style $styleToUpdate Style to update (passed as reference) * @param Style $baseStyle * @return void */ private function mergeCellProperties($styleToUpdate, $baseStyle) { if (!$this->hasSetWrapText && $baseStyle->shouldWrapText()) { $styleToUpdate->setShouldWrapText(); } if (!$this->getBorder() && $baseStyle->shouldApplyBorder()) { $styleToUpdate->setBorder($baseStyle->getBorder()); } if (!$this->hasSetBackgroundColor && $baseStyle->shouldApplyBackgroundColor()) { $styleToUpdate->setBackgroundColor($baseStyle->getBackgroundColor()); } } } Writer/Style/StyleBuilder.php 0000644 00000005665 15073227061 0012257 0 ustar 00 <?php namespace Box\Spout\Writer\Style; /** * Class StyleBuilder * Builder to create new styles * * @package Box\Spout\Writer\Style */ class StyleBuilder { /** @var Style Style to be created */ protected $style; /** * */ public function __construct() { $this->style = new Style(); } /** * Makes the font bold. * * @api * @return StyleBuilder */ public function setFontBold() { $this->style->setFontBold(); return $this; } /** * Makes the font italic. * * @api * @return StyleBuilder */ public function setFontItalic() { $this->style->setFontItalic(); return $this; } /** * Makes the font underlined. * * @api * @return StyleBuilder */ public function setFontUnderline() { $this->style->setFontUnderline(); return $this; } /** * Makes the font struck through. * * @api * @return StyleBuilder */ public function setFontStrikethrough() { $this->style->setFontStrikethrough(); return $this; } /** * Sets the font size. * * @api * @param int $fontSize Font size, in pixels * @return StyleBuilder */ public function setFontSize($fontSize) { $this->style->setFontSize($fontSize); return $this; } /** * Sets the font color. * * @api * @param string $fontColor ARGB color (@see Color) * @return StyleBuilder */ public function setFontColor($fontColor) { $this->style->setFontColor($fontColor); return $this; } /** * Sets the font name. * * @api * @param string $fontName Name of the font to use * @return StyleBuilder */ public function setFontName($fontName) { $this->style->setFontName($fontName); return $this; } /** * Makes the text wrap in the cell if requested * * @api * @param bool $shouldWrap Should the text be wrapped * @return StyleBuilder */ public function setShouldWrapText($shouldWrap = true) { $this->style->setShouldWrapText($shouldWrap); return $this; } /** * Set a border * * @param Border $border * @return $this */ public function setBorder(Border $border) { $this->style->setBorder($border); return $this; } /** * Sets a background color * * @api * @param string $color ARGB color (@see Color) * @return StyleBuilder */ public function setBackgroundColor($color) { $this->style->setBackgroundColor($color); return $this; } /** * Returns the configured style. The style is cached and can be reused. * * @api * @return Style */ public function build() { return $this->style; } } Writer/Style/BorderBuilder.php 0000644 00000004435 15073227061 0012366 0 ustar 00 <?php namespace Box\Spout\Writer\Style; /** * Class BorderBuilder */ class BorderBuilder { /** * @var Border */ protected $border; public function __construct() { $this->border = new Border(); } /** * @param string|void $color Border A RGB color code * @param string|void $width Border width @see BorderPart::allowedWidths * @param string|void $style Border style @see BorderPart::allowedStyles * @return BorderBuilder */ public function setBorderTop($color = Color::BLACK, $width = Border::WIDTH_MEDIUM, $style = Border::STYLE_SOLID) { $this->border->addPart(new BorderPart(Border::TOP, $color, $width, $style)); return $this; } /** * @param string|void $color Border A RGB color code * @param string|void $width Border width @see BorderPart::allowedWidths * @param string|void $style Border style @see BorderPart::allowedStyles * @return BorderBuilder */ public function setBorderRight($color = Color::BLACK, $width = Border::WIDTH_MEDIUM, $style = Border::STYLE_SOLID) { $this->border->addPart(new BorderPart(Border::RIGHT, $color, $width, $style)); return $this; } /** * @param string|void $color Border A RGB color code * @param string|void $width Border width @see BorderPart::allowedWidths * @param string|void $style Border style @see BorderPart::allowedStyles * @return BorderBuilder */ public function setBorderBottom($color = Color::BLACK, $width = Border::WIDTH_MEDIUM, $style = Border::STYLE_SOLID) { $this->border->addPart(new BorderPart(Border::BOTTOM, $color, $width, $style)); return $this; } /** * @param string|void $color Border A RGB color code * @param string|void $width Border width @see BorderPart::allowedWidths * @param string|void $style Border style @see BorderPart::allowedStyles * @return BorderBuilder */ public function setBorderLeft($color = Color::BLACK, $width = Border::WIDTH_MEDIUM, $style = Border::STYLE_SOLID) { $this->border->addPart(new BorderPart(Border::LEFT, $color, $width, $style)); return $this; } /** * @return Border */ public function build() { return $this->border; } } Writer/Exception/InvalidColorException.php 0000644 00000000277 15073227061 0014744 0 ustar 00 <?php namespace Box\Spout\Writer\Exception; /** * Class InvalidColorException * * @api * @package Box\Spout\Writer\Exception */ class InvalidColorException extends WriterException { } Writer/Exception/WriterNotOpenedException.php 0000644 00000000305 15073227061 0015437 0 ustar 00 <?php namespace Box\Spout\Writer\Exception; /** * Class WriterNotOpenedException * * @api * @package Box\Spout\Writer\Exception */ class WriterNotOpenedException extends WriterException { } Writer/Exception/WriterException.php 0000644 00000000360 15073227061 0013624 0 ustar 00 <?php namespace Box\Spout\Writer\Exception; use Box\Spout\Common\Exception\SpoutException; /** * Class WriterException * * @package Box\Spout\Writer\Exception * @abstract */ abstract class WriterException extends SpoutException { } Writer/Exception/SheetNotFoundException.php 0000644 00000000301 15073227061 0015070 0 ustar 00 <?php namespace Box\Spout\Writer\Exception; /** * Class SheetNotFoundException * * @api * @package Box\Spout\Writer\Exception */ class SheetNotFoundException extends WriterException { } Writer/Exception/Border/InvalidStyleException.php 0000644 00000000670 15073227061 0016200 0 ustar 00 <?php namespace Box\Spout\Writer\Exception\Border; use Box\Spout\Writer\Exception\WriterException; use Box\Spout\Writer\Style\BorderPart; class InvalidStyleException extends WriterException { public function __construct($name) { $msg = '%s is not a valid style identifier for a border. Valid identifiers are: %s.'; parent::__construct(sprintf($msg, $name, implode(',', BorderPart::getAllowedStyles()))); } } Writer/Exception/Border/InvalidWidthException.php 0000644 00000000670 15073227061 0016157 0 ustar 00 <?php namespace Box\Spout\Writer\Exception\Border; use Box\Spout\Writer\Exception\WriterException; use Box\Spout\Writer\Style\BorderPart; class InvalidWidthException extends WriterException { public function __construct($name) { $msg = '%s is not a valid width identifier for a border. Valid identifiers are: %s.'; parent::__construct(sprintf($msg, $name, implode(',', BorderPart::getAllowedWidths()))); } } Writer/Exception/Border/InvalidNameException.php 0000644 00000000665 15073227061 0015764 0 ustar 00 <?php namespace Box\Spout\Writer\Exception\Border; use Box\Spout\Writer\Exception\WriterException; use Box\Spout\Writer\Style\BorderPart; class InvalidNameException extends WriterException { public function __construct($name) { $msg = '%s is not a valid name identifier for a border. Valid identifiers are: %s.'; parent::__construct(sprintf($msg, $name, implode(',', BorderPart::getAllowedNames()))); } } Writer/Exception/WriterAlreadyOpenedException.php 0000644 00000000315 15073227061 0016261 0 ustar 00 <?php namespace Box\Spout\Writer\Exception; /** * Class WriterAlreadyOpenedException * * @api * @package Box\Spout\Writer\Exception */ class WriterAlreadyOpenedException extends WriterException { } Writer/Exception/InvalidSheetNameException.php 0000644 00000000307 15073227061 0015531 0 ustar 00 <?php namespace Box\Spout\Writer\Exception; /** * Class InvalidSheetNameException * * @api * @package Box\Spout\Writer\Exception */ class InvalidSheetNameException extends WriterException { } Writer/Common/Helper/ZipHelper.php 0000644 00000017126 15073227061 0013114 0 ustar 00 <?php namespace Box\Spout\Writer\Common\Helper; /** * Class ZipHelper * This class provides helper functions to create zip files * * @package Box\Spout\Writer\Common\Helper */ class ZipHelper { const ZIP_EXTENSION = '.zip'; /** Controls what to do when trying to add an existing file */ const EXISTING_FILES_SKIP = 'skip'; const EXISTING_FILES_OVERWRITE = 'overwrite'; /** @var string Path of the folder where the zip file will be created */ protected $tmpFolderPath; /** @var \ZipArchive The ZipArchive instance */ protected $zip; /** * @param string $tmpFolderPath Path of the temp folder where the zip file will be created */ public function __construct($tmpFolderPath) { $this->tmpFolderPath = $tmpFolderPath; } /** * Returns the already created ZipArchive instance or * creates one if none exists. * * @return \ZipArchive */ protected function createOrGetZip() { if (!isset($this->zip)) { $this->zip = new \ZipArchive(); $zipFilePath = $this->getZipFilePath(); $this->zip->open($zipFilePath, \ZipArchive::CREATE|\ZipArchive::OVERWRITE); } return $this->zip; } /** * @return string Path where the zip file of the given folder will be created */ public function getZipFilePath() { return $this->tmpFolderPath . self::ZIP_EXTENSION; } /** * Adds the given file, located under the given root folder to the archive. * The file will be compressed. * * Example of use: * addFileToArchive('/tmp/xlsx/foo', 'bar/baz.xml'); * => will add the file located at '/tmp/xlsx/foo/bar/baz.xml' in the archive, but only as 'bar/baz.xml' * * @param string $rootFolderPath Path of the root folder that will be ignored in the archive tree. * @param string $localFilePath Path of the file to be added, under the root folder * @param string|void $existingFileMode Controls what to do when trying to add an existing file * @return void */ public function addFileToArchive($rootFolderPath, $localFilePath, $existingFileMode = self::EXISTING_FILES_OVERWRITE) { $this->addFileToArchiveWithCompressionMethod( $rootFolderPath, $localFilePath, $existingFileMode, \ZipArchive::CM_DEFAULT ); } /** * Adds the given file, located under the given root folder to the archive. * The file will NOT be compressed. * * Example of use: * addUncompressedFileToArchive('/tmp/xlsx/foo', 'bar/baz.xml'); * => will add the file located at '/tmp/xlsx/foo/bar/baz.xml' in the archive, but only as 'bar/baz.xml' * * @param string $rootFolderPath Path of the root folder that will be ignored in the archive tree. * @param string $localFilePath Path of the file to be added, under the root folder * @param string|void $existingFileMode Controls what to do when trying to add an existing file * @return void */ public function addUncompressedFileToArchive($rootFolderPath, $localFilePath, $existingFileMode = self::EXISTING_FILES_OVERWRITE) { $this->addFileToArchiveWithCompressionMethod( $rootFolderPath, $localFilePath, $existingFileMode, \ZipArchive::CM_STORE ); } /** * Adds the given file, located under the given root folder to the archive. * The file will NOT be compressed. * * Example of use: * addUncompressedFileToArchive('/tmp/xlsx/foo', 'bar/baz.xml'); * => will add the file located at '/tmp/xlsx/foo/bar/baz.xml' in the archive, but only as 'bar/baz.xml' * * @param string $rootFolderPath Path of the root folder that will be ignored in the archive tree. * @param string $localFilePath Path of the file to be added, under the root folder * @param string $existingFileMode Controls what to do when trying to add an existing file * @param int $compressionMethod The compression method * @return void */ protected function addFileToArchiveWithCompressionMethod($rootFolderPath, $localFilePath, $existingFileMode, $compressionMethod) { $zip = $this->createOrGetZip(); if (!$this->shouldSkipFile($zip, $localFilePath, $existingFileMode)) { $normalizedFullFilePath = $this->getNormalizedRealPath($rootFolderPath . '/' . $localFilePath); $zip->addFile($normalizedFullFilePath, $localFilePath); if (self::canChooseCompressionMethod()) { $zip->setCompressionName($localFilePath, $compressionMethod); } } } /** * @return bool Whether it is possible to choose the desired compression method to be used */ public static function canChooseCompressionMethod() { // setCompressionName() is a PHP7+ method... return (method_exists(new \ZipArchive(), 'setCompressionName')); } /** * @param string $folderPath Path to the folder to be zipped * @param string|void $existingFileMode Controls what to do when trying to add an existing file * @return void */ public function addFolderToArchive($folderPath, $existingFileMode = self::EXISTING_FILES_OVERWRITE) { $zip = $this->createOrGetZip(); $folderRealPath = $this->getNormalizedRealPath($folderPath) . '/'; $itemIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($folderPath, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST); foreach ($itemIterator as $itemInfo) { $itemRealPath = $this->getNormalizedRealPath($itemInfo->getPathname()); $itemLocalPath = str_replace($folderRealPath, '', $itemRealPath); if ($itemInfo->isFile() && !$this->shouldSkipFile($zip, $itemLocalPath, $existingFileMode)) { $zip->addFile($itemRealPath, $itemLocalPath); } } } /** * @param \ZipArchive $zip * @param string $itemLocalPath * @param string $existingFileMode * @return bool Whether the file should be added to the archive or skipped */ protected function shouldSkipFile($zip, $itemLocalPath, $existingFileMode) { // Skip files if: // - EXISTING_FILES_SKIP mode chosen // - File already exists in the archive return ($existingFileMode === self::EXISTING_FILES_SKIP && $zip->locateName($itemLocalPath) !== false); } /** * Returns canonicalized absolute pathname, containing only forward slashes. * * @param string $path Path to normalize * @return string Normalized and canonicalized path */ protected function getNormalizedRealPath($path) { $realPath = realpath($path); return str_replace(DIRECTORY_SEPARATOR, '/', $realPath); } /** * Closes the archive and copies it into the given stream * * @param resource $streamPointer Pointer to the stream to copy the zip * @return void */ public function closeArchiveAndCopyToStream($streamPointer) { $zip = $this->createOrGetZip(); $zip->close(); unset($this->zip); $this->copyZipToStream($streamPointer); } /** * Streams the contents of the zip file into the given stream * * @param resource $pointer Pointer to the stream to copy the zip * @return void */ protected function copyZipToStream($pointer) { $zipFilePointer = fopen($this->getZipFilePath(), 'r'); stream_copy_to_stream($zipFilePointer, $pointer); fclose($zipFilePointer); } } Writer/Common/Helper/CellHelper.php 0000644 00000005331 15073227061 0013224 0 ustar 00 <?php namespace Box\Spout\Writer\Common\Helper; /** * Class CellHelper * This class provides helper functions when working with cells * * @package Box\Spout\Writer\Common\Helper */ class CellHelper { /** @var array Cache containing the mapping column index => cell index */ private static $columnIndexToCellIndexCache = []; /** * Returns the cell index (base 26) associated to the base 10 column index. * Excel uses A to Z letters for column indexing, where A is the 1st column, * Z is the 26th and AA is the 27th. * The mapping is zero based, so that 0 maps to A, B maps to 1, Z to 25 and AA to 26. * * @param int $columnIndex The Excel column index (0, 42, ...) * @return string The associated cell index ('A', 'BC', ...) */ public static function getCellIndexFromColumnIndex($columnIndex) { $originalColumnIndex = $columnIndex; // Using isset here because it is way faster than array_key_exists... if (!isset(self::$columnIndexToCellIndexCache[$originalColumnIndex])) { $cellIndex = ''; $capitalAAsciiValue = ord('A'); do { $modulus = $columnIndex % 26; $cellIndex = chr($capitalAAsciiValue + $modulus) . $cellIndex; // substracting 1 because it's zero-based $columnIndex = intval($columnIndex / 26) - 1; } while ($columnIndex >= 0); self::$columnIndexToCellIndexCache[$originalColumnIndex] = $cellIndex; } return self::$columnIndexToCellIndexCache[$originalColumnIndex]; } /** * @param $value * @return bool Whether the given value is considered "empty" */ public static function isEmpty($value) { return ($value === null || $value === ''); } /** * @param $value * @return bool Whether the given value is a non empty string */ public static function isNonEmptyString($value) { return (gettype($value) === 'string' && $value !== ''); } /** * Returns whether the given value is numeric. * A numeric value is from type "integer" or "double" ("float" is not returned by gettype). * * @param $value * @return bool Whether the given value is numeric */ public static function isNumeric($value) { $valueType = gettype($value); return ($valueType === 'integer' || $valueType === 'double'); } /** * Returns whether the given value is boolean. * "true"/"false" and 0/1 are not booleans. * * @param $value * @return bool Whether the given value is boolean */ public static function isBoolean($value) { return gettype($value) === 'boolean'; } } Writer/Common/Helper/AbstractStyleHelper.php 0000644 00000011171 15073227061 0015130 0 ustar 00 <?php namespace Box\Spout\Writer\Common\Helper; /** * Class AbstractStyleHelper * This class provides helper functions to manage styles * * @package Box\Spout\Writer\Common\Helper */ abstract class AbstractStyleHelper { /** @var array [SERIALIZED_STYLE] => [STYLE_ID] mapping table, keeping track of the registered styles */ protected $serializedStyleToStyleIdMappingTable = []; /** @var array [STYLE_ID] => [STYLE] mapping table, keeping track of the registered styles */ protected $styleIdToStyleMappingTable = []; /** * @param \Box\Spout\Writer\Style\Style $defaultStyle */ public function __construct($defaultStyle) { // This ensures that the default style is the first one to be registered $this->registerStyle($defaultStyle); } /** * Registers the given style as a used style. * Duplicate styles won't be registered more than once. * * @param \Box\Spout\Writer\Style\Style $style The style to be registered * @return \Box\Spout\Writer\Style\Style The registered style, updated with an internal ID. */ public function registerStyle($style) { $serializedStyle = $style->serialize(); if (!$this->hasStyleAlreadyBeenRegistered($style)) { $nextStyleId = count($this->serializedStyleToStyleIdMappingTable); $style->setId($nextStyleId); $this->serializedStyleToStyleIdMappingTable[$serializedStyle] = $nextStyleId; $this->styleIdToStyleMappingTable[$nextStyleId] = $style; } return $this->getStyleFromSerializedStyle($serializedStyle); } /** * Returns whether the given style has already been registered. * * @param \Box\Spout\Writer\Style\Style $style * @return bool */ protected function hasStyleAlreadyBeenRegistered($style) { $serializedStyle = $style->serialize(); // Using isset here because it is way faster than array_key_exists... return isset($this->serializedStyleToStyleIdMappingTable[$serializedStyle]); } /** * Returns the registered style associated to the given serialization. * * @param string $serializedStyle The serialized style from which the actual style should be fetched from * @return \Box\Spout\Writer\Style\Style */ protected function getStyleFromSerializedStyle($serializedStyle) { $styleId = $this->serializedStyleToStyleIdMappingTable[$serializedStyle]; return $this->styleIdToStyleMappingTable[$styleId]; } /** * @return \Box\Spout\Writer\Style\Style[] List of registered styles */ protected function getRegisteredStyles() { return array_values($this->styleIdToStyleMappingTable); } /** * Returns the default style * * @return \Box\Spout\Writer\Style\Style Default style */ protected function getDefaultStyle() { // By construction, the default style has ID 0 return $this->styleIdToStyleMappingTable[0]; } /** * Apply additional styles if the given row needs it. * Typically, set "wrap text" if a cell contains a new line. * * @param \Box\Spout\Writer\Style\Style $style The original style * @param array $dataRow The row the style will be applied to * @return \Box\Spout\Writer\Style\Style The updated style */ public function applyExtraStylesIfNeeded($style, $dataRow) { $updatedStyle = $this->applyWrapTextIfCellContainsNewLine($style, $dataRow); return $updatedStyle; } /** * Set the "wrap text" option if a cell of the given row contains a new line. * * @NOTE: There is a bug on the Mac version of Excel (2011 and below) where new lines * are ignored even when the "wrap text" option is set. This only occurs with * inline strings (shared strings do work fine). * A workaround would be to encode "\n" as "_x000D_" but it does not work * on the Windows version of Excel... * * @param \Box\Spout\Writer\Style\Style $style The original style * @param array $dataRow The row the style will be applied to * @return \Box\Spout\Writer\Style\Style The eventually updated style */ protected function applyWrapTextIfCellContainsNewLine($style, $dataRow) { // if the "wrap text" option is already set, no-op if ($style->hasSetWrapText()) { return $style; } foreach ($dataRow as $cell) { if (is_string($cell) && strpos($cell, "\n") !== false) { $style->setShouldWrapText(); break; } } return $style; } } Writer/Common/Sheet.php 0000644 00000014066 15073227061 0011043 0 ustar 00 <?php namespace Box\Spout\Writer\Common; use Box\Spout\Common\Helper\StringHelper; use Box\Spout\Writer\Exception\InvalidSheetNameException; /** * Class Sheet * External representation of a worksheet * * @package Box\Spout\Writer\Common */ class Sheet { const DEFAULT_SHEET_NAME_PREFIX = 'Sheet'; /** Sheet name should not exceed 31 characters */ const MAX_LENGTH_SHEET_NAME = 31; /** @var array Invalid characters that cannot be contained in the sheet name */ private static $INVALID_CHARACTERS_IN_SHEET_NAME = ['\\', '/', '?', '*', ':', '[', ']']; /** @var array Associative array [WORKBOOK_ID] => [[SHEET_INDEX] => [SHEET_NAME]] keeping track of sheets' name to enforce uniqueness per workbook */ protected static $SHEETS_NAME_USED = []; /** @var int Index of the sheet, based on order in the workbook (zero-based) */ protected $index; /** @var string ID of the sheet's associated workbook. Used to restrict sheet name uniqueness enforcement to a single workbook */ protected $associatedWorkbookId; /** @var string Name of the sheet */ protected $name; /** @var \Box\Spout\Common\Helper\StringHelper */ protected $stringHelper; /** * @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based) * @param string $associatedWorkbookId ID of the sheet's associated workbook */ public function __construct($sheetIndex, $associatedWorkbookId) { $this->index = $sheetIndex; $this->associatedWorkbookId = $associatedWorkbookId; if (!isset(self::$SHEETS_NAME_USED[$associatedWorkbookId])) { self::$SHEETS_NAME_USED[$associatedWorkbookId] = []; } $this->stringHelper = new StringHelper(); $this->setName(self::DEFAULT_SHEET_NAME_PREFIX . ($sheetIndex + 1)); } /** * @api * @return int Index of the sheet, based on order in the workbook (zero-based) */ public function getIndex() { return $this->index; } /** * @api * @return string Name of the sheet */ public function getName() { return $this->name; } /** * Sets the name of the sheet. Note that Excel has some restrictions on the name: * - it should not be blank * - it should not exceed 31 characters * - it should not contain these characters: \ / ? * : [ or ] * - it should be unique * * @api * @param string $name Name of the sheet * @return Sheet * @throws \Box\Spout\Writer\Exception\InvalidSheetNameException If the sheet's name is invalid. */ public function setName($name) { $this->throwIfNameIsInvalid($name); $this->name = $name; self::$SHEETS_NAME_USED[$this->associatedWorkbookId][$this->index] = $name; return $this; } /** * Throws an exception if the given sheet's name is not valid. * @see Sheet::setName for validity rules. * * @param string $name * @return void * @throws \Box\Spout\Writer\Exception\InvalidSheetNameException If the sheet's name is invalid. */ protected function throwIfNameIsInvalid($name) { if (!is_string($name)) { $actualType = gettype($name); $errorMessage = "The sheet's name is invalid. It must be a string ($actualType given)."; throw new InvalidSheetNameException($errorMessage); } $failedRequirements = []; $nameLength = $this->stringHelper->getStringLength($name); if (!$this->isNameUnique($name)) { $failedRequirements[] = 'It should be unique'; } else { if ($nameLength === 0) { $failedRequirements[] = 'It should not be blank'; } else { if ($nameLength > self::MAX_LENGTH_SHEET_NAME) { $failedRequirements[] = 'It should not exceed 31 characters'; } if ($this->doesContainInvalidCharacters($name)) { $failedRequirements[] = 'It should not contain these characters: \\ / ? * : [ or ]'; } if ($this->doesStartOrEndWithSingleQuote($name)) { $failedRequirements[] = 'It should not start or end with a single quote'; } } } if (count($failedRequirements) !== 0) { $errorMessage = "The sheet's name (\"$name\") is invalid. It did not respect these rules:\n - "; $errorMessage .= implode("\n - ", $failedRequirements); throw new InvalidSheetNameException($errorMessage); } } /** * Returns whether the given name contains at least one invalid character. * @see Sheet::$INVALID_CHARACTERS_IN_SHEET_NAME for the full list. * * @param string $name * @return bool TRUE if the name contains invalid characters, FALSE otherwise. */ protected function doesContainInvalidCharacters($name) { return (str_replace(self::$INVALID_CHARACTERS_IN_SHEET_NAME, '', $name) !== $name); } /** * Returns whether the given name starts or ends with a single quote * * @param string $name * @return bool TRUE if the name starts or ends with a single quote, FALSE otherwise. */ protected function doesStartOrEndWithSingleQuote($name) { $startsWithSingleQuote = ($this->stringHelper->getCharFirstOccurrencePosition('\'', $name) === 0); $endsWithSingleQuote = ($this->stringHelper->getCharLastOccurrencePosition('\'', $name) === ($this->stringHelper->getStringLength($name) - 1)); return ($startsWithSingleQuote || $endsWithSingleQuote); } /** * Returns whether the given name is unique. * * @param string $name * @return bool TRUE if the name is unique, FALSE otherwise. */ protected function isNameUnique($name) { foreach (self::$SHEETS_NAME_USED[$this->associatedWorkbookId] as $sheetIndex => $sheetName) { if ($sheetIndex !== $this->index && $sheetName === $name) { return false; } } return true; } } Writer/Common/Internal/WorksheetInterface.php 0000644 00000002071 15073227061 0015334 0 ustar 00 <?php namespace Box\Spout\Writer\Common\Internal; /** * Interface WorksheetInterface * * @package Box\Spout\Writer\Common\Internal */ interface WorksheetInterface { /** * @return \Box\Spout\Writer\Common\Sheet The "external" sheet */ public function getExternalSheet(); /** * @return int The index of the last written row */ public function getLastWrittenRowIndex(); /** * Adds data to the worksheet. * * @param array $dataRow Array containing data to be written. * 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); /** * Closes the worksheet * * @return void */ public function close(); } Writer/Common/Internal/WorkbookInterface.php 0000644 00000005220 15073227061 0015155 0 ustar 00 <?php namespace Box\Spout\Writer\Common\Internal; /** * Interface WorkbookInterface * * @package Box\Spout\Writer\Common\Internal */ interface WorkbookInterface { /** * Creates a new sheet in the workbook. The current sheet remains unchanged. * * @return WorksheetInterface The created sheet * @throws \Box\Spout\Common\Exception\IOException If unable to open the sheet for writing */ public function addNewSheet(); /** * Creates a new sheet in the workbook and make it the current sheet. * The writing will resume where it stopped (i.e. data won't be truncated). * * @return WorksheetInterface The created sheet * @throws \Box\Spout\Common\Exception\IOException If unable to open the sheet for writing */ public function addNewSheetAndMakeItCurrent(); /** * @return WorksheetInterface[] All the workbook's sheets */ public function getWorksheets(); /** * Returns the current sheet * * @return WorksheetInterface The current sheet */ public function getCurrentWorksheet(); /** * Sets the given sheet as the current one. New data will be written to this sheet. * The writing will resume where it stopped (i.e. data won't be truncated). * * @param \Box\Spout\Writer\Common\Sheet $sheet The "external" sheet to set as current * @return void * @throws \Box\Spout\Writer\Exception\SheetNotFoundException If the given sheet does not exist in the workbook */ public function setCurrentSheet($sheet); /** * Adds data to the current sheet. * If shouldCreateNewSheetsAutomatically option is set to true, it will handle pagination * with the creation of new worksheets if one worksheet has reached its maximum capicity. * * @param array $dataRow Array containing data to be written. * Example $dataRow = ['data1', 1234, null, '', 'data5']; * @param \Box\Spout\Writer\Style\Style $style Style to be applied to the row. * @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, $style); /** * Closes the workbook and all its associated sheets. * All the necessary files are written to disk and zipped together to create the ODS file. * All the temporary files are then deleted. * * @param resource $finalFilePointer Pointer to the ODS that will be created * @return void */ public function close($finalFilePointer); } Writer/Common/Internal/AbstractWorkbook.php 0000644 00000015431 15073227061 0015025 0 ustar 00 <?php namespace Box\Spout\Writer\Common\Internal; use Box\Spout\Writer\Exception\SheetNotFoundException; /** * Class Workbook * Represents a workbook within a spreadsheet file. * It provides the functions to work with worksheets. * * @package Box\Spout\Writer\Common */ abstract class AbstractWorkbook implements WorkbookInterface { /** @var bool Whether new sheets should be automatically created when the max rows limit per sheet is reached */ protected $shouldCreateNewSheetsAutomatically; /** @var string Timestamp based unique ID identifying the workbook */ protected $internalId; /** @var WorksheetInterface[] Array containing the workbook's sheets */ protected $worksheets = []; /** @var WorksheetInterface The worksheet where data will be written to */ protected $currentWorksheet; /** * @param bool $shouldCreateNewSheetsAutomatically * @param \Box\Spout\Writer\Style\Style $defaultRowStyle * @throws \Box\Spout\Common\Exception\IOException If unable to create at least one of the base folders */ public function __construct($shouldCreateNewSheetsAutomatically, $defaultRowStyle) { $this->shouldCreateNewSheetsAutomatically = $shouldCreateNewSheetsAutomatically; $this->internalId = uniqid(); } /** * @return \Box\Spout\Writer\Common\Helper\AbstractStyleHelper The specific style helper */ abstract protected function getStyleHelper(); /** * @return int Maximum number of rows/columns a sheet can contain */ abstract protected function getMaxRowsPerWorksheet(); /** * Creates a new sheet in the workbook. The current sheet remains unchanged. * * @return WorksheetInterface The created sheet * @throws \Box\Spout\Common\Exception\IOException If unable to open the sheet for writing */ abstract public function addNewSheet(); /** * Creates a new sheet in the workbook and make it the current sheet. * The writing will resume where it stopped (i.e. data won't be truncated). * * @return WorksheetInterface The created sheet * @throws \Box\Spout\Common\Exception\IOException If unable to open the sheet for writing */ public function addNewSheetAndMakeItCurrent() { $worksheet = $this->addNewSheet(); $this->setCurrentWorksheet($worksheet); return $worksheet; } /** * @return WorksheetInterface[] All the workbook's sheets */ public function getWorksheets() { return $this->worksheets; } /** * Returns the current sheet * * @return WorksheetInterface The current sheet */ public function getCurrentWorksheet() { return $this->currentWorksheet; } /** * Sets the given sheet as the current one. New data will be written to this sheet. * The writing will resume where it stopped (i.e. data won't be truncated). * * @param \Box\Spout\Writer\Common\Sheet $sheet The "external" sheet to set as current * @return void * @throws \Box\Spout\Writer\Exception\SheetNotFoundException If the given sheet does not exist in the workbook */ public function setCurrentSheet($sheet) { $worksheet = $this->getWorksheetFromExternalSheet($sheet); if ($worksheet !== null) { $this->currentWorksheet = $worksheet; } else { throw new SheetNotFoundException('The given sheet does not exist in the workbook.'); } } /** * @param WorksheetInterface $worksheet * @return void */ protected function setCurrentWorksheet($worksheet) { $this->currentWorksheet = $worksheet; } /** * Returns the worksheet associated to the given external sheet. * * @param \Box\Spout\Writer\Common\Sheet $sheet * @return WorksheetInterface|null The worksheet associated to the given external sheet or null if not found. */ protected function getWorksheetFromExternalSheet($sheet) { $worksheetFound = null; foreach ($this->worksheets as $worksheet) { if ($worksheet->getExternalSheet() === $sheet) { $worksheetFound = $worksheet; break; } } return $worksheetFound; } /** * Adds data to the current sheet. * If shouldCreateNewSheetsAutomatically option is set to true, it will handle pagination * with the creation of new worksheets if one worksheet has reached its maximum capicity. * * @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. * @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, $style) { $currentWorksheet = $this->getCurrentWorksheet(); $hasReachedMaxRows = $this->hasCurrentWorkseetReachedMaxRows(); $styleHelper = $this->getStyleHelper(); // if we reached the maximum number of rows for the current sheet... if ($hasReachedMaxRows) { // ... continue writing in a new sheet if option set if ($this->shouldCreateNewSheetsAutomatically) { $currentWorksheet = $this->addNewSheetAndMakeItCurrent(); $updatedStyle = $styleHelper->applyExtraStylesIfNeeded($style, $dataRow); $registeredStyle = $styleHelper->registerStyle($updatedStyle); $currentWorksheet->addRow($dataRow, $registeredStyle); } else { // otherwise, do nothing as the data won't be read anyways } } else { $updatedStyle = $styleHelper->applyExtraStylesIfNeeded($style, $dataRow); $registeredStyle = $styleHelper->registerStyle($updatedStyle); $currentWorksheet->addRow($dataRow, $registeredStyle); } } /** * @return bool Whether the current worksheet has reached the maximum number of rows per sheet. */ protected function hasCurrentWorkseetReachedMaxRows() { $currentWorksheet = $this->getCurrentWorksheet(); return ($currentWorksheet->getLastWrittenRowIndex() >= $this->getMaxRowsPerWorksheet()); } /** * Closes the workbook and all its associated sheets. * All the necessary files are written to disk and zipped together to create the ODS file. * All the temporary files are then deleted. * * @param resource $finalFilePointer Pointer to the ODS that will be created * @return void */ abstract public function close($finalFilePointer); } Writer/WriterInterface.php 0000644 00000007220 15073227061 0011632 0 ustar 00 <?php namespace Box\Spout\Writer; /** * Interface WriterInterface * * @package Box\Spout\Writer */ interface WriterInterface { /** * Inits the writer and opens it to accept data. * By using this method, the data will be written to a file. * * @param string $outputFilePath Path of the output file that will contain the data * @return WriterInterface * @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened or if the given path is not writable */ public function openToFile($outputFilePath); /** * Inits the writer and opens it to accept data. * By using this method, the data will be outputted directly to the browser. * * @param string $outputFileName Name of the output file that will contain the data. If a path is passed in, only the file name will be kept * @return WriterInterface * @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened */ public function openToBrowser($outputFileName); /** * Write given data to the output. New data will be appended to end of stream. * * @param array $dataRow Array containing data to be streamed. * Example $dataRow = ['data1', 1234, null, '', 'data5']; * @return WriterInterface * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet * @throws \Box\Spout\Common\Exception\IOException If unable to write data */ public function addRow(array $dataRow); /** * Write given data to the output and apply the given style. * @see addRow * * @param array $dataRow Array of array containing data to be streamed. * @param Style\Style $style Style to be applied to the row. * @return WriterInterface * @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid * @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 addRowWithStyle(array $dataRow, $style); /** * Write given data to the output. New data will be appended to end of stream. * * @param array $dataRows Array of array containing data to be streamed. * Example $dataRow = [ * ['data11', 12, , '', 'data13'], * ['data21', 'data22', null], * ]; * @return WriterInterface * @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet * @throws \Box\Spout\Common\Exception\IOException If unable to write data */ public function addRows(array $dataRows); /** * Write given data to the output and apply the given style. * @see addRows * * @param array $dataRows Array of array containing data to be streamed. * @param Style\Style $style Style to be applied to the rows. * @return WriterInterface * @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid * @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 addRowsWithStyle(array $dataRows, $style); /** * Closes the writer. This will close the streamer as well, preventing new data * to be written to the file. * * @return void */ public function close(); } Writer/CSV/Writer.php 0000644 00000006417 15073227061 0010453 0 ustar 00 <?php namespace Box\Spout\Writer\CSV; use Box\Spout\Writer\AbstractWriter; use Box\Spout\Common\Exception\IOException; use Box\Spout\Common\Helper\EncodingHelper; /** * Class Writer * This class provides support to write data to CSV files * * @package Box\Spout\Writer\CSV */ class Writer extends AbstractWriter { /** Number of rows to write before flushing */ const FLUSH_THRESHOLD = 500; /** @var string Content-Type value for the header */ protected static $headerContentType = 'text/csv; charset=UTF-8'; /** @var string Defines the character used to delimit fields (one character only) */ protected $fieldDelimiter = ','; /** @var string Defines the character used to enclose fields (one character only) */ protected $fieldEnclosure = '"'; /** @var int */ protected $lastWrittenRowIndex = 0; /** @var bool */ protected $shouldAddBOM = true; /** * Sets the field delimiter for the CSV * * @api * @param string $fieldDelimiter Character that delimits fields * @return Writer */ public function setFieldDelimiter($fieldDelimiter) { $this->fieldDelimiter = $fieldDelimiter; return $this; } /** * Sets the field enclosure for the CSV * * @api * @param string $fieldEnclosure Character that enclose fields * @return Writer */ public function setFieldEnclosure($fieldEnclosure) { $this->fieldEnclosure = $fieldEnclosure; return $this; } /** * Set if a BOM has to be added to the file * * @param bool $shouldAddBOM * @return Writer */ public function setShouldAddBOM($shouldAddBOM) { $this->shouldAddBOM = (bool) $shouldAddBOM; return $this; } /** * Opens the CSV streamer and makes it ready to accept data. * * @return void */ protected function openWriter() { if ($this->shouldAddBOM) { // Adds UTF-8 BOM for Unicode compatibility $this->globalFunctionsHelper->fputs($this->filePointer, EncodingHelper::BOM_UTF8); } } /** * Adds data to the currently opened writer. * * @param array $dataRow Array containing data to be written. * Example $dataRow = ['data1', 1234, null, '', 'data5']; * @param \Box\Spout\Writer\Style\Style $style Ignored here since CSV does not support styling. * @return void * @throws \Box\Spout\Common\Exception\IOException If unable to write data */ protected function addRowToWriter(array $dataRow, $style) { $wasWriteSuccessful = $this->globalFunctionsHelper->fputcsv($this->filePointer, $dataRow, $this->fieldDelimiter, $this->fieldEnclosure); 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 CSV streamer, preventing any additional writing. * If set, sets the headers and redirects output to the browser. * * @return void */ protected function closeWriter() { $this->lastWrittenRowIndex = 0; } } Writer/WriterFactory.php 0000644 00000002454 15073227061 0011345 0 ustar 00 <?php namespace Box\Spout\Writer; use Box\Spout\Common\Exception\UnsupportedTypeException; use Box\Spout\Common\Helper\GlobalFunctionsHelper; use Box\Spout\Common\Type; /** * Class WriterFactory * This factory is used to create writers, based on the type of the file to be read. * It supports CSV, XLSX and ODS formats. * * @package Box\Spout\Writer */ class WriterFactory { /** * This creates an instance of the appropriate writer, given the type of the file to be read * * @api * @param string $writerType Type of the writer to instantiate * @return WriterInterface * @throws \Box\Spout\Common\Exception\UnsupportedTypeException */ public static function create($writerType) { $writer = null; switch ($writerType) { case Type::CSV: $writer = new CSV\Writer(); break; case Type::XLSX: $writer = new XLSX\Writer(); break; case Type::ODS: $writer = new ODS\Writer(); break; default: throw new UnsupportedTypeException('No writers supporting the given type: ' . $writerType); } $writer->setGlobalFunctionsHelper(new GlobalFunctionsHelper()); return $writer; } } Writer/ODS/Helper/FileSystemHelper.php 0000644 00000024742 15073227061 0013635 0 ustar 00 <?php namespace Box\Spout\Writer\ODS\Helper; use Box\Spout\Writer\Common\Helper\ZipHelper; use Box\Spout\Writer\ODS\Internal\Worksheet; /** * Class FileSystemHelper * This class provides helper functions to help with the file system operations * like files/folders creation & deletion for ODS files * * @package Box\Spout\Writer\ODS\Helper */ class FileSystemHelper extends \Box\Spout\Common\Helper\FileSystemHelper { const APP_NAME = 'Spout'; const MIMETYPE = 'application/vnd.oasis.opendocument.spreadsheet'; const META_INF_FOLDER_NAME = 'META-INF'; const SHEETS_CONTENT_TEMP_FOLDER_NAME = 'worksheets-temp'; const MANIFEST_XML_FILE_NAME = 'manifest.xml'; const CONTENT_XML_FILE_NAME = 'content.xml'; const META_XML_FILE_NAME = 'meta.xml'; const MIMETYPE_FILE_NAME = 'mimetype'; const STYLES_XML_FILE_NAME = 'styles.xml'; /** @var string Path to the root folder inside the temp folder where the files to create the ODS will be stored */ protected $rootFolder; /** @var string Path to the "META-INF" folder inside the root folder */ protected $metaInfFolder; /** @var string Path to the temp folder, inside the root folder, where specific sheets content will be written to */ protected $sheetsContentTempFolder; /** * @return string */ public function getRootFolder() { return $this->rootFolder; } /** * @return string */ public function getSheetsContentTempFolder() { return $this->sheetsContentTempFolder; } /** * Creates all the folders needed to create a ODS file, as well as the files that won't change. * * @return void * @throws \Box\Spout\Common\Exception\IOException If unable to create at least one of the base folders */ public function createBaseFilesAndFolders() { $this ->createRootFolder() ->createMetaInfoFolderAndFile() ->createSheetsContentTempFolder() ->createMetaFile() ->createMimetypeFile(); } /** * Creates the folder that will be used as root * * @return FileSystemHelper * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder */ protected function createRootFolder() { $this->rootFolder = $this->createFolder($this->baseFolderRealPath, uniqid('ods')); return $this; } /** * Creates the "META-INF" folder under the root folder as well as the "manifest.xml" file in it * * @return FileSystemHelper * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder or the "manifest.xml" file */ protected function createMetaInfoFolderAndFile() { $this->metaInfFolder = $this->createFolder($this->rootFolder, self::META_INF_FOLDER_NAME); $this->createManifestFile(); return $this; } /** * Creates the "manifest.xml" file under the "META-INF" folder (under root) * * @return FileSystemHelper * @throws \Box\Spout\Common\Exception\IOException If unable to create the file */ protected function createManifestFile() { $manifestXmlFileContents = <<<EOD <?xml version="1.0" encoding="UTF-8"?> <manifest:manifest xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0" manifest:version="1.2"> <manifest:file-entry manifest:full-path="/" manifest:media-type="application/vnd.oasis.opendocument.spreadsheet"/> <manifest:file-entry manifest:full-path="styles.xml" manifest:media-type="text/xml"/> <manifest:file-entry manifest:full-path="content.xml" manifest:media-type="text/xml"/> <manifest:file-entry manifest:full-path="meta.xml" manifest:media-type="text/xml"/> </manifest:manifest> EOD; $this->createFileWithContents($this->metaInfFolder, self::MANIFEST_XML_FILE_NAME, $manifestXmlFileContents); return $this; } /** * Creates the temp folder where specific sheets content will be written to. * This folder is not part of the final ODS file and is only used to be able to jump between sheets. * * @return FileSystemHelper * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder */ protected function createSheetsContentTempFolder() { $this->sheetsContentTempFolder = $this->createFolder($this->rootFolder, self::SHEETS_CONTENT_TEMP_FOLDER_NAME); return $this; } /** * Creates the "meta.xml" file under the root folder * * @return FileSystemHelper * @throws \Box\Spout\Common\Exception\IOException If unable to create the file */ protected function createMetaFile() { $appName = self::APP_NAME; $createdDate = (new \DateTime())->format(\DateTime::W3C); $metaXmlFileContents = <<<EOD <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <office:document-meta office:version="1.2" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:xlink="http://www.w3.org/1999/xlink"> <office:meta> <dc:creator>$appName</dc:creator> <meta:creation-date>$createdDate</meta:creation-date> <dc:date>$createdDate</dc:date> </office:meta> </office:document-meta> EOD; $this->createFileWithContents($this->rootFolder, self::META_XML_FILE_NAME, $metaXmlFileContents); return $this; } /** * Creates the "mimetype" file under the root folder * * @return FileSystemHelper * @throws \Box\Spout\Common\Exception\IOException If unable to create the file */ protected function createMimetypeFile() { $this->createFileWithContents($this->rootFolder, self::MIMETYPE_FILE_NAME, self::MIMETYPE); return $this; } /** * Creates the "content.xml" file under the root folder * * @param Worksheet[] $worksheets * @param StyleHelper $styleHelper * @return FileSystemHelper */ public function createContentFile($worksheets, $styleHelper) { $contentXmlFileContents = <<<EOD <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <office:document-content office:version="1.2" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:calcext="urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0" xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:msoxl="http://schemas.microsoft.com/office/excel/formula" xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:xlink="http://www.w3.org/1999/xlink"> EOD; $contentXmlFileContents .= $styleHelper->getContentXmlFontFaceSectionContent(); $contentXmlFileContents .= $styleHelper->getContentXmlAutomaticStylesSectionContent(count($worksheets)); $contentXmlFileContents .= '<office:body><office:spreadsheet>'; $this->createFileWithContents($this->rootFolder, self::CONTENT_XML_FILE_NAME, $contentXmlFileContents); // Append sheets content to "content.xml" $contentXmlFilePath = $this->rootFolder . '/' . self::CONTENT_XML_FILE_NAME; $contentXmlHandle = fopen($contentXmlFilePath, 'a'); foreach ($worksheets as $worksheet) { // write the "<table:table>" node, with the final sheet's name fwrite($contentXmlHandle, $worksheet->getTableElementStartAsString()); $worksheetFilePath = $worksheet->getWorksheetFilePath(); $this->copyFileContentsToTarget($worksheetFilePath, $contentXmlHandle); fwrite($contentXmlHandle, '</table:table>'); } $contentXmlFileContents = '</office:spreadsheet></office:body></office:document-content>'; fwrite($contentXmlHandle, $contentXmlFileContents); fclose($contentXmlHandle); return $this; } /** * Streams the content of the file at the given path into the target resource. * Depending on which mode the target resource was created with, it will truncate then copy * or append the content to the target file. * * @param string $sourceFilePath Path of the file whose content will be copied * @param resource $targetResource Target resource that will receive the content * @return void */ protected function copyFileContentsToTarget($sourceFilePath, $targetResource) { $sourceHandle = fopen($sourceFilePath, 'r'); stream_copy_to_stream($sourceHandle, $targetResource); fclose($sourceHandle); } /** * Deletes the temporary folder where sheets content was stored. * * @return FileSystemHelper */ public function deleteWorksheetTempFolder() { $this->deleteFolderRecursively($this->sheetsContentTempFolder); return $this; } /** * Creates the "styles.xml" file under the root folder * * @param StyleHelper $styleHelper * @param int $numWorksheets Number of created worksheets * @return FileSystemHelper */ public function createStylesFile($styleHelper, $numWorksheets) { $stylesXmlFileContents = $styleHelper->getStylesXMLFileContent($numWorksheets); $this->createFileWithContents($this->rootFolder, self::STYLES_XML_FILE_NAME, $stylesXmlFileContents); return $this; } /** * Zips the root folder and streams the contents of the zip into the given stream * * @param resource $streamPointer Pointer to the stream to copy the zip * @return void */ public function zipRootFolderAndCopyToStream($streamPointer) { $zipHelper = new ZipHelper($this->rootFolder); // In order to have the file's mime type detected properly, files need to be added // to the zip file in a particular order. // @see http://www.jejik.com/articles/2010/03/how_to_correctly_create_odf_documents_using_zip/ $zipHelper->addUncompressedFileToArchive($this->rootFolder, self::MIMETYPE_FILE_NAME); $zipHelper->addFolderToArchive($this->rootFolder, ZipHelper::EXISTING_FILES_SKIP); $zipHelper->closeArchiveAndCopyToStream($streamPointer); // once the zip is copied, remove it $this->deleteFile($zipHelper->getZipFilePath()); } } Writer/ODS/Helper/StyleHelper.php 0000644 00000027475 15073227061 0012657 0 ustar 00 <?php namespace Box\Spout\Writer\ODS\Helper; use Box\Spout\Writer\Common\Helper\AbstractStyleHelper; use Box\Spout\Writer\Style\BorderPart; /** * Class StyleHelper * This class provides helper functions to manage styles * * @package Box\Spout\Writer\ODS\Helper */ class StyleHelper extends AbstractStyleHelper { /** @var string[] [FONT_NAME] => [] Map whose keys contain all the fonts used */ protected $usedFontsSet = []; /** * Registers the given style as a used style. * Duplicate styles won't be registered more than once. * * @param \Box\Spout\Writer\Style\Style $style The style to be registered * @return \Box\Spout\Writer\Style\Style The registered style, updated with an internal ID. */ public function registerStyle($style) { $this->usedFontsSet[$style->getFontName()] = true; return parent::registerStyle($style); } /** * @return string[] List of used fonts name */ protected function getUsedFonts() { return array_keys($this->usedFontsSet); } /** * Returns the content of the "styles.xml" file, given a list of styles. * * @param int $numWorksheets Number of worksheets created * @return string */ public function getStylesXMLFileContent($numWorksheets) { $content = <<<EOD <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <office:document-styles office:version="1.2" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:msoxl="http://schemas.microsoft.com/office/excel/formula" xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:xlink="http://www.w3.org/1999/xlink"> EOD; $content .= $this->getFontFaceSectionContent(); $content .= $this->getStylesSectionContent(); $content .= $this->getAutomaticStylesSectionContent($numWorksheets); $content .= $this->getMasterStylesSectionContent($numWorksheets); $content .= <<<EOD </office:document-styles> EOD; return $content; } /** * Returns the content of the "<office:font-face-decls>" section, inside "styles.xml" file. * * @return string */ protected function getFontFaceSectionContent() { $content = '<office:font-face-decls>'; foreach ($this->getUsedFonts() as $fontName) { $content .= '<style:font-face style:name="' . $fontName . '" svg:font-family="' . $fontName . '"/>'; } $content .= '</office:font-face-decls>'; return $content; } /** * Returns the content of the "<office:styles>" section, inside "styles.xml" file. * * @return string */ protected function getStylesSectionContent() { $defaultStyle = $this->getDefaultStyle(); return <<<EOD <office:styles> <number:number-style style:name="N0"> <number:number number:min-integer-digits="1"/> </number:number-style> <style:style style:data-style-name="N0" style:family="table-cell" style:name="Default"> <style:table-cell-properties fo:background-color="transparent" style:vertical-align="automatic"/> <style:text-properties fo:color="#{$defaultStyle->getFontColor()}" fo:font-size="{$defaultStyle->getFontSize()}pt" style:font-size-asian="{$defaultStyle->getFontSize()}pt" style:font-size-complex="{$defaultStyle->getFontSize()}pt" style:font-name="{$defaultStyle->getFontName()}" style:font-name-asian="{$defaultStyle->getFontName()}" style:font-name-complex="{$defaultStyle->getFontName()}"/> </style:style> </office:styles> EOD; } /** * Returns the content of the "<office:automatic-styles>" section, inside "styles.xml" file. * * @param int $numWorksheets Number of worksheets created * @return string */ protected function getAutomaticStylesSectionContent($numWorksheets) { $content = '<office:automatic-styles>'; for ($i = 1; $i <= $numWorksheets; $i++) { $content .= <<<EOD <style:page-layout style:name="pm$i"> <style:page-layout-properties style:first-page-number="continue" style:print="objects charts drawings" style:table-centering="none"/> <style:header-style/> <style:footer-style/> </style:page-layout> EOD; } $content .= '</office:automatic-styles>'; return $content; } /** * Returns the content of the "<office:master-styles>" section, inside "styles.xml" file. * * @param int $numWorksheets Number of worksheets created * @return string */ protected function getMasterStylesSectionContent($numWorksheets) { $content = '<office:master-styles>'; for ($i = 1; $i <= $numWorksheets; $i++) { $content .= <<<EOD <style:master-page style:name="mp$i" style:page-layout-name="pm$i"> <style:header/> <style:header-left style:display="false"/> <style:footer/> <style:footer-left style:display="false"/> </style:master-page> EOD; } $content .= '</office:master-styles>'; return $content; } /** * Returns the contents of the "<office:font-face-decls>" section, inside "content.xml" file. * * @return string */ public function getContentXmlFontFaceSectionContent() { $content = '<office:font-face-decls>'; foreach ($this->getUsedFonts() as $fontName) { $content .= '<style:font-face style:name="' . $fontName . '" svg:font-family="' . $fontName . '"/>'; } $content .= '</office:font-face-decls>'; return $content; } /** * Returns the contents of the "<office:automatic-styles>" section, inside "content.xml" file. * * @param int $numWorksheets Number of worksheets created * @return string */ public function getContentXmlAutomaticStylesSectionContent($numWorksheets) { $content = '<office:automatic-styles>'; foreach ($this->getRegisteredStyles() as $style) { $content .= $this->getStyleSectionContent($style); } $content .= <<<EOD <style:style style:family="table-column" style:name="co1"> <style:table-column-properties fo:break-before="auto"/> </style:style> <style:style style:family="table-row" style:name="ro1"> <style:table-row-properties fo:break-before="auto" style:row-height="15pt" style:use-optimal-row-height="true"/> </style:style> EOD; for ($i = 1; $i <= $numWorksheets; $i++) { $content .= <<<EOD <style:style style:family="table" style:master-page-name="mp$i" style:name="ta$i"> <style:table-properties style:writing-mode="lr-tb" table:display="true"/> </style:style> EOD; } $content .= '</office:automatic-styles>'; return $content; } /** * Returns the contents of the "<style:style>" section, inside "<office:automatic-styles>" section * * @param \Box\Spout\Writer\Style\Style $style * @return string */ protected function getStyleSectionContent($style) { $styleIndex = $style->getId() + 1; // 1-based $content = '<style:style style:data-style-name="N0" style:family="table-cell" style:name="ce' . $styleIndex . '" style:parent-style-name="Default">'; $content .= $this->getTextPropertiesSectionContent($style); $content .= $this->getTableCellPropertiesSectionContent($style); $content .= '</style:style>'; return $content; } /** * Returns the contents of the "<style:text-properties>" section, inside "<style:style>" section * * @param \Box\Spout\Writer\Style\Style $style * @return string */ private function getTextPropertiesSectionContent($style) { $content = ''; if ($style->shouldApplyFont()) { $content .= $this->getFontSectionContent($style); } return $content; } /** * Returns the contents of the "<style:text-properties>" section, inside "<style:style>" section * * @param \Box\Spout\Writer\Style\Style $style * @return string */ private function getFontSectionContent($style) { $defaultStyle = $this->getDefaultStyle(); $content = '<style:text-properties'; $fontColor = $style->getFontColor(); if ($fontColor !== $defaultStyle->getFontColor()) { $content .= ' fo:color="#' . $fontColor . '"'; } $fontName = $style->getFontName(); if ($fontName !== $defaultStyle->getFontName()) { $content .= ' style:font-name="' . $fontName . '" style:font-name-asian="' . $fontName . '" style:font-name-complex="' . $fontName . '"'; } $fontSize = $style->getFontSize(); if ($fontSize !== $defaultStyle->getFontSize()) { $content .= ' fo:font-size="' . $fontSize . 'pt" style:font-size-asian="' . $fontSize . 'pt" style:font-size-complex="' . $fontSize . 'pt"'; } if ($style->isFontBold()) { $content .= ' fo:font-weight="bold" style:font-weight-asian="bold" style:font-weight-complex="bold"'; } if ($style->isFontItalic()) { $content .= ' fo:font-style="italic" style:font-style-asian="italic" style:font-style-complex="italic"'; } if ($style->isFontUnderline()) { $content .= ' style:text-underline-style="solid" style:text-underline-type="single"'; } if ($style->isFontStrikethrough()) { $content .= ' style:text-line-through-style="solid"'; } $content .= '/>'; return $content; } /** * Returns the contents of the "<style:table-cell-properties>" section, inside "<style:style>" section * * @param \Box\Spout\Writer\Style\Style $style * @return string */ private function getTableCellPropertiesSectionContent($style) { $content = ''; if ($style->shouldWrapText()) { $content .= $this->getWrapTextXMLContent(); } if ($style->shouldApplyBorder()) { $content .= $this->getBorderXMLContent($style); } if ($style->shouldApplyBackgroundColor()) { $content .= $this->getBackgroundColorXMLContent($style); } return $content; } /** * Returns the contents of the wrap text definition for the "<style:table-cell-properties>" section * * @return string */ private function getWrapTextXMLContent() { return '<style:table-cell-properties fo:wrap-option="wrap" style:vertical-align="automatic"/>'; } /** * Returns the contents of the borders definition for the "<style:table-cell-properties>" section * * @param \Box\Spout\Writer\Style\Style $style * @return string */ private function getBorderXMLContent($style) { $borderProperty = '<style:table-cell-properties %s />'; $borders = array_map(function (BorderPart $borderPart) { return BorderHelper::serializeBorderPart($borderPart); }, $style->getBorder()->getParts()); return sprintf($borderProperty, implode(' ', $borders)); } /** * Returns the contents of the background color definition for the "<style:table-cell-properties>" section * * @param \Box\Spout\Writer\Style\Style $style * @return string */ private function getBackgroundColorXMLContent($style) { return sprintf( '<style:table-cell-properties fo:background-color="#%s"/>', $style->getBackgroundColor() ); } } Writer/ODS/Helper/BorderHelper.php 0000644 00000003577 15073227061 0012771 0 ustar 00 <?php namespace Box\Spout\Writer\ODS\Helper; use Box\Spout\Writer\Style\BorderPart; use Box\Spout\Writer\Style\Border; /** * Class BorderHelper * * The fo:border, fo:border-top, fo:border-bottom, fo:border-left and fo:border-right attributes * specify border properties * http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#__RefHeading__1419780_253892949 * * Example table-cell-properties * * <style:table-cell-properties * fo:border-bottom="0.74pt solid #ffc000" style:diagonal-bl-tr="none" * style:diagonal-tl-br="none" fo:border-left="none" fo:border-right="none" * style:rotation-align="none" fo:border-top="none"/> */ class BorderHelper { /** * Width mappings * * @var array */ protected static $widthMap = [ Border::WIDTH_THIN => '0.75pt', Border::WIDTH_MEDIUM => '1.75pt', Border::WIDTH_THICK => '2.5pt', ]; /** * Style mapping * * @var array */ protected static $styleMap = [ Border::STYLE_SOLID => 'solid', Border::STYLE_DASHED => 'dashed', Border::STYLE_DOTTED => 'dotted', Border::STYLE_DOUBLE => 'double', ]; /** * @param BorderPart $borderPart * @return string */ public static function serializeBorderPart(BorderPart $borderPart) { $definition = 'fo:border-%s="%s"'; if ($borderPart->getStyle() === Border::STYLE_NONE) { $borderPartDefinition = sprintf($definition, $borderPart->getName(), 'none'); } else { $attributes = [ self::$widthMap[$borderPart->getWidth()], self::$styleMap[$borderPart->getStyle()], '#' . $borderPart->getColor(), ]; $borderPartDefinition = sprintf($definition, $borderPart->getName(), implode(' ', $attributes)); } return $borderPartDefinition; } } Writer/ODS/Internal/Worksheet.php 0000644 00000020566 15073227061 0012721 0 ustar 00 <?php namespace Box\Spout\Writer\ODS\Internal; use Box\Spout\Common\Exception\InvalidArgumentException; use Box\Spout\Common\Exception\IOException; use Box\Spout\Common\Helper\StringHelper; use Box\Spout\Writer\Common\Helper\CellHelper; use Box\Spout\Writer\Common\Internal\WorksheetInterface; /** * Class Worksheet * Represents a worksheet within a ODS file. The difference with the Sheet object is * that this class provides an interface to write data * * @package Box\Spout\Writer\ODS\Internal */ class Worksheet implements WorksheetInterface { /** @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\Common\Escaper\ODS Strings escaper */ protected $stringsEscaper; /** @var \Box\Spout\Common\Helper\StringHelper To help with string manipulation */ protected $stringHelper; /** @var Resource Pointer to the temporary sheet data file (e.g. worksheets-temp/sheet1.xml) */ protected $sheetFilePointer; /** @var int Maximum number of columns among all the written rows */ protected $maxNumColumns = 1; /** @var int Index of the last written row */ protected $lastWrittenRowIndex = 0; /** * @param \Box\Spout\Writer\Common\Sheet $externalSheet The associated "external" sheet * @param string $worksheetFilesFolder Temporary folder where the files to create the ODS will be stored * @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing */ public function __construct($externalSheet, $worksheetFilesFolder) { $this->externalSheet = $externalSheet; /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ $this->stringsEscaper = \Box\Spout\Common\Escaper\ODS::getInstance(); $this->worksheetFilePath = $worksheetFilesFolder . '/sheet' . $externalSheet->getIndex() . '.xml'; $this->stringHelper = new StringHelper(); $this->startSheet(); } /** * Prepares the worksheet to accept data * The XML file does not contain the "<table:table>" node as it contains the sheet's name * which may change during the execution of the program. It will be added at the end. * * @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(); } /** * 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 string Path to the temporary sheet content XML file */ public function getWorksheetFilePath() { return $this->worksheetFilePath; } /** * Returns the table XML root node as string. * * @return string <table> node as string */ public function getTableElementStartAsString() { $escapedSheetName = $this->stringsEscaper->escape($this->externalSheet->getName()); $tableStyleName = 'ta' . ($this->externalSheet->getIndex() + 1); $tableElement = '<table:table table:style-name="' . $tableStyleName . '" table:name="' . $escapedSheetName . '">'; $tableElement .= '<table:table-column table:default-cell-style-name="ce1" table:style-name="co1" table:number-columns-repeated="' . $this->maxNumColumns . '"/>'; return $tableElement; } /** * @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; } /** * 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) { // $dataRow can be an associative array. We need to transform // it into a regular array, as we'll use the numeric indexes. $dataRowWithNumericIndexes = array_values($dataRow); $styleIndex = ($style->getId() + 1); // 1-based $cellsCount = count($dataRow); $this->maxNumColumns = max($this->maxNumColumns, $cellsCount); $data = '<table:table-row table:style-name="ro1">'; $currentCellIndex = 0; $nextCellIndex = 1; for ($i = 0; $i < $cellsCount; $i++) { $currentCellValue = $dataRowWithNumericIndexes[$currentCellIndex]; // Using isset here because it is way faster than array_key_exists... if (!isset($dataRowWithNumericIndexes[$nextCellIndex]) || $currentCellValue !== $dataRowWithNumericIndexes[$nextCellIndex]) { $numTimesValueRepeated = ($nextCellIndex - $currentCellIndex); $data .= $this->getCellXML($currentCellValue, $styleIndex, $numTimesValueRepeated); $currentCellIndex = $nextCellIndex; } $nextCellIndex++; } $data .= '</table:table-row>'; $wasWriteSuccessful = fwrite($this->sheetFilePointer, $data); if ($wasWriteSuccessful === false) { throw new IOException("Unable to write data in {$this->worksheetFilePath}"); } // only update the count if the write worked $this->lastWrittenRowIndex++; } /** * Returns the cell XML content, given its value. * * @param mixed $cellValue The value to be written * @param int $styleIndex Index of the used style * @param int $numTimesValueRepeated Number of times the value is consecutively repeated * @return string The cell XML content * @throws \Box\Spout\Common\Exception\InvalidArgumentException If a cell value's type is not supported */ protected function getCellXML($cellValue, $styleIndex, $numTimesValueRepeated) { $data = '<table:table-cell table:style-name="ce' . $styleIndex . '"'; if ($numTimesValueRepeated !== 1) { $data .= ' table:number-columns-repeated="' . $numTimesValueRepeated . '"'; } if (CellHelper::isNonEmptyString($cellValue)) { $data .= ' office:value-type="string" calcext:value-type="string">'; $cellValueLines = explode("\n", $cellValue); foreach ($cellValueLines as $cellValueLine) { $data .= '<text:p>' . $this->stringsEscaper->escape($cellValueLine) . '</text:p>'; } $data .= '</table:table-cell>'; } else if (CellHelper::isBoolean($cellValue)) { $data .= ' office:value-type="boolean" calcext:value-type="boolean" office:boolean-value="' . $cellValue . '">'; $data .= '<text:p>' . $cellValue . '</text:p>'; $data .= '</table:table-cell>'; } else if (CellHelper::isNumeric($cellValue)) { $data .= ' office:value-type="float" calcext:value-type="float" office:value="' . $cellValue . '">'; $data .= '<text:p>' . $cellValue . '</text:p>'; $data .= '</table:table-cell>'; } else if (empty($cellValue)) { $data .= '/>'; } else { throw new InvalidArgumentException('Trying to add a value with an unsupported type: ' . gettype($cellValue)); } return $data; } /** * Closes the worksheet * * @return void */ public function close() { if (!is_resource($this->sheetFilePointer)) { return; } fclose($this->sheetFilePointer); } } Writer/ODS/Internal/Workbook.php 0000644 00000007517 15073227061 0012544 0 ustar 00 <?php namespace Box\Spout\Writer\ODS\Internal; use Box\Spout\Writer\Common\Internal\AbstractWorkbook; use Box\Spout\Writer\ODS\Helper\FileSystemHelper; use Box\Spout\Writer\ODS\Helper\StyleHelper; use Box\Spout\Writer\Common\Sheet; /** * Class Workbook * Represents a workbook within a ODS file. * It provides the functions to work with worksheets. * * @package Box\Spout\Writer\ODS\Internal */ class Workbook extends AbstractWorkbook { /** * Maximum number of rows a ODS sheet can contain * @see https://ask.libreoffice.org/en/question/8631/upper-limit-to-number-of-rows-in-calc/ */ protected static $maxRowsPerWorksheet = 1048576; /** @var \Box\Spout\Writer\ODS\Helper\FileSystemHelper Helper to perform file system operations */ protected $fileSystemHelper; /** @var \Box\Spout\Writer\ODS\Helper\StyleHelper Helper to apply styles */ protected $styleHelper; /** * @param string $tempFolder * @param bool $shouldCreateNewSheetsAutomatically * @param \Box\Spout\Writer\Style\Style $defaultRowStyle * @throws \Box\Spout\Common\Exception\IOException If unable to create at least one of the base folders */ public function __construct($tempFolder, $shouldCreateNewSheetsAutomatically, $defaultRowStyle) { parent::__construct($shouldCreateNewSheetsAutomatically, $defaultRowStyle); $this->fileSystemHelper = new FileSystemHelper($tempFolder); $this->fileSystemHelper->createBaseFilesAndFolders(); $this->styleHelper = new StyleHelper($defaultRowStyle); } /** * @return \Box\Spout\Writer\ODS\Helper\StyleHelper Helper to apply styles to ODS files */ protected function getStyleHelper() { return $this->styleHelper; } /** * @return int Maximum number of rows/columns a sheet can contain */ protected function getMaxRowsPerWorksheet() { return self::$maxRowsPerWorksheet; } /** * Creates a new sheet in the workbook. The current sheet remains unchanged. * * @return Worksheet The created sheet * @throws \Box\Spout\Common\Exception\IOException If unable to open the sheet for writing */ public function addNewSheet() { $newSheetIndex = count($this->worksheets); $sheet = new Sheet($newSheetIndex, $this->internalId); $sheetsContentTempFolder = $this->fileSystemHelper->getSheetsContentTempFolder(); $worksheet = new Worksheet($sheet, $sheetsContentTempFolder); $this->worksheets[] = $worksheet; return $worksheet; } /** * Closes the workbook and all its associated sheets. * All the necessary files are written to disk and zipped together to create the ODS file. * All the temporary files are then deleted. * * @param resource $finalFilePointer Pointer to the ODS that will be created * @return void */ public function close($finalFilePointer) { /** @var Worksheet[] $worksheets */ $worksheets = $this->worksheets; $numWorksheets = count($worksheets); foreach ($worksheets as $worksheet) { $worksheet->close(); } // Finish creating all the necessary files before zipping everything together $this->fileSystemHelper ->createContentFile($worksheets, $this->styleHelper) ->deleteWorksheetTempFolder() ->createStylesFile($this->styleHelper, $numWorksheets) ->zipRootFolderAndCopyToStream($finalFilePointer); $this->cleanupTempFolder(); } /** * Deletes the root folder created in the temp folder and all its contents. * * @return void */ protected function cleanupTempFolder() { $xlsxRootFolder = $this->fileSystemHelper->getRootFolder(); $this->fileSystemHelper->deleteFolderRecursively($xlsxRootFolder); } } Writer/ODS/Writer.php 0000644 00000006002 15073227061 0010433 0 ustar 00 <?php namespace Box\Spout\Writer\ODS; use Box\Spout\Writer\AbstractMultiSheetsWriter; use Box\Spout\Writer\Common; use Box\Spout\Writer\ODS\Internal\Workbook; /** * Class Writer * This class provides base support to write data to ODS files * * @package Box\Spout\Writer\ODS */ class Writer extends AbstractMultiSheetsWriter { /** @var string Content-Type value for the header */ protected static $headerContentType = 'application/vnd.oasis.opendocument.spreadsheet'; /** @var string Temporary folder where the files to create the ODS will be stored */ protected $tempFolder; /** @var Internal\Workbook The workbook for the ODS file */ protected $book; /** * Sets a custom temporary folder for creating intermediate files/folders. * This must be set before opening the writer. * * @api * @param string $tempFolder Temporary folder where the files to create the ODS will be stored * @return Writer * @throws \Box\Spout\Writer\Exception\WriterAlreadyOpenedException If the writer was already opened */ public function setTempFolder($tempFolder) { $this->throwIfWriterAlreadyOpened('Writer must be configured before opening it.'); $this->tempFolder = $tempFolder; return $this; } /** * Configures the write and sets the current sheet pointer to a new sheet. * * @return void * @throws \Box\Spout\Common\Exception\IOException If unable to open the file for writing */ protected function openWriter() { $tempFolder = ($this->tempFolder) ? : sys_get_temp_dir(); $this->book = new Workbook($tempFolder, $this->shouldCreateNewSheetsAutomatically, $this->defaultRowStyle); $this->book->addNewSheetAndMakeItCurrent(); } /** * @return Internal\Workbook The workbook representing the file to be written */ protected function getWorkbook() { return $this->book; } /** * Adds data to the currently opened writer. * If shouldCreateNewSheetsAutomatically option is set to true, it will handle pagination * with the creation of new worksheets if one worksheet has reached its maximum capicity. * * @param array $dataRow Array containing data to be written. * Example $dataRow = ['data1', 1234, null, '', 'data5']; * @param \Box\Spout\Writer\Style\Style $style Style to be applied to the row. * @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, $style) { $this->throwIfBookIsNotAvailable(); $this->book->addRowToCurrentWorksheet($dataRow, $style); } /** * Closes the writer, preventing any additional writing. * * @return void */ protected function closeWriter() { if ($this->book) { $this->book->close($this->filePointer); } } } Writer/AbstractMultiSheetsWriter.php 0000644 00000007723 15073227061 0013674 0 ustar 00 <?php namespace Box\Spout\Writer; use Box\Spout\Writer\Exception\WriterNotOpenedException; /** * Class AbstractMultiSheetsWriter * * @package Box\Spout\Writer * @abstract */ abstract class AbstractMultiSheetsWriter extends AbstractWriter { /** @var bool Whether new sheets should be automatically created when the max rows limit per sheet is reached */ protected $shouldCreateNewSheetsAutomatically = true; /** * @return Common\Internal\WorkbookInterface The workbook representing the file to be written */ abstract protected function getWorkbook(); /** * Sets whether new sheets should be automatically created when the max rows limit per sheet is reached. * This must be set before opening the writer. * * @api * @param bool $shouldCreateNewSheetsAutomatically Whether new sheets should be automatically created when the max rows limit per sheet is reached * @return AbstractMultiSheetsWriter * @throws \Box\Spout\Writer\Exception\WriterAlreadyOpenedException If the writer was already opened */ public function setShouldCreateNewSheetsAutomatically($shouldCreateNewSheetsAutomatically) { $this->throwIfWriterAlreadyOpened('Writer must be configured before opening it.'); $this->shouldCreateNewSheetsAutomatically = $shouldCreateNewSheetsAutomatically; return $this; } /** * Returns all the workbook's sheets * * @api * @return Common\Sheet[] All the workbook's sheets * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet */ public function getSheets() { $this->throwIfBookIsNotAvailable(); $externalSheets = []; $worksheets = $this->getWorkbook()->getWorksheets(); /** @var Common\Internal\WorksheetInterface $worksheet */ foreach ($worksheets as $worksheet) { $externalSheets[] = $worksheet->getExternalSheet(); } return $externalSheets; } /** * Creates a new sheet and make it the current sheet. The data will now be written to this sheet. * * @api * @return Common\Sheet The created sheet * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet */ public function addNewSheetAndMakeItCurrent() { $this->throwIfBookIsNotAvailable(); $worksheet = $this->getWorkbook()->addNewSheetAndMakeItCurrent(); return $worksheet->getExternalSheet(); } /** * Returns the current sheet * * @api * @return Common\Sheet The current sheet * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet */ public function getCurrentSheet() { $this->throwIfBookIsNotAvailable(); return $this->getWorkbook()->getCurrentWorksheet()->getExternalSheet(); } /** * Sets the given sheet as the current one. New data will be written to this sheet. * The writing will resume where it stopped (i.e. data won't be truncated). * * @api * @param Common\Sheet $sheet The sheet to set as current * @return void * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet * @throws \Box\Spout\Writer\Exception\SheetNotFoundException If the given sheet does not exist in the workbook */ public function setCurrentSheet($sheet) { $this->throwIfBookIsNotAvailable(); $this->getWorkbook()->setCurrentSheet($sheet); } /** * Checks if the book has been created. Throws an exception if not created yet. * * @return void * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the book is not created yet */ protected function throwIfBookIsNotAvailable() { if (!$this->getWorkbook()) { throw new WriterNotOpenedException('The writer must be opened before performing this action.'); } } } Common/Singleton.php 0000644 00000001302 15073227061 0010446 0 ustar 00 <?php namespace Box\Spout\Common; /** * Class Singleton * Defines a class as a singleton. * * @package Box\Spout\Common */ trait Singleton { protected static $instance; /** * @return static */ final public static function getInstance() { return isset(static::$instance) ? static::$instance : static::$instance = new static; } /** * Singleton constructor. */ final private function __construct() { $this->init(); } /** * Initializes the singleton * @return void */ protected function init() {} final private function __wakeup() {} final private function __clone() {} } Common/Helper/FileSystemHelper.php 0000644 00000011323 15073227061 0013153 0 ustar 00 <?php namespace Box\Spout\Common\Helper; use Box\Spout\Common\Exception\IOException; /** * Class FileSystemHelper * This class provides helper functions to help with the file system operations * like files/folders creation & deletion * * @package Box\Spout\Common\Helper */ class FileSystemHelper { /** @var string Real path of the base folder where all the I/O can occur */ protected $baseFolderRealPath; /** * @param string $baseFolderPath The path of the base folder where all the I/O can occur */ public function __construct($baseFolderPath) { $this->baseFolderRealPath = realpath($baseFolderPath); } /** * Creates an empty folder with the given name under the given parent folder. * * @param string $parentFolderPath The parent folder path under which the folder is going to be created * @param string $folderName The name of the folder to create * @return string Path of the created folder * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder or if the folder path is not inside of the base folder */ public function createFolder($parentFolderPath, $folderName) { $this->throwIfOperationNotInBaseFolder($parentFolderPath); $folderPath = $parentFolderPath . '/' . $folderName; $wasCreationSuccessful = mkdir($folderPath, 0777, true); if (!$wasCreationSuccessful) { throw new IOException("Unable to create folder: $folderPath"); } return $folderPath; } /** * Creates a file with the given name and content in the given folder. * The parent folder must exist. * * @param string $parentFolderPath The parent folder path where the file is going to be created * @param string $fileName The name of the file to create * @param string $fileContents The contents of the file to create * @return string Path of the created file * @throws \Box\Spout\Common\Exception\IOException If unable to create the file or if the file path is not inside of the base folder */ public function createFileWithContents($parentFolderPath, $fileName, $fileContents) { $this->throwIfOperationNotInBaseFolder($parentFolderPath); $filePath = $parentFolderPath . '/' . $fileName; $wasCreationSuccessful = file_put_contents($filePath, $fileContents); if ($wasCreationSuccessful === false) { throw new IOException("Unable to create file: $filePath"); } return $filePath; } /** * Delete the file at the given path * * @param string $filePath Path of the file to delete * @return void * @throws \Box\Spout\Common\Exception\IOException If the file path is not inside of the base folder */ public function deleteFile($filePath) { $this->throwIfOperationNotInBaseFolder($filePath); if (file_exists($filePath) && is_file($filePath)) { unlink($filePath); } } /** * Delete the folder at the given path as well as all its contents * * @param string $folderPath Path of the folder to delete * @return void * @throws \Box\Spout\Common\Exception\IOException If the folder path is not inside of the base folder */ public function deleteFolderRecursively($folderPath) { $this->throwIfOperationNotInBaseFolder($folderPath); $itemIterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($folderPath, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::CHILD_FIRST ); foreach ($itemIterator as $item) { if ($item->isDir()) { rmdir($item->getPathname()); } else { unlink($item->getPathname()); } } rmdir($folderPath); } /** * All I/O operations must occur inside the base folder, for security reasons. * This function will throw an exception if the folder where the I/O operation * should occur is not inside the base folder. * * @param string $operationFolderPath The path of the folder where the I/O operation should occur * @return void * @throws \Box\Spout\Common\Exception\IOException If the folder where the I/O operation should occur is not inside the base folder */ protected function throwIfOperationNotInBaseFolder($operationFolderPath) { $operationFolderRealPath = realpath($operationFolderPath); $isInBaseFolder = (strpos($operationFolderRealPath, $this->baseFolderRealPath) === 0); if (!$isInBaseFolder) { throw new IOException("Cannot perform I/O operation outside of the base folder: {$this->baseFolderRealPath}"); } } } Common/Helper/GlobalFunctionsHelper.php 0000644 00000016575 15073227061 0014176 0 ustar 00 <?php namespace Box\Spout\Common\Helper; /** * Class GlobalFunctionsHelper * This class wraps global functions to facilitate testing * * @codeCoverageIgnore * * @package Box\Spout\Common\Helper */ class GlobalFunctionsHelper { /** * Wrapper around global function fopen() * @see fopen() * * @param string $fileName * @param string $mode * @return resource|bool */ public function fopen($fileName, $mode) { return fopen($fileName, $mode); } /** * Wrapper around global function fgets() * @see fgets() * * @param resource $handle * @param int|void $length * @return string */ public function fgets($handle, $length = null) { return fgets($handle, $length); } /** * Wrapper around global function fputs() * @see fputs() * * @param resource $handle * @param string $string * @return int */ public function fputs($handle, $string) { return fputs($handle, $string); } /** * Wrapper around global function fflush() * @see fflush() * * @param resource $handle * @return bool */ public function fflush($handle) { return fflush($handle); } /** * Wrapper around global function fseek() * @see fseek() * * @param resource $handle * @param int $offset * @return int */ public function fseek($handle, $offset) { return fseek($handle, $offset); } /** * Wrapper around global function fgetcsv() * @see fgetcsv() * * @param resource $handle * @param int|void $length * @param string|void $delimiter * @param string|void $enclosure * @return array */ public function fgetcsv($handle, $length = null, $delimiter = null, $enclosure = null) { return fgetcsv($handle, $length, $delimiter, $enclosure); } /** * Wrapper around global function fputcsv() * @see fputcsv() * * @param resource $handle * @param array $fields * @param string|void $delimiter * @param string|void $enclosure * @return int */ public function fputcsv($handle, array $fields, $delimiter = null, $enclosure = null) { return fputcsv($handle, $fields, $delimiter, $enclosure); } /** * Wrapper around global function fwrite() * @see fwrite() * * @param resource $handle * @param string $string * @return int */ public function fwrite($handle, $string) { return fwrite($handle, $string); } /** * Wrapper around global function fclose() * @see fclose() * * @param resource $handle * @return bool */ public function fclose($handle) { return fclose($handle); } /** * Wrapper around global function rewind() * @see rewind() * * @param resource $handle * @return bool */ public function rewind($handle) { return rewind($handle); } /** * Wrapper around global function file_exists() * @see file_exists() * * @param string $fileName * @return bool */ public function file_exists($fileName) { return file_exists($fileName); } /** * Wrapper around global function file_get_contents() * @see file_get_contents() * * @param string $filePath * @return string */ public function file_get_contents($filePath) { $realFilePath = $this->convertToUseRealPath($filePath); return file_get_contents($realFilePath); } /** * Updates the given file path to use a real path. * This is to avoid issues on some Windows setup. * * @param string $filePath File path * @return string The file path using a real path */ protected function convertToUseRealPath($filePath) { $realFilePath = $filePath; if ($this->isZipStream($filePath)) { if (preg_match('/zip:\/\/(.*)#(.*)/', $filePath, $matches)) { $documentPath = $matches[1]; $documentInsideZipPath = $matches[2]; $realFilePath = 'zip://' . realpath($documentPath) . '#' . $documentInsideZipPath; } } else { $realFilePath = realpath($filePath); } return $realFilePath; } /** * Returns whether the given path is a zip stream. * * @param string $path Path pointing to a document * @return bool TRUE if path is a zip stream, FALSE otherwise */ protected function isZipStream($path) { return (strpos($path, 'zip://') === 0); } /** * Wrapper around global function feof() * @see feof() * * @param resource * @return bool */ public function feof($handle) { return feof($handle); } /** * Wrapper around global function is_readable() * @see is_readable() * * @param string $fileName * @return bool */ public function is_readable($fileName) { return is_readable($fileName); } /** * Wrapper around global function basename() * @see basename() * * @param string $path * @param string|void $suffix * @return string */ public function basename($path, $suffix = null) { return basename($path, $suffix); } /** * Wrapper around global function header() * @see header() * * @param string $string * @return void */ public function header($string) { header($string); } /** * Wrapper around global function ob_end_clean() * @see ob_end_clean() * * @return void */ public function ob_end_clean() { if (ob_get_length() > 0) { ob_end_clean(); } } /** * Wrapper around global function iconv() * @see iconv() * * @param string $string The string to be converted * @param string $sourceEncoding The encoding of the source string * @param string $targetEncoding The encoding the source string should be converted to * @return string|bool the converted string or FALSE on failure. */ public function iconv($string, $sourceEncoding, $targetEncoding) { return iconv($sourceEncoding, $targetEncoding, $string); } /** * Wrapper around global function mb_convert_encoding() * @see mb_convert_encoding() * * @param string $string The string to be converted * @param string $sourceEncoding The encoding of the source string * @param string $targetEncoding The encoding the source string should be converted to * @return string|bool the converted string or FALSE on failure. */ public function mb_convert_encoding($string, $sourceEncoding, $targetEncoding) { return mb_convert_encoding($string, $targetEncoding, $sourceEncoding); } /** * Wrapper around global function stream_get_wrappers() * @see stream_get_wrappers() * * @return array */ public function stream_get_wrappers() { return stream_get_wrappers(); } /** * Wrapper around global function function_exists() * @see function_exists() * * @param string $functionName * @return bool */ public function function_exists($functionName) { return function_exists($functionName); } } Common/Helper/EncodingHelper.php 0000644 00000014676 15073227061 0012633 0 ustar 00 <?php namespace Box\Spout\Common\Helper; use Box\Spout\Common\Exception\EncodingConversionException; /** * Class EncodingHelper * This class provides helper functions to work with encodings. * * @package Box\Spout\Common\Helper */ class EncodingHelper { /** Definition of the encodings that can have a BOM */ const ENCODING_UTF8 = 'UTF-8'; const ENCODING_UTF16_LE = 'UTF-16LE'; const ENCODING_UTF16_BE = 'UTF-16BE'; const ENCODING_UTF32_LE = 'UTF-32LE'; const ENCODING_UTF32_BE = 'UTF-32BE'; /** Definition of the BOMs for the different encodings */ const BOM_UTF8 = "\xEF\xBB\xBF"; const BOM_UTF16_LE = "\xFF\xFE"; const BOM_UTF16_BE = "\xFE\xFF"; const BOM_UTF32_LE = "\xFF\xFE\x00\x00"; const BOM_UTF32_BE = "\x00\x00\xFE\xFF"; /** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */ protected $globalFunctionsHelper; /** @var array Map representing the encodings supporting BOMs (key) and their associated BOM (value) */ protected $supportedEncodingsWithBom; /** * @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper */ public function __construct($globalFunctionsHelper) { $this->globalFunctionsHelper = $globalFunctionsHelper; $this->supportedEncodingsWithBom = [ self::ENCODING_UTF8 => self::BOM_UTF8, self::ENCODING_UTF16_LE => self::BOM_UTF16_LE, self::ENCODING_UTF16_BE => self::BOM_UTF16_BE, self::ENCODING_UTF32_LE => self::BOM_UTF32_LE, self::ENCODING_UTF32_BE => self::BOM_UTF32_BE, ]; } /** * Returns the number of bytes to use as offset in order to skip the BOM. * * @param resource $filePointer Pointer to the file to check * @param string $encoding Encoding of the file to check * @return int Bytes offset to apply to skip the BOM (0 means no BOM) */ public function getBytesOffsetToSkipBOM($filePointer, $encoding) { $byteOffsetToSkipBom = 0; if ($this->hasBOM($filePointer, $encoding)) { $bomUsed = $this->supportedEncodingsWithBom[$encoding]; // we skip the N first bytes $byteOffsetToSkipBom = strlen($bomUsed); } return $byteOffsetToSkipBom; } /** * Returns whether the file identified by the given pointer has a BOM. * * @param resource $filePointer Pointer to the file to check * @param string $encoding Encoding of the file to check * @return bool TRUE if the file has a BOM, FALSE otherwise */ protected function hasBOM($filePointer, $encoding) { $hasBOM = false; $this->globalFunctionsHelper->rewind($filePointer); if (array_key_exists($encoding, $this->supportedEncodingsWithBom)) { $potentialBom = $this->supportedEncodingsWithBom[$encoding]; $numBytesInBom = strlen($potentialBom); $hasBOM = ($this->globalFunctionsHelper->fgets($filePointer, $numBytesInBom + 1) === $potentialBom); } return $hasBOM; } /** * Attempts to convert a non UTF-8 string into UTF-8. * * @param string $string Non UTF-8 string to be converted * @param string $sourceEncoding The encoding used to encode the source string * @return string The converted, UTF-8 string * @throws \Box\Spout\Common\Exception\EncodingConversionException If conversion is not supported or if the conversion failed */ public function attemptConversionToUTF8($string, $sourceEncoding) { return $this->attemptConversion($string, $sourceEncoding, self::ENCODING_UTF8); } /** * Attempts to convert a UTF-8 string into the given encoding. * * @param string $string UTF-8 string to be converted * @param string $targetEncoding The encoding the string should be re-encoded into * @return string The converted string, encoded with the given encoding * @throws \Box\Spout\Common\Exception\EncodingConversionException If conversion is not supported or if the conversion failed */ public function attemptConversionFromUTF8($string, $targetEncoding) { return $this->attemptConversion($string, self::ENCODING_UTF8, $targetEncoding); } /** * Attempts to convert the given string to the given encoding. * Depending on what is installed on the server, we will try to iconv or mbstring. * * @param string $string string to be converted * @param string $sourceEncoding The encoding used to encode the source string * @param string $targetEncoding The encoding the string should be re-encoded into * @return string The converted string, encoded with the given encoding * @throws \Box\Spout\Common\Exception\EncodingConversionException If conversion is not supported or if the conversion failed */ protected function attemptConversion($string, $sourceEncoding, $targetEncoding) { // if source and target encodings are the same, it's a no-op if ($sourceEncoding === $targetEncoding) { return $string; } $convertedString = null; if ($this->canUseIconv()) { $convertedString = $this->globalFunctionsHelper->iconv($string, $sourceEncoding, $targetEncoding); } else if ($this->canUseMbString()) { $convertedString = $this->globalFunctionsHelper->mb_convert_encoding($string, $sourceEncoding, $targetEncoding); } else { throw new EncodingConversionException("The conversion from $sourceEncoding to $targetEncoding is not supported. Please install \"iconv\" or \"PHP Intl\"."); } if ($convertedString === false) { throw new EncodingConversionException("The conversion from $sourceEncoding to $targetEncoding failed."); } return $convertedString; } /** * Returns whether "iconv" can be used. * * @return bool TRUE if "iconv" is available and can be used, FALSE otherwise */ protected function canUseIconv() { return $this->globalFunctionsHelper->function_exists('iconv'); } /** * Returns whether "mb_string" functions can be used. * These functions come with the PHP Intl package. * * @return bool TRUE if "mb_string" functions are available and can be used, FALSE otherwise */ protected function canUseMbString() { return $this->globalFunctionsHelper->function_exists('mb_convert_encoding'); } } Common/Helper/StringHelper.php 0000644 00000004141 15073227061 0012335 0 ustar 00 <?php namespace Box\Spout\Common\Helper; /** * Class StringHelper * This class provides helper functions to work with strings and multibyte strings. * * @codeCoverageIgnore * * @package Box\Spout\Common\Helper */ class StringHelper { /** @var bool Whether the mbstring extension is loaded */ protected $hasMbstringSupport; /** * */ public function __construct() { $this->hasMbstringSupport = extension_loaded('mbstring'); } /** * Returns the length of the given string. * It uses the multi-bytes function is available. * @see strlen * @see mb_strlen * * @param string $string * @return int */ public function getStringLength($string) { return $this->hasMbstringSupport ? mb_strlen($string) : strlen($string); } /** * Returns the position of the first occurrence of the given character/substring within the given string. * It uses the multi-bytes function is available. * @see strpos * @see mb_strpos * * @param string $char Needle * @param string $string Haystack * @return int Char/substring's first occurrence position within the string if found (starts at 0) or -1 if not found */ public function getCharFirstOccurrencePosition($char, $string) { $position = $this->hasMbstringSupport ? mb_strpos($string, $char) : strpos($string, $char); return ($position !== false) ? $position : -1; } /** * Returns the position of the last occurrence of the given character/substring within the given string. * It uses the multi-bytes function is available. * @see strrpos * @see mb_strrpos * * @param string $char Needle * @param string $string Haystack * @return int Char/substring's last occurrence position within the string if found (starts at 0) or -1 if not found */ public function getCharLastOccurrencePosition($char, $string) { $position = $this->hasMbstringSupport ? mb_strrpos($string, $char) : strrpos($string, $char); return ($position !== false) ? $position : -1; } } Common/Exception/IOException.php 0000644 00000000252 15073227061 0012633 0 ustar 00 <?php namespace Box\Spout\Common\Exception; /** * Class IOException * * @api * @package Box\Spout\Common\Exception */ class IOException extends SpoutException { } Common/Exception/EncodingConversionException.php 0000644 00000000312 15073227061 0016115 0 ustar 00 <?php namespace Box\Spout\Common\Exception; /** * Class EncodingConversionException * * @api * @package Box\Spout\Common\Exception */ class EncodingConversionException extends SpoutException { } Common/Exception/UnsupportedTypeException.php 0000644 00000000304 15073227061 0015514 0 ustar 00 <?php namespace Box\Spout\Common\Exception; /** * Class UnsupportedTypeException * * @api * @package Box\Spout\Common\Exception */ class UnsupportedTypeException extends SpoutException { } Common/Exception/InvalidArgumentException.php 0000644 00000000304 15073227061 0015413 0 ustar 00 <?php namespace Box\Spout\Common\Exception; /** * Class InvalidArgumentException * * @api * @package Box\Spout\Common\Exception */ class InvalidArgumentException extends SpoutException { } Common/Exception/SpoutException.php 0000644 00000000272 15073227061 0013440 0 ustar 00 <?php namespace Box\Spout\Common\Exception; /** * Class SpoutException * * @package Box\Spout\Common\Exception * @abstract */ abstract class SpoutException extends \Exception { } Common/Escaper/ODS.php 0000644 00000004600 15073227061 0010517 0 ustar 00 <?php namespace Box\Spout\Common\Escaper; use Box\Spout\Common\Singleton; /** * Class ODS * Provides functions to escape and unescape data for ODS files * * @package Box\Spout\Common\Escaper */ class ODS implements EscaperInterface { use Singleton; /** * Escapes the given string to make it compatible with XLSX * * @param string $string The string to escape * @return string The escaped string */ public function escape($string) { if (defined('ENT_DISALLOWED')) { // 'ENT_DISALLOWED' ensures that invalid characters in the given document type are replaced. // Otherwise control characters like a vertical tab "\v" will make the XML document unreadable by the XML processor // @link https://github.com/box/spout/issues/329 $replacedString = htmlspecialchars($string, ENT_NOQUOTES | ENT_DISALLOWED); } else { // We are on hhvm or any other engine that does not support ENT_DISALLOWED. // // @NOTE: Using ENT_NOQUOTES as only XML entities ('<', '>', '&') need to be encoded. // Single and double quotes can be left as is. $escapedString = htmlspecialchars($string, ENT_NOQUOTES); // control characters values are from 0 to 1F (hex values) in the ASCII table // some characters should not be escaped though: "\t", "\r" and "\n". $regexPattern = '[\x00-\x08' . // skipping "\t" (0x9) and "\n" (0xA) '\x0B-\x0C' . // skipping "\r" (0xD) '\x0E-\x1F]'; $replacedString = preg_replace("/$regexPattern/", '�', $escapedString); } return $replacedString; } /** * Unescapes the given string to make it compatible with XLSX * * @param string $string The string to unescape * @return string The unescaped string */ public function unescape($string) { // ============== // = WARNING = // ============== // It is assumed that the given string has already had its XML entities decoded. // This is true if the string is coming from a DOMNode (as DOMNode already decode XML entities on creation). // Therefore there is no need to call "htmlspecialchars_decode()". return $string; } } Common/Escaper/XLSX.php 0000644 00000015207 15073227061 0010675 0 ustar 00 <?php namespace Box\Spout\Common\Escaper; use Box\Spout\Common\Singleton; /** * Class XLSX * Provides functions to escape and unescape data for XLSX files * * @package Box\Spout\Common\Escaper */ class XLSX implements EscaperInterface { use Singleton; /** @var string Regex pattern to detect control characters that need to be escaped */ protected $escapableControlCharactersPattern; /** @var string[] Map containing control characters to be escaped (key) and their escaped value (value) */ protected $controlCharactersEscapingMap; /** @var string[] Map containing control characters to be escaped (value) and their escaped value (key) */ protected $controlCharactersEscapingReverseMap; /** * Initializes the singleton instance */ protected function init() { $this->escapableControlCharactersPattern = $this->getEscapableControlCharactersPattern(); $this->controlCharactersEscapingMap = $this->getControlCharactersEscapingMap(); $this->controlCharactersEscapingReverseMap = array_flip($this->controlCharactersEscapingMap); } /** * Escapes the given string to make it compatible with XLSX * * @param string $string The string to escape * @return string The escaped string */ public function escape($string) { $escapedString = $this->escapeControlCharacters($string); // @NOTE: Using ENT_NOQUOTES as only XML entities ('<', '>', '&') need to be encoded. // Single and double quotes can be left as is. $escapedString = htmlspecialchars($escapedString, ENT_NOQUOTES); return $escapedString; } /** * Unescapes the given string to make it compatible with XLSX * * @param string $string The string to unescape * @return string The unescaped string */ public function unescape($string) { // ============== // = WARNING = // ============== // It is assumed that the given string has already had its XML entities decoded. // This is true if the string is coming from a DOMNode (as DOMNode already decode XML entities on creation). // Therefore there is no need to call "htmlspecialchars_decode()". $unescapedString = $this->unescapeControlCharacters($string); return $unescapedString; } /** * @return string Regex pattern containing all escapable control characters */ protected function getEscapableControlCharactersPattern() { // control characters values are from 0 to 1F (hex values) in the ASCII table // some characters should not be escaped though: "\t", "\r" and "\n". return '[\x00-\x08' . // skipping "\t" (0x9) and "\n" (0xA) '\x0B-\x0C' . // skipping "\r" (0xD) '\x0E-\x1F]'; } /** * Builds the map containing control characters to be escaped * mapped to their escaped values. * "\t", "\r" and "\n" don't need to be escaped. * * NOTE: the logic has been adapted from the XlsxWriter library (BSD License) * @link https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89 * * @return string[] */ protected function getControlCharactersEscapingMap() { $controlCharactersEscapingMap = []; // control characters values are from 0 to 1F (hex values) in the ASCII table for ($charValue = 0x00; $charValue <= 0x1F; $charValue++) { $character = chr($charValue); if (preg_match("/{$this->escapableControlCharactersPattern}/", $character)) { $charHexValue = dechex($charValue); $escapedChar = '_x' . sprintf('%04s' , strtoupper($charHexValue)) . '_'; $controlCharactersEscapingMap[$escapedChar] = $character; } } return $controlCharactersEscapingMap; } /** * Converts PHP control characters from the given string to OpenXML escaped control characters * * Excel escapes control characters with _xHHHH_ and also escapes any * literal strings of that type by encoding the leading underscore. * So "\0" -> _x0000_ and "_x0000_" -> _x005F_x0000_. * * NOTE: the logic has been adapted from the XlsxWriter library (BSD License) * @link https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89 * * @param string $string String to escape * @return string */ protected function escapeControlCharacters($string) { $escapedString = $this->escapeEscapeCharacter($string); // if no control characters if (!preg_match("/{$this->escapableControlCharactersPattern}/", $escapedString)) { return $escapedString; } return preg_replace_callback("/({$this->escapableControlCharactersPattern})/", function($matches) { return $this->controlCharactersEscapingReverseMap[$matches[0]]; }, $escapedString); } /** * Escapes the escape character: "_x0000_" -> "_x005F_x0000_" * * @param string $string String to escape * @return string The escaped string */ protected function escapeEscapeCharacter($string) { return preg_replace('/_(x[\dA-F]{4})_/', '_x005F_$1_', $string); } /** * Converts OpenXML escaped control characters from the given string to PHP control characters * * Excel escapes control characters with _xHHHH_ and also escapes any * literal strings of that type by encoding the leading underscore. * So "_x0000_" -> "\0" and "_x005F_x0000_" -> "_x0000_" * * NOTE: the logic has been adapted from the XlsxWriter library (BSD License) * @link https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89 * * @param string $string String to unescape * @return string */ protected function unescapeControlCharacters($string) { $unescapedString = $string; foreach ($this->controlCharactersEscapingMap as $escapedCharValue => $charValue) { // only unescape characters that don't contain the escaped escape character for now $unescapedString = preg_replace("/(?<!_x005F)($escapedCharValue)/", $charValue, $unescapedString); } return $this->unescapeEscapeCharacter($unescapedString); } /** * Unecapes the escape character: "_x005F_x0000_" => "_x0000_" * * @param string $string String to unescape * @return string The unescaped string */ protected function unescapeEscapeCharacter($string) { return preg_replace('/_x005F(_x[\dA-F]{4}_)/', '$1', $string); } } Common/Escaper/EscaperInterface.php 0000644 00000001116 15073227061 0013274 0 ustar 00 <?php namespace Box\Spout\Common\Escaper; /** * Interface EscaperInterface * * @package Box\Spout\Common\Escaper */ interface EscaperInterface { /** * Escapes the given string to make it compatible with PHP * * @param string $string The string to escape * @return string The escaped string */ public function escape($string); /** * Unescapes the given string to make it compatible with PHP * * @param string $string The string to unescape * @return string The unescaped string */ public function unescape($string); } Common/Escaper/CSV.php 0000644 00000001422 15073227061 0010524 0 ustar 00 <?php namespace Box\Spout\Common\Escaper; /** * Class CSV * Provides functions to escape and unescape data for CSV files * * @package Box\Spout\Common\Escaper */ class CSV implements EscaperInterface { /** * Escapes the given string to make it compatible with CSV * * @codeCoverageIgnore * * @param string $string The string to escape * @return string The escaped string */ public function escape($string) { return $string; } /** * Unescapes the given string to make it compatible with CSV * * @codeCoverageIgnore * * @param string $string The string to unescape * @return string The unescaped string */ public function unescape($string) { return $string; } } Common/Type.php 0000644 00000000321 15073227061 0007425 0 ustar 00 <?php namespace Box\Spout\Common; /** * Class Type * This class references the supported types * * @api */ abstract class Type { const CSV = 'csv'; const XLSX = 'xlsx'; const ODS = 'ods'; }
| ver. 1.4 |
Github
|
.
| PHP 7.4.33 | Generation time: 0.01 |
proxy
|
phpinfo
|
Settings