First external release

This commit is contained in:
Adrien Loison committed 2015-01-15 18:14:07 -08:00
commit 5e199009e6
81 files changed
+5447

No files matched your search

+34
View File
@@ -0,0 +1,34 @@
<?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
*
* @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
*
* @param string $string The string to unescape
* @return string The unescaped string
*/
public function unescape($string)
{
return $string;
}
}
@@ -0,0 +1,27 @@
<?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);
}
+140
View File
@@ -0,0 +1,140 @@
<?php
namespace Box\Spout\Common\Escaper;
/**
* Class XLSX
* Provides functions to escape and unescape data for XLSX files
*
* @package Box\Spout\Common\Escaper
*/
class XLSX implements EscaperInterface
{
/** @var string[] Control characters to be escaped */
protected $controlCharactersEscapingMap;
public function __construct()
{
$this->controlCharactersEscapingMap = $this->getControlCharactersEscapingMap();
}
/**
* 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);
$escapedString = htmlspecialchars($escapedString, ENT_QUOTES, 'utf-8');
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)
{
$unescapedString = htmlspecialchars_decode($string, ENT_QUOTES);
$unescapedString = $this->unescapeControlCharacters($unescapedString);
return $unescapedString;
}
/**
* 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 = [];
$whitelistedControlCharacters = ["\t", "\r", "\n"];
// control characters values are from 0 to 1F (hex values) in the ASCII table
for ($charValue = 0x0; $charValue <= 0x1F; $charValue++) {
if (!in_array(chr($charValue), $whitelistedControlCharacters)) {
$charHexValue = dechex($charValue);
$escapedChar = '_x' . sprintf('%04s' , strtoupper($charHexValue)) . '_';
$controlCharactersEscapingMap[$escapedChar] = chr($charValue);
}
}
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);
return str_replace(array_values($this->controlCharactersEscapingMap), array_keys($this->controlCharactersEscapingMap), $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);
}
}
@@ -0,0 +1,12 @@
<?php
namespace Box\Spout\Common\Exception;
/**
* Class BadUsageException
*
* @package Box\Spout\Common\Exception
*/
class BadUsageException extends SpoutException
{
}
@@ -0,0 +1,12 @@
<?php
namespace Box\Spout\Common\Exception;
/**
* Class IOException
*
* @package Box\Spout\Common\Exception
*/
class IOException extends SpoutException
{
}
@@ -0,0 +1,12 @@
<?php
namespace Box\Spout\Common\Exception;
/**
* Class InvalidArgumentException
*
* @package Box\Spout\Common\Exception
*/
class InvalidArgumentException extends SpoutException
{
}
@@ -0,0 +1,12 @@
<?php
namespace Box\Spout\Common\Exception;
/**
* Class SpoutException
*
* @package Box\Spout\Common\Exception
*/
abstract class SpoutException extends \Exception
{
}
@@ -0,0 +1,12 @@
<?php
namespace Box\Spout\Common\Exception;
/**
* Class UnsupportedTypeException
*
* @package Box\Spout\Common\Exception
*/
class UnsupportedTypeException extends SpoutException
{
}
@@ -0,0 +1,132 @@
<?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 Path of the base folder where all the I/O can occur */
protected $baseFolderPath;
/**
* @param string $baseFolderPath The path of the base folder where all the I/O can occur
*/
public function __construct($baseFolderPath)
{
$this->baseFolderPath = $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 . DIRECTORY_SEPARATOR . $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 . DIRECTORY_SEPARATOR . $fileName;
$wasCreationSuccessful = file_put_contents($filePath, $fileContents);
if (!$wasCreationSuccessful) {
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)
{
$isInBaseFolder = (strpos($operationFolderPath, $this->baseFolderPath) === 0);
if (!$isInBaseFolder) {
throw new IOException('Cannot perform I/O operation outside of the base folder: ' . $this->baseFolderPath);
}
}
}
@@ -0,0 +1,163 @@
<?php
namespace Box\Spout\Common\Helper;
/**
* Class GlobalFunctionsHelper
* This class wraps global functions to facilitate testing
*
* @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 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 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
* @return string
*/
public function basename($path)
{
return basename($path);
}
/**
* Wrapper around global function header()
* @see header()
*
* @param string $string
* @return void
*/
public function header($string)
{
header($string);
}
}
+13
View File
@@ -0,0 +1,13 @@
<?php
namespace Box\Spout\Common;
/**
* Class Type
* This class references the supported types
*/
abstract class Type
{
const CSV = "csv";
const XLSX = "xlsx";
}
+196
View File
@@ -0,0 +1,196 @@
<?php
namespace Box\Spout\Reader;
use Box\Spout\Common\Exception\IOException;
use Box\Spout\Reader\Exception\ReaderNotOpenedException;
use Box\Spout\Reader\Exception\EndOfFileReachedException;
/**
* Class AbstractReader
*
* @package Box\Spout\Reader
* @abstract
*/
abstract class AbstractReader implements ReaderInterface
{
/** @var int Used to keep track of the row number */
protected $currentRowIndex = 0;
/** @var bool Indicates whether the stream is currently open */
protected $isStreamOpened = false;
/** @var bool Indicates whether all rows have been read */
protected $hasReachedEndOfFile = false;
/** @var array Buffer used to store the row data, while checking if there are more rows to read */
protected $rowDataBuffer = null;
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
protected $globalFunctionsHelper;
/**
* 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);
/**
* Reads and returns next row if available.
*
* @return array|null Array that contains the data for the read row or null at the end of the file
*/
abstract protected function read();
/**
* Closes the reader. To be used after reading the file.
*
* @return AbstractReader
*/
abstract protected function closeReader();
/**
* @param $globalFunctionsHelper
* @return AbstractReader
*/
public function setGlobalFunctionsHelper($globalFunctionsHelper)
{
$this->globalFunctionsHelper = $globalFunctionsHelper;
return $this;
}
/**
* 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 If the file at the given path does not exist, is not readable or is corrupted
*/
public function open($filePath)
{
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.');
}
}
$this->currentRowIndex = 0;
$this->hasReachedEndOfFile = false;
try {
$this->openReader($filePath);
$this->isStreamOpened = true;
} catch (\Exception $exception) {
throw new IOException('Could not open ' . $filePath . ' for reading! (' . $exception->getMessage() . ')');
}
}
/**
* 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)
{
return (strpos($filePath, 'php://') === 0);
}
/**
* Returns whether all rows have been read (i.e. if we are at the end of the file).
* To know if the end of file has been reached, it uses a buffer. If the buffer is
* empty (meaning, nothing has been read or previous read line has been consumed), then
* it reads the next line, store it in the buffer for the next time or flip a variable if
* the end of file has been reached.
*
* @return bool Whether all rows have been read (i.e. if we are at the end of the file)
* @throws \Box\Spout\Reader\Exception\ReaderNotOpenedException If the stream was not opened first
*/
public function hasNextRow()
{
if (!$this->isStreamOpened) {
throw new ReaderNotOpenedException('Stream should be opened first.');
}
if ($this->hasReachedEndOfFile) {
return false;
}
// if the buffer contains unprocessed row
if (!$this->isRowDataBufferEmpty()) {
return true;
}
// otherwise, try to read the next line line, and store it in the buffer
$this->rowDataBuffer = $this->read();
// if the buffer is still empty after reading a row, it means end of file was reached
$this->hasReachedEndOfFile = $this->isRowDataBufferEmpty();
return (!$this->hasReachedEndOfFile);
}
/**
* Returns next row if available. The row is either retrieved from the buffer if it is not empty or fetched by
* actually reading the file.
*
* @return array Array that contains the data for the read row
* @throws \Box\Spout\Common\Exception\IOException If the stream was not opened first
* @throws \Box\Spout\Reader\Exception\EndOfFileReachedException
*/
public function nextRow()
{
if (!$this->hasNextRow()) {
throw new EndOfFileReachedException('End of file was reached. Cannot read more rows.');
}
// Get data from buffer (if the buffer was empty, it was filled by the call to hasNextRow())
$rowData = $this->rowDataBuffer;
// empty buffer to mark the row as consumed
$this->emptyRowDataBuffer();
$this->currentRowIndex++;
return $rowData;
}
/**
* Returns whether the buffer where the row data is stored is empty
*
* @return bool
*/
protected function isRowDataBufferEmpty()
{
return ($this->rowDataBuffer === null);
}
/**
* Empty the buffer that stores row data
*
* @return void
*/
protected function emptyRowDataBuffer()
{
$this->rowDataBuffer = null;
}
/**
* Closes the reader, preventing any additional reading
*
* @return void
*/
public function close()
{
if ($this->isStreamOpened) {
$this->closeReader();
$this->isStreamOpened = false;
}
}
}
+130
View File
@@ -0,0 +1,130 @@
<?php
namespace Box\Spout\Reader;
use Box\Spout\Common\Exception\IOException;
/**
* Class CSV
* This class provides support to read data from a CSV file.
*
* @package Box\Spout\Reader
*/
class CSV extends AbstractReader
{
const UTF8_BOM = "\xEF\xBB\xBF";
/** @var resource Pointer to the file to be written */
protected $filePointer;
/** @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 = '"';
/**
* Sets the field delimiter for the CSV
*
* @param string $fieldDelimiter Character that delimits fields
* @return CSV
*/
public function setFieldDelimiter($fieldDelimiter)
{
$this->fieldDelimiter = $fieldDelimiter;
return $this;
}
/**
* Sets the field enclosure for the CSV
*
* @param string $fieldEnclosure Character that enclose fields
* @return CSV
*/
public function setFieldEnclosure($fieldEnclosure)
{
$this->fieldEnclosure = $fieldEnclosure;
return $this;
}
/**
* Opens the file at the given path to make it ready to be read.
* The file must be UTF-8 encoded.
* @TODO add encoding detection/conversion
*
* @param string $filePath Path of the XLSX file to be read
* @return void
* @throws \Box\Spout\Common\Exception\IOException
*/
protected function openReader($filePath)
{
$this->filePointer = $this->globalFunctionsHelper->fopen($filePath, 'r');
if (!$this->filePointer) {
throw new IOException('Could not open file ' . $filePath . ' for reading.');
}
$this->skipUtf8Bom();
}
/**
* This skips the UTF-8 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 skipUtf8Bom()
{
$this->globalFunctionsHelper->rewind($this->filePointer);
$hasUtf8Bom = ($this->globalFunctionsHelper->fgets($this->filePointer, 4) === self::UTF8_BOM);
if ($hasUtf8Bom) {
// we skip the 2 first bytes (so start from the 3rd byte)
$this->globalFunctionsHelper->fseek($this->filePointer, 3);
} else {
// if no BOM, reset the pointer to read from the beginning
$this->globalFunctionsHelper->fseek($this->filePointer, 0);
}
}
/**
* Reads and returns next row if available.
* Empty rows are skipped.
*
* @return array|null Array that contains the data for the read row or null at the end of the file
*/
protected function read()
{
$lineData = null;
if ($this->filePointer) {
do {
$lineData = $this->globalFunctionsHelper->fgetcsv($this->filePointer, 0, $this->fieldDelimiter, $this->fieldEnclosure);
} while ($lineData && $this->isEmptyLine($lineData));
}
// When reaching the end of the file, return null instead of false
return ($lineData !== false) ? $lineData : null;
}
/**
* @param array $lineData Array containing the cells value for the line
* @return bool Whether the given line is empty
*/
protected function isEmptyLine($lineData)
{
return (count($lineData) === 1 && $lineData[0] === null);
}
/**
* Closes the reader. To be used after reading the file.
*
* @return void
*/
protected function closeReader()
{
if ($this->filePointer) {
$this->globalFunctionsHelper->fclose($this->filePointer);
}
}
}
@@ -0,0 +1,12 @@
<?php
namespace Box\Spout\Reader\Exception;
/**
* Class EndOfFileReachedException
*
* @package Box\Spout\Reader\Exception
*/
class EndOfFileReachedException extends ReaderException
{
}
@@ -0,0 +1,12 @@
<?php
namespace Box\Spout\Reader\Exception;
/**
* Class EndOfWorksheetsReachedException
*
* @package Box\Spout\Reader\Exception
*/
class EndOfWorksheetsReachedException extends ReaderException
{
}
@@ -0,0 +1,12 @@
<?php
namespace Box\Spout\Reader\Exception;
/**
* Class NoWorksheetsFoundException
*
* @package Box\Spout\Reader\Exception
*/
class NoWorksheetsFoundException extends ReaderException
{
}
@@ -0,0 +1,15 @@
<?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
{
}
@@ -0,0 +1,12 @@
<?php
namespace Box\Spout\Reader\Exception;
/**
* Class ReaderNotOpenedException
*
* @package Box\Spout\Reader\Exception
*/
class ReaderNotOpenedException extends ReaderException
{
}
@@ -0,0 +1,12 @@
<?php
namespace Box\Spout\Reader\Exception;
/**
* Class SharedStringNotFoundException
*
* @package Box\Spout\Reader\Exception
*/
class SharedStringNotFoundException extends ReaderException
{
}
@@ -0,0 +1,97 @@
<?php
namespace Box\Spout\Reader\Helper\XLSX;
use Box\Spout\Common\Exception\InvalidArgumentException;
/**
* Class CellHelper
* This class provides helper functions when working with cells
*
* @package Box\Spout\Reader\Helper\XLSX
*/
class CellHelper
{
/**
* 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 = '')
{
$existingIndexes = array_keys($dataArray);
$newIndexes = array_fill_keys(range(min($existingIndexes), 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;
$capitalAAsciiValue = ord('A');
$capitalZAsciiValue = ord('Z');
$step = $capitalZAsciiValue - $capitalAAsciiValue + 1;
// Remove row information
$column = preg_replace('/\d/', '', $cellIndex);
$columnLength = strlen($column);
/*
* This is how the following loop will process the data:
* A => 0
* Z => 25
* AA => 26 : (26^(2-1) * (0+1)) + 0
* AB => 27 : (26^(2-1) * (0+1)) + 1
* BC => 54 : (26^(2-1) * (1+1)) + 2
* BCZ => 1455 : (26^(3-1) * (1+1)) + (26^(2-1) * (2+1)) + 25
*/
foreach (str_split($column) as $single_cell_index)
{
$currentColumnIndex = ord($single_cell_index) - $capitalAAsciiValue;
if ($columnLength == 1) {
$columnIndex += $currentColumnIndex;
} else {
$columnIndex += pow($step, ($columnLength - 1)) * ($currentColumnIndex + 1);
}
$columnLength--;
}
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.
*
* @param string $cellIndex The Excel cell index ('A1', 'BC13', ...)
* @return bool
*/
protected static function isValidCellIndex($cellIndex)
{
return (preg_match('/^[A-Z]+\d+$/', $cellIndex) === 1);
}
}
@@ -0,0 +1,268 @@
<?php
namespace Box\Spout\Reader\Helper\XLSX;
use Box\Spout\Common\Exception\IOException;
use Box\Spout\Common\Helper\FileSystemHelper;
use Box\Spout\Reader\Exception\SharedStringNotFoundException;
/**
* Class SharedStringsHelper
* This class provides helper functions for reading sharedStrings XML file
*
* @package Box\Spout\Reader\Helper\XLSX
*/
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';
/**
* To avoid running out of memory when extracting the shared strings, they will 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 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 \Box\Spout\Writer\Helper\XLSX\FileSystemHelper Helper to perform file system operations */
protected $fileSystemHelper;
/** @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 MAX_NUM_STRINGS_PER_TEMP_FILE
*/
protected $inMemoryTempFilePath;
/**
* @var string Contents of the temporary file that was last read
* @see MAX_NUM_STRINGS_PER_TEMP_FILE
*/
protected $inMemoryTempFileContents;
/**
* @param string $filePath Path of the XLSX file being read
* @param string|void $tempFolder Temporary folder where the temporary files to store shared strings will be stored
*/
public function __construct($filePath, $tempFolder = null)
{
$this->filePath = $filePath;
$rootTempFolder = ($tempFolder) ?: sys_get_temp_dir();
$this->fileSystemHelper = new FileSystemHelper($rootTempFolder);
$this->tempFolder = $this->fileSystemHelper->createFolder($rootTempFolder, uniqid('sharedstrings'));
}
/**
* Builds an in-memory array containing all the shared strings of the worksheet.
* All the strings are stored in a XML file, located at 'xl/sharedStrings.xml'.
* It is then accessed by the worksheet 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 worksheets containing a lot of data. That is why
* we need to use a XML reader that provides streaming like the XMLReader library.
* Please note that SimpleXML does not provide such a functionality but since it is faster
* and more handy to parse few XML nodes, it is used in combination with XMLReader for that purpose.
*
* @param string $filePath
* @return void
* @throws \Box\Spout\Common\Exception\IOException If sharedStrings.xml can't be read
*/
public function extractSharedStrings()
{
$xmlReader = new \XMLReader();
$sharedStringIndex = 0;
$this->tempFilePointer = null;
$escaper = new \Box\Spout\Common\Escaper\XLSX();
$sharedStringsFilePath = 'zip://' . $this->filePath . '#' . self::SHARED_STRINGS_XML_FILE_PATH;
if ($xmlReader->open($sharedStringsFilePath, null, LIBXML_NONET) === false) {
throw new IOException('Could not open "' . self::SHARED_STRINGS_XML_FILE_PATH . '".');
}
while ($xmlReader->read() && $xmlReader->name !== 'si') {
// do nothing until a 'si' tag is reached
}
while ($xmlReader->name === 'si') {
$node = new \SimpleXMLElement($xmlReader->readOuterXml());
$node->registerXPathNamespace('ns', self::MAIN_NAMESPACE_FOR_SHARED_STRINGS_XML);
// removes nodes that should not be read, like the pronunciation of the Kanji characters
$cleanNode = $this->removeSuperfluousTextNodes($node);
// find all text nodes 't'; there can be multiple if the cell contains formatting
$textNodes = $cleanNode->xpath('//ns:t');
$textValue = '';
foreach ($textNodes as $textNode) {
if ($this->shouldPreserveWhitespace($textNode)) {
$textValue .= $textNode->__toString();
} else {
$textValue .= trim($textNode->__toString());
}
}
$unescapedTextValue = $escaper->unescape($textValue);
$this->writeSharedStringToTempFile($unescapedTextValue, $sharedStringIndex);
$sharedStringIndex++;
// jump to the next 'si' tag
$xmlReader->next('si');
}
// close pointer to the last temp file that was written
if ($this->tempFilePointer) {
fclose($this->tempFilePointer);
}
$xmlReader->close();
}
/**
* Removes nodes that should not be read, like the pronunciation of the Kanji characters.
* By keeping them, their text content would be added to the read string.
*
* @param \SimpleXMLElement $parentNode Parent node that may contain nodes to remove
* @return \SimpleXMLElement Cleaned parent node
*/
protected function removeSuperfluousTextNodes($parentNode)
{
$tagsToRemove = [
'rPh', // Pronunciation of the text
];
foreach ($tagsToRemove as $tagToRemove) {
$xpath = '//ns:' . $tagToRemove;
$nodesToRemove = $parentNode->xpath($xpath);
foreach ($nodesToRemove as $nodeToRemove) {
// This is how to remove a node from the XML
unset($nodeToRemove[0]);
}
}
return $parentNode;
}
/**
* If the text node has the attribute 'xml:space="preserve"', then preserve whitespace.
*
* @param \SimpleXMLElement $textNode The text node element (<t>) whitespace may be preserved
* @return bool Whether whitespace should be preserved
*/
protected function shouldPreserveWhitespace($textNode)
{
$shouldPreserveWhitespace = false;
$attributes = $textNode->attributes('xml', true);
if ($attributes) {
foreach ($attributes as $attributeName => $attributeValue) {
if ($attributeName === 'space' && $attributeValue->__toString() === 'preserve') {
$shouldPreserveWhitespace = true;
break;
}
}
}
return $shouldPreserveWhitespace;
}
/**
* Writes the given string to its associated temp file.
* A new temporary file is created when the previous one has reached its max capacity.
*
* @param string $sharedString Shared string to write to the temp file
* @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
* @return void
*/
protected function writeSharedStringToTempFile($sharedString, $sharedStringIndex)
{
$tempFilePath = $this->getSharedStringTempFilePath($sharedStringIndex);
if (!file_exists($tempFilePath)) {
if ($this->tempFilePointer) {
fclose($this->tempFilePointer);
}
$this->tempFilePointer = fopen($tempFilePath, 'w');
}
fwrite($this->tempFilePointer, $sharedString . 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 / self::MAX_NUM_STRINGS_PER_TEMP_FILE);
return $this->tempFolder . DIRECTORY_SEPARATOR . 'sharedstrings' . $numTempFile;
}
/**
* Returns the shared string at the given index.
* Because the strings have been split into different files, it looks for the value in the correct file.
*
* @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 % self::MAX_NUM_STRINGS_PER_TEMP_FILE;
if (!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, file_get_contents($tempFilePath));
$this->inMemoryTempFilePath = $tempFilePath;
}
$sharedString = null;
if (array_key_exists($indexInFile, $this->inMemoryTempFileContents)) {
$sharedString = $this->inMemoryTempFileContents[$indexInFile];
}
if (!$sharedString) {
throw new SharedStringNotFoundException("Shared string not found for index: $sharedStringIndex");
}
return rtrim($sharedString, PHP_EOL);
}
/**
* Deletes the created temporary folder and all its contents
*
* @return void
*/
public function cleanup()
{
$this->fileSystemHelper->deleteFolderRecursively($this->tempFolder);
}
}
@@ -0,0 +1,74 @@
<?php
namespace Box\Spout\Reader\Helper\XLSX;
use Box\Spout\Reader\Internal\XLSX\Worksheet;
/**
* Class WorksheetHelper
* This class provides helper functions related to XLSX worksheets
*
* @package Box\Spout\Reader\Helper\XLSX
*/
class WorksheetHelper
{
/** Path of Content_Types XML file inside the XLSX file */
const CONTENT_TYPES_XML_FILE_PATH = '[Content_Types].xml';
/** Main namespace for the [Content_Types].xml file */
const MAIN_NAMESPACE_FOR_CONTENT_TYPES_XML = 'http://schemas.openxmlformats.org/package/2006/content-types';
/** Value of the Override attribute used in [Content_Types].xml to define worksheets */
const OVERRIDE_CONTENT_TYPES_ATTRIBUTE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml';
/** @var string Path of the XLSX file being read */
protected $filePath;
/**
* @param string $filePath Path of the XLSX file being read
*/
public function __construct($filePath)
{
$this->filePath = $filePath;
}
/**
* Returns the file paths of the worksheet data XML files within the XLSX file.
* The paths are read from the [Content_Types].xml file.
*
* @return Worksheet[] Worksheets within the XLSX file
*/
public function getWorksheets()
{
$worksheets = [];
$xmlContents = file_get_contents('zip://' . $this->filePath . '#' . self::CONTENT_TYPES_XML_FILE_PATH);
$contentTypes = new \SimpleXMLElement($xmlContents);
$contentTypes->registerXPathNamespace('ns', self::MAIN_NAMESPACE_FOR_CONTENT_TYPES_XML);
// find all nodes defining a worksheet
$sheetNodes = $contentTypes->xpath('//ns:Override[@ContentType="' . self::OVERRIDE_CONTENT_TYPES_ATTRIBUTE . '"]');
for ($i = 0; $i < count($sheetNodes); $i++) {
$sheetNode = $sheetNodes[$i];
$sheetDataXMLFilePath = (string) $sheetNode->attributes()->PartName;
$worksheets[] = new Worksheet($i, $sheetDataXMLFilePath);
}
return $worksheets;
}
/**
* Returns whether another worksheet exists after the current worksheet.
* The order is determined by the order of appearance in the [Content_Types].xml file.
*
* @param Worksheet|null $currentWorksheet The worksheet being currently read or null if reading has not started yet
* @param Worksheet[] $allWorksheets A list of all worksheets in the XLSX file. Must contain at least one worksheet
* @return bool Whether another worksheet exists after the current sheet
*/
public function hasNextWorksheet($currentWorksheet, $allWorksheets)
{
return ($currentWorksheet === null || ($currentWorksheet->getWorksheetNumber() + 1 < count($allWorksheets)));
}
}
@@ -0,0 +1,44 @@
<?php
namespace Box\Spout\Reader\Internal\XLSX;
/**
* Class Worksheet
* Represents a worksheet within a XLSX file
*
* @package Box\Spout\Reader\Internal\XLSX
*/
class Worksheet
{
/** @var int Worksheet number, based on the order of appareance in [Content_Types].xml (zero-based) */
protected $worksheetNumber;
/** @var string Path of the XML file containing the worksheet data */
protected $dataXmlFilePath;
/**
* @param int $worksheetNumber Worksheet number, based on the order of appareance in [Content_Types].xml (zero-based)
* @param string $dataXmlFilePath Path of the XML file containing the worksheet data
*/
public function __construct($worksheetNumber, $dataXmlFilePath)
{
$this->worksheetNumber = $worksheetNumber;
$this->dataXmlFilePath = $dataXmlFilePath;
}
/**
* @return string Path of the XML file containing the worksheet data, without the leading slash
*/
public function getDataXmlFilePath()
{
return ltrim($this->dataXmlFilePath, DIRECTORY_SEPARATOR);
}
/**
* @return int
*/
public function getWorksheetNumber()
{
return $this->worksheetNumber;
}
}
+44
View File
@@ -0,0 +1,44 @@
<?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
*
* @param string $readerType Type of the reader to instantiate
* @return \Box\Spout\Reader\CSV|\Box\Spout\Reader\XLSX
* @throws \Box\Spout\Common\Exception\UnsupportedTypeException
*/
public static function create($readerType)
{
$reader = null;
switch ($readerType) {
case Type::CSV:
$reader = new CSV();
break;
case Type::XLSX:
$reader = new XLSX();
break;
default:
throw new UnsupportedTypeException('No readers supporting the given type: ' . $readerType);
}
$reader->setGlobalFunctionsHelper(new GlobalFunctionsHelper());
return $reader;
}
}
+50
View File
@@ -0,0 +1,50 @@
<?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 whether all rows have been read (i.e. if we are at the end of the file).
* To know if the end of file has been reached, it uses a buffer. If the buffer is
* empty (meaning, nothing has been read or previous read line has been consumed), then
* it reads the next line, store it in the buffer for the next time or flip a variable if
* the end of file has been reached.
*
* @return bool
* @throws \Box\Spout\Common\Exception\IOException If the stream was not opened first
*/
public function hasNextRow();
/**
* Returns next row if available. The row is either retrieved from the buffer if it is not empty or fetched by
* actually reading the file.
*
* @return array Array that contains the data for the read row
* @throws \Box\Spout\Common\Exception\IOException If the stream was not opened first
* @throws \Box\Spout\Reader\Exception\EndOfFileReachedException
*/
public function nextRow();
/**
* Closes the reader, preventing any additional reading
*
* @return void
*/
public function close();
}
+251
View File
@@ -0,0 +1,251 @@
<?php
namespace Box\Spout\Reader;
use Box\Spout\Common\Exception\BadUsageException;
use Box\Spout\Common\Exception\IOException;
use Box\Spout\Reader\Exception\EndOfWorksheetsReachedException;
use Box\Spout\Reader\Exception\NoWorksheetsFoundException;
use Box\Spout\Reader\Exception\ReaderNotOpenedException;
use Box\Spout\Reader\Helper\XLSX\CellHelper;
use Box\Spout\Reader\Helper\XLSX\SharedStringsHelper;
use Box\Spout\Reader\Helper\XLSX\WorksheetHelper;
/**
* Class XLSX
* This class provides support to read data from a XLSX file
*
* @package Box\Spout\Reader
*/
class XLSX extends AbstractReader
{
/** @var string Real path of the file to read */
protected $filePath;
/** @var string Temporary folder where the temporary files will be created */
protected $tempFolder;
/** @var \ZipArchive */
protected $zip;
/** @var Helper\XLSX\SharedStringsHelper Helper to work with shared strings */
protected $sharedStringsHelper;
/** @var Helper\XLSX\WorksheetHelper Helper to work with worksheets */
protected $worksheetHelper;
/** @var Internal\XLSX\Worksheet[] The list of worksheets present in the file */
protected $worksheets;
/** @var Internal\XLSX\Worksheet The worksheet being read */
protected $currentWorksheet;
/** @var \XMLReader The XMLReader object that will help read sheets XML data */
protected $xmlReader;
/** @var int The number of columns the worksheet has (0 meaning undefined) */
protected $numberOfColumns = 0;
/**
* @param string $tempFolder Temporary folder where the temporary files will be created
* @return XLSX
*/
public function setTempFolder($tempFolder)
{
$this->tempFolder = $tempFolder;
return $this;
}
/**
* 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 worksheets.
*
* @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 Exception\NoWorksheetsFoundException If there are no worksheets in the file
*/
protected function openReader($filePath)
{
$this->filePath = $filePath;
$this->zip = new \ZipArchive();
if ($this->zip->open($filePath) === true) {
// Extracts all the strings from the worksheets for easy access in the future
$this->extractSharedStrings($filePath);
// Fetch all available worksheets
$this->worksheetHelper = new WorksheetHelper($filePath);
$this->worksheets = $this->worksheetHelper->getWorksheets($filePath);
if (count($this->worksheets) === 0) {
throw new NoWorksheetsFoundException('The file must contain at least one worksheet.');
}
} else {
throw new IOException('Could not open ' . $filePath . ' for reading.');
}
}
/**
* Builds an in-memory array containing all the shared strings of the worksheets.
*
* @param string $filePath Path of the XLSX file to be read
* @return void
* @throws \Box\Spout\Common\Exception\IOException If sharedStrings XML file can't be read
*/
protected function extractSharedStrings($filePath)
{
$this->sharedStringsHelper = new SharedStringsHelper($filePath, $this->tempFolder);
$this->sharedStringsHelper->extractSharedStrings();
}
/**
* Returns whether another worksheet exists after the current worksheet.
*
* @return bool Whether another worksheet exists after the current worksheet.
* @throws Exception\ReaderNotOpenedException If the stream was not opened first
*/
public function hasNextSheet()
{
if (!$this->isStreamOpened) {
throw new ReaderNotOpenedException('Stream should be opened first.');
}
return $this->worksheetHelper->hasNextWorksheet($this->currentWorksheet, $this->worksheets);
}
/**
* Moves the pointer to the current worksheet.
* Moving to another worksheet will stop the reading in the current worksheet.
*
* @return void
* @throws Exception\ReaderNotOpenedException If the stream was not opened first
* @throws Exception\EndOfWorksheetsReachedException If there is no more worksheets to read
*/
public function nextSheet()
{
if ($this->hasNextSheet()) {
if ($this->currentWorksheet === null) {
$nextWorksheet = $this->worksheets[0];
} else {
$currentWorksheetNumber = $this->currentWorksheet->getWorksheetNumber();
$nextWorksheet = $this->worksheets[$currentWorksheetNumber + 1];
}
$this->initXmlReaderForWorksheetData($nextWorksheet);
$this->currentWorksheet = $nextWorksheet;
// make sure that we are ready to read more rows
$this->hasReachedEndOfFile = false;
$this->emptyRowDataBuffer();
} else {
throw new EndOfWorksheetsReachedException('End of worksheets was reached. Cannot read more worksheets.');
}
}
/**
* Initializes the XMLReader object that reads worksheet data for the given worksheet.
* If another worksheet was being read, it closes the reader before reopening it for the new worksheet.
* The XMLReader is configured to be safe from billion laughs attack.
*
* @param Internal\XLSX\Worksheet $worksheet The worksheet to initialize the XMLReader with
* @return void
* @throws \Box\Spout\Common\Exception\IOException If the worksheet data XML cannot be read
*/
protected function initXmlReaderForWorksheetData($worksheet)
{
// if changing worksheet and the XMLReader was initialized for the current worksheet
if ($worksheet != $this->currentWorksheet && $this->xmlReader) {
$this->xmlReader->close();
} else if (!$this->xmlReader) {
$this->xmlReader = new \XMLReader();
}
$worksheetDataXMLFilePath = $worksheet->getDataXmlFilePath();
$worksheetDataFilePath = 'zip://' . $this->filePath . '#' . $worksheetDataXMLFilePath;
if ($this->xmlReader->open($worksheetDataFilePath, null, LIBXML_NONET) === false) {
throw new IOException('Could not open "' . $worksheetDataXMLFilePath . '".');
}
}
/**
* Reads and returns data of the line that comes after the last read line, on the current worksheet.
* Empty rows will be skipped.
*
* @return array|null Array that contains the data for the read line or null at the end of the file
* @throws \Box\Spout\Common\Exception\BadUsageException If the pointer to the current worksheet has not been set
* @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If a shared string was not found
*/
protected function read()
{
if (!$this->currentWorksheet) {
throw new BadUsageException('You must call nextSheet() before calling hasNextRow() or nextRow()');
}
$isInsideRowTag = false;
$rowData = [];
while ($this->xmlReader->read()) {
if ($this->xmlReader->nodeType == \XMLReader::ELEMENT && $this->xmlReader->name === 'dimension') {
// Read dimensions of the worksheet
$dimensionRef = $this->xmlReader->getAttribute('ref'); // returns 'A1:M13' for instance
list(, $lastCellIndex) = explode(':', $dimensionRef);
$this->numberOfColumns = CellHelper::getColumnIndexFromCellIndex($lastCellIndex) + 1;
} else if ($this->xmlReader->nodeType == \XMLReader::ELEMENT && $this->xmlReader->name === 'row') {
// Start of the row description
$isInsideRowTag = true;
// Read spans info if present
$numberOfColumnsForRow = $this->numberOfColumns;
$spans = $this->xmlReader->getAttribute('spans'); // returns '1:5' for instance
if ($spans) {
list(, $numberOfColumnsForRow) = explode(':', $spans);
$numberOfColumnsForRow = intval($numberOfColumnsForRow);
}
$rowData = ($numberOfColumnsForRow !== 0) ? array_fill(0, $numberOfColumnsForRow, '') : [];
} else if ($isInsideRowTag && $this->xmlReader->nodeType == \XMLReader::ELEMENT && $this->xmlReader->name === 'c') {
// Start of a cell description
$currentCellIndex = $this->xmlReader->getAttribute('r');
$currentColumnIndex = CellHelper::getColumnIndexFromCellIndex($currentCellIndex);
$node = $this->xmlReader->expand();
$hasSharedString = ($this->xmlReader->getAttribute('t') === 's');
if ($hasSharedString) {
$sharedStringIndex = intval($node->nodeValue);
$rowData[$currentColumnIndex] = $this->sharedStringsHelper->getStringAtIndex($sharedStringIndex);
} else {
// for inline strings or numbers, just get the value
$rowData[$currentColumnIndex] = trim($node->nodeValue);
}
} else if ($this->xmlReader->nodeType == \XMLReader::END_ELEMENT && $this->xmlReader->name === 'row') {
// End of the row description
// If needed, we fill the empty cells
$rowData = ($this->numberOfColumns !== 0) ? $rowData : CellHelper::fillMissingArrayIndexes($rowData);
break;
}
}
// no data means "end of file"
return ($rowData !== []) ? $rowData : null;
}
/**
* Closes the reader. To be used after reading the file.
*
* @return void
*/
protected function closeReader()
{
if ($this->xmlReader) {
$this->xmlReader->close();
}
if ($this->zip) {
$this->zip->close();
}
$this->sharedStringsHelper->cleanup();
}
}
+198
View File
@@ -0,0 +1,198 @@
<?php
namespace Box\Spout\Writer;
use Box\Spout\Common\Exception\IOException;
use Box\Spout\Common\Exception\InvalidArgumentException;
use Box\Spout\Writer\Exception\WriterNotOpenedException;
/**
* 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 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'];
* @return void
*/
abstract protected function addRowToWriter(array $dataRow);
/**
* Closes the streamer, preventing any additional writing.
*
* @return void
*/
abstract protected function closeWriter();
/**
* @param $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.
*
* @param string $outputFilePath Path of the output file that will contain the data
* @return \Box\Spout\Writer\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.
*
* @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 \Box\Spout\Writer\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();
// 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');
}
}
/**
* 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 \Box\Spout\Writer\AbstractWriter
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer
* @throws \Box\Spout\Common\Exception\IOException If unable to write data
*/
public function addRow(array $dataRow)
{
if ($this->isWriterOpened) {
$this->addRowToWriter($dataRow);
} else {
throw new WriterNotOpenedException('The writer needs to be opened before adding row.');
}
return $this;
}
/**
* 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 \Box\Spout\Writer\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)) {
if (!is_array($dataRows[0])) {
throw new InvalidArgumentException('The input should be an array of arrays');
}
foreach ($dataRows as $dataRow) {
$this->addRow($dataRow);
}
}
return $this;
}
/**
* Closes the writer. This will close the streamer as well, preventing new data
* to be written to the file.
*
* @return void
*/
public function close()
{
$this->closeWriter();
$this->isWriterOpened = false;
}
}
+101
View File
@@ -0,0 +1,101 @@
<?php
namespace Box\Spout\Writer;
use Box\Spout\Common\Exception\IOException;
/**
* Class CSV
* This class provides support to write data to CSV files
*
* @package Box\Spout\Writer
*/
class CSV extends AbstractWriter
{
/** Number of rows to write before flushing */
const FLUSH_THRESHOLD = 500;
const UTF8_BOM = "\xEF\xBB\xBF";
/** @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;
/**
* Sets the field delimiter for the CSV
*
* @param string $fieldDelimiter Character that delimits fields
* @return CSV
*/
public function setFieldDelimiter($fieldDelimiter)
{
$this->fieldDelimiter = $fieldDelimiter;
return $this;
}
/**
* Sets the field enclosure for the CSV
*
* @param string $fieldEnclosure Character that enclose fields
* @return CSV
*/
public function setFieldEnclosure($fieldEnclosure)
{
$this->fieldEnclosure = $fieldEnclosure;
return $this;
}
/**
* Opens the CSV streamer and makes it ready to accept data.
*
* @return void
*/
protected function openWriter()
{
// Adds UTF-8 BOM for Unicode compatibility
$this->globalFunctionsHelper->fputs($this->filePointer, self::UTF8_BOM);
}
/**
* Adds data to the currently opened writer.
*
* @param array $dataRow Array containing data to be written.
* Example $dataRow = ['data1', 1234, null, '', 'data5'];
* @return void
* @throws \Box\Spout\Common\Exception\IOException If unable to write data
*/
protected function addRowToWriter(array $dataRow)
{
$wasWriteSuccessful = 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()
{
if ($this->filePointer) {
$this->globalFunctionsHelper->fclose($this->filePointer);
}
$this->lastWrittenRowIndex = 0;
}
}
@@ -0,0 +1,12 @@
<?php
namespace Box\Spout\Writer\Exception;
/**
* Class SheetNotFoundException
*
* @package Box\Spout\Writer\Exception
*/
class SheetNotFoundException extends WriterException
{
}
@@ -0,0 +1,15 @@
<?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
{
}
@@ -0,0 +1,12 @@
<?php
namespace Box\Spout\Writer\Exception;
/**
* Class WriterNotOpenedException
*
* @package Box\Spout\Writer\Exception
*/
class WriterNotOpenedException extends WriterException
{
}
@@ -0,0 +1,37 @@
<?php
namespace Box\Spout\Writer\Helper\XLSX;
/**
* Class CellHelper
* This class provides helper functions when working with cells
*
* @package Box\Spout\Writer\Helper\XLSX
*/
class CellHelper
{
/**
* 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)
{
$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);
return $cellIndex;
}
}
@@ -0,0 +1,378 @@
<?php
namespace Box\Spout\Writer\Helper\XLSX;
use Box\Spout\Writer\Internal\XLSX\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\Helper\XLSX
*/
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';
/** @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->baseFolderPath, uniqid('xlsx'));
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('c');
$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: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"/>' . PHP_EOL;
}
$contentTypesXmlFileContents .= <<<EOD
<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;
$escaper = new \Box\Spout\Common\Escaper\XLSX();
/** @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 . '"/>' . PHP_EOL;
}
$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="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"/>' . PHP_EOL;
}
$workbookRelsXmlFileContents .= '</Relationships>';
$this->createFileWithContents($this->xlRelsFolder, self::WORKBOOK_RELS_XML_FILE_NAME, $workbookRelsXmlFileContents);
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)
{
$this->zipRootFolder();
$this->copyZipToStream($streamPointer);
// once the zip is copied, remove it
$this->deleteFile($this->getZipFilePath());
}
/**
* Zips the root folder
*
* @return void
*/
protected function zipRootFolder()
{
$zipHelper = new ZipHelper();
$zipHelper->zipFolder($this->rootFolder, $this->getZipFilePath());
}
/**
* @return string Path of the zip file created from the root folder
*/
protected function getZipFilePath()
{
return $this->rootFolder . '.zip';
}
/**
* Streams the contents of the zip 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);
}
}
@@ -0,0 +1,99 @@
<?php
namespace Box\Spout\Writer\Helper\XLSX;
use Box\Spout\Common\Exception\IOException;
/**
* Class SharedStringsHelper
* This class provides helper functions to write shared strings
*
* @package Box\Spout\Writer\Helper\XLSX
*/
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 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 . DIRECTORY_SEPARATOR . 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 . '>' . PHP_EOL;
fwrite($this->sharedStringsFilePointer, $header);
$this->stringsEscaper = new \Box\Spout\Common\Escaper\XLSX();
}
/**
* 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>' . PHP_EOL);
$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()
{
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);
}
}
@@ -0,0 +1,50 @@
<?php
namespace Box\Spout\Writer\Helper\XLSX;
/**
* Class ZipHelper
* This class provides helper functions to create zip files
*
* @package Box\Spout\Writer\Helper\XLSX
*/
class ZipHelper
{
/**
* Zips the given folder
*
* @param string $folderPath Path of the folder to be zipped
* @param string $destinationPath Path where the zip file will be created
* @return void
*/
public function zipFolder($folderPath, $destinationPath)
{
$zip = new \ZipArchive();
if ($zip->open($destinationPath, \ZipArchive::CREATE)) {
$this->addFolderToZip($zip, $folderPath);
$zip->close();
}
}
/**
* @param \ZipArchive $zip
* @param string $folderPath Path of the folder to add to the zip
* @return void
*/
protected function addFolderToZip($zip, $folderPath)
{
$folderRealPath = realpath($folderPath) . DIRECTORY_SEPARATOR;
$itemIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($folderPath, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST);
foreach ($itemIterator as $itemInfo) {
$itemRealPath = realpath($itemInfo->getPathname());
$itemLocalPath = str_replace($folderRealPath, '', $itemRealPath);
if ($itemInfo->isFile()) {
$zip->addFile($itemInfo->getPathname(), $itemLocalPath);
} else if ($itemInfo->isDir()) {
$zip->addEmptyDir($itemLocalPath);
}
}
}
}
+236
View File
@@ -0,0 +1,236 @@
<?php
namespace Box\Spout\Writer\Internal\XLSX;
use Box\Spout\Writer\Exception\SheetNotFoundException;
use Box\Spout\Writer\Helper\XLSX\FileSystemHelper;
use Box\Spout\Writer\Helper\XLSX\SharedStringsHelper;
use Box\Spout\Writer\Helper\XLSX\ZipHelper;
use Box\Spout\Writer\Sheet;
/**
* Class Book
* Represents a workbook within a XLSX file.
* It provides the functions to work with worksheets.
*
* @package Box\Spout\Writer\Internal\XLSX
*/
class Workbook
{
/**
* 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 bool Whether new sheets should be automatically created when the max rows limit per sheet is reached */
protected $shouldCreateNewSheetsAutomatically;
/** @var \Box\Spout\Writer\Helper\XLSX\FileSystemHelper Helper to perform file system operations */
protected $fileSystemHelper;
/** @var \Box\Spout\Writer\Helper\XLSX\SharedStringsHelper Helper to write shared strings */
protected $sharedStringsHelper;
/** @var Worksheet[] Array containing the workbook's sheets */
protected $worksheets = [];
/** @var Worksheet The worksheet where data will be written to */
protected $currentWorksheet;
/**
* @param string $tempFolder
* @param bool $shouldUseInlineStrings
* @param bool $shouldCreateNewSheetsAutomatically
* @throws \Box\Spout\Common\Exception\IOException If unable to create at least one of the base folders
*/
public function __construct($tempFolder, $shouldUseInlineStrings, $shouldCreateNewSheetsAutomatically)
{
$this->shouldUseInlineStrings = $shouldUseInlineStrings;
$this->shouldCreateNewSheetsAutomatically = $shouldCreateNewSheetsAutomatically;
$this->fileSystemHelper = new FileSystemHelper($tempFolder);
$this->fileSystemHelper->createBaseFilesAndFolders();
// This helper will be shared by all sheets
$xlFolder = $this->fileSystemHelper->getXlFolder();
$this->sharedStringsHelper = new SharedStringsHelper($xlFolder);
}
/**
* 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()
{
$newSheetNumber = count($this->worksheets);
$sheet = new Sheet($newSheetNumber);
$worksheetFilesFolder = $this->fileSystemHelper->getXlWorksheetsFolder();
$worksheet = new Worksheet($sheet, $worksheetFilesFolder, $this->sharedStringsHelper, $this->shouldUseInlineStrings);
$this->worksheets[] = $worksheet;
return $worksheet;
}
/**
* 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 Worksheet 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 Worksheet[] All the workbook's sheets
*/
public function getWorksheets()
{
return $this->worksheets;
}
/**
* Returns the current sheet
*
* @return Worksheet 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\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 Worksheet $worksheet
* @return void
*/
protected function setCurrentWorksheet($worksheet)
{
$this->currentWorksheet = $worksheet;
}
/**
* Returns the worksheet associated to the given external sheet.
*
* @param \Box\Spout\Writer\Sheet $sheet
* @return Worksheet|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.
* Example $dataRow = ['data1', 1234, null, '', 'data5'];
* @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)
{
$currentWorksheet = $this->getCurrentWorksheet();
$hasReachedMaxRows = $this->hasCurrentWorkseetReachedMaxRows();
// 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();
$currentWorksheet->addRow($dataRow);
} else {
// otherwise, do nothing as the data won't be read anyways
}
} else {
$currentWorksheet->addRow($dataRow);
}
}
/**
* @return bool Whether the current worksheet has reached the maximum number of rows per sheet.
*/
protected function hasCurrentWorkseetReachedMaxRows()
{
$currentWorksheet = $this->getCurrentWorksheet();
return ($currentWorksheet->getLastWrittenRowIndex() >= self::$maxRowsPerWorksheet);
}
/**
* 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)
{
foreach ($this->worksheets as $worksheet) {
$worksheet->close();
}
$this->sharedStringsHelper->close();
// Finish creating all the necessary files before zipping everything together
$this->fileSystemHelper
->createContentTypesFile($this->worksheets)
->createWorkbookFile($this->worksheets)
->createWorkbookRelsFile($this->worksheets)
->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);
}
}
@@ -0,0 +1,174 @@
<?php
namespace Box\Spout\Writer\Internal\XLSX;
use Box\Spout\Common\Exception\IOException;
use Box\Spout\Writer\Helper\XLSX\CellHelper;
/**
* 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\Internal\XLSX
*/
class Worksheet
{
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\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\Helper\XLSX\SharedStringsHelper Helper to write shared strings */
protected $sharedStringsHelper;
/** @var bool Whether inline or shared strings should be used */
protected $shouldUseInlineStrings;
/** @var \Box\Spout\Common\Escaper\XLSX Strings escaper */
protected $stringsEscaper;
/** @var Resource Pointer to the sheet data file (e.g. xl/worksheets/sheet1.xml) */
protected $sheetFilePointer;
/** @var int */
protected $lastWrittenRowIndex = 0;
/**
* @param \Box\Spout\Writer\Sheet $externalSheet The associated "external" sheet
* @param string $tempFolder Temporary folder where the files to create the XLSX will be stored
* @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, $shouldUseInlineStrings)
{
$this->externalSheet = $externalSheet;
$this->sharedStringsHelper = $sharedStringsHelper;
$this->shouldUseInlineStrings = $shouldUseInlineStrings;
$this->stringsEscaper = new \Box\Spout\Common\Escaper\XLSX();
$this->worksheetFilePath = $worksheetFilesFolder . DIRECTORY_SEPARATOR . 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 . PHP_EOL);
fwrite($this->sheetFilePointer, ' <sheetData>' . PHP_EOL);
}
/**
* @return \Box\Spout\Writer\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 number is zero-based, while ID is 1-based
return $this->externalSheet->getSheetNumber() + 1;
}
/**
* 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.');
}
}
/**
* Adds data to the worksheet.
*
* @param array $dataRow Array containing data to be written.
* Example $dataRow = ['data1', 1234, null, '', 'data5'];
* @return void
* @throws \Box\Spout\Common\Exception\IOException If the data cannot be written
*/
public function addRow($dataRow)
{
$cellNumber = 0;
$rowIndex = $this->lastWrittenRowIndex + 1;
$numCells = count($dataRow);
$data = ' <row r="' . $rowIndex . '" spans="1:' . $numCells . '">' . PHP_EOL;
foreach($dataRow as $cellValue) {
$columnIndex = CellHelper::getCellIndexFromColumnIndex($cellNumber);
$data .= ' <c r="' . $columnIndex . $rowIndex . '"';
if (empty($cellValue)) {
$data .= '/>' . PHP_EOL;
} else {
if (is_numeric($cellValue)) {
$data .= '><v>' . $cellValue . '</v></c>' . PHP_EOL;
} else {
if ($this->shouldUseInlineStrings) {
$data .= ' t="inlineStr"><is><t>' . $this->stringsEscaper->escape($cellValue) . '</t></is></c>' . PHP_EOL;
} else {
$sharedStringId = $this->sharedStringsHelper->writeString($cellValue);
$data .= ' t="s"><v>' . $sharedStringId . '</v></c>' . PHP_EOL;
}
}
}
$cellNumber++;
}
$data .= ' </row>' . PHP_EOL;
$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++;
}
/**
* Closes the worksheet
*
* @return void
*/
public function close()
{
fwrite($this->sheetFilePointer, ' </sheetData>' . PHP_EOL);
fwrite($this->sheetFilePointer, '</worksheet>');
fclose($this->sheetFilePointer);
}
}
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace Box\Spout\Writer;
/**
* Class Sheet
* Represents a worksheet within a XLSX file
*
* @package Box\Spout\Writer
*/
class Sheet
{
const DEFAULT_SHEET_NAME_PREFIX = 'Sheet';
/** @var int Number of the sheet, based on order of creation (zero-based) */
protected $sheetNumber;
/** @var string Name of the sheet */
protected $name;
/**
* @param $sheetNumber Number of the sheet, based on order of creation (zero-based)
*/
function __construct($sheetNumber)
{
$this->sheetNumber = $sheetNumber;
$this->name = self::DEFAULT_SHEET_NAME_PREFIX . ($sheetNumber + 1);
}
/**
* @return int Number of the sheet, based on order of creation (zero-based)
*/
public function getSheetNumber()
{
return $this->sheetNumber;
}
/**
* @return string Name of the sheet
*/
public function getName()
{
return $this->name;
}
}
+44
View File
@@ -0,0 +1,44 @@
<?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 and XLSX formats.
*
* @package Box\Spout\Writer
*/
class WriterFactory
{
/**
* This creates an instance of the appropriate writer, given the type of the file to be read
*
* @param string $writerType Type of the writer to instantiate
* @return \Box\Spout\Writer\CSV|\Box\Spout\Writer\XLSX
* @throws \Box\Spout\Common\Exception\UnsupportedTypeException
*/
public static function create($writerType)
{
$writer = null;
switch ($writerType) {
case Type::CSV:
$writer = new CSV();
break;
case Type::XLSX:
$writer = new XLSX();
break;
default:
throw new UnsupportedTypeException('No writers supporting the given type: ' . $writerType);
}
$writer->setGlobalFunctionsHelper(new GlobalFunctionsHelper());
return $writer;
}
}
+65
View File
@@ -0,0 +1,65 @@
<?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 \Box\Spout\Writer\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 \Box\Spout\Writer\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 \Box\Spout\Writer\WriterInterface
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yetthe writer
* @throws \Box\Spout\Common\Exception\IOException If unable to write data
*/
public function addRow(array $dataRow);
/**
* 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 \Box\Spout\Writer\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);
/**
* Closes the writer. This will close the streamer as well, preventing new data
* to be written to the file.
*
* @return void
*/
public function close();
}
+184
View File
@@ -0,0 +1,184 @@
<?php
namespace Box\Spout\Writer;
use Box\Spout\Writer\Exception\WriterNotOpenedException;
use Box\Spout\Writer\Internal\XLSX\Workbook;
/**
* Class XLSX
* This class provides base support to write data to XLSX files
*
* @package Box\Spout\Writer
*/
class XLSX extends AbstractWriter
{
/** @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 bool Whether new sheets should be automatically created when the max rows limit per sheet is reached */
protected $shouldCreateNewSheetsAutomatically = true;
/** @var Internal\XLSX\Workbook The workbook for the XLSX file */
protected $book;
/** @var int */
protected $highestRowIndex = 0;
/**
* @param string $tempFolder Temporary folder where the files to create the XLSX will be stored
* @return XLSX
*/
public function setTempFolder($tempFolder)
{
$this->tempFolder = $tempFolder;
return $this;
}
/**
* Use inline string to be more memory efficient. If set to false, it will use shared strings.
*
* @param bool $shouldUseInlineStrings Whether inline or shared strings should be used
* @return XLSX
*/
public function setShouldUseInlineStrings($shouldUseInlineStrings)
{
$this->shouldUseInlineStrings = $shouldUseInlineStrings;
return $this;
}
/**
* @param bool $shouldCreateNewSheetsAutomatically Whether new sheets should be automatically created when the max rows limit per sheet is reached
* @return XLSX
*/
public function setShouldCreateNewSheetsAutomatically($shouldCreateNewSheetsAutomatically)
{
$this->shouldCreateNewSheetsAutomatically = $shouldCreateNewSheetsAutomatically;
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->book->addNewSheetAndMakeItCurrent();
}
}
/**
* Returns all the workbook's sheets
*
* @return 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->book->getWorksheets();
/** @var Internal\XLSX\Worksheet $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.
*
* @return 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->book->addNewSheetAndMakeItCurrent();
return $worksheet->getExternalSheet();
}
/**
* Returns the current sheet
*
* @return 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->book->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).
*
* @param 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->book->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->book) {
throw new WriterNotOpenedException('The writer must be opened before performing this action.');
}
}
/**
* 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'];
* @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)
{
$this->throwIfBookIsNotAvailable();
$this->book->addRowToCurrentWorksheet($dataRow);
}
/**
* Closes the writer, preventing any additional writing.
*
* @return void
*/
protected function closeWriter()
{
if ($this->book) {
$this->book->close($this->filePointer);
}
}
}