Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d617a50b85 | ||
|
|
92981fa80b | ||
|
|
766c733466 | ||
|
|
46cae76546 | ||
|
|
03be58b84d | ||
|
|
b21bb86682 | ||
|
|
8a45cec220 | ||
|
|
04d41d7c9f | ||
|
|
f8f194bc8f | ||
|
|
044ad11596 | ||
|
|
935ba1fb5e | ||
|
|
fb0175d633 | ||
|
|
69190cd855 | ||
|
|
13a53fb43f | ||
|
|
a848be52de | ||
|
|
52faef0168 | ||
|
|
3b4dfba38e | ||
|
|
cfd3e0ffa3 | ||
|
|
c6e943041e | ||
|
|
502dd54c81 | ||
|
|
c6cfc4c80f | ||
|
|
d02013c82e | ||
|
|
71d9c18a81 | ||
|
|
e9ec4e745c |
No files matched your search
@@ -1,11 +1,13 @@
|
||||
# Spout
|
||||
|
||||
[](https://packagist.org/packages/box/spout)
|
||||
[](http://opensource.box.com/badges)
|
||||
[](http://travis-ci.org/box/spout)
|
||||
[](https://packagist.org/packages/box/spout) [](https://packagist.org/packages/box/spout)
|
||||
|
||||
Spout is a PHP library to read and write CSV and XLSX files, in a fast and scalable way.
|
||||
Contrary to other file readers or writers, it is capable of processing very large files while keeping the memory usage really low (less than 10MB).
|
||||
|
||||
[](http://travis-ci.org/box/spout)
|
||||
[](http://opensource.box.com/badges)
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -180,6 +182,23 @@ It is also possible to retrieve all the sheets currently created:
|
||||
$sheets = $writer->getSheets();
|
||||
```
|
||||
|
||||
If you rely on the sheet's name in your application, you can access it and customize it this way:
|
||||
```php
|
||||
// Accessing the sheet name when reading
|
||||
while ($reader->hasNextSheet()) {
|
||||
$sheet = $reader->nextSheet();
|
||||
$sheetName = $sheet->getName();
|
||||
}
|
||||
|
||||
// Accessing the sheet name when writing
|
||||
$sheet = $writer->getCurrentSheet();
|
||||
$sheetName = $sheet->getName();
|
||||
|
||||
// Customizing the sheet name when writing
|
||||
$sheet = $writer->getCurrentSheet();
|
||||
$sheetName = $sheet->setName('My custom name');
|
||||
```
|
||||
|
||||
### Fluent interface
|
||||
|
||||
Because fluent interfaces are great, you can use them with Spout:
|
||||
@@ -231,6 +250,11 @@ Here are a few numbers regarding the performance of Spout:
|
||||
| Read XLSX (using shared strings) | 1 second | 1-2 minutes | 35-40 minutes |
|
||||
| Write XLSX | 1 second | 20-25 seconds | 8-10 minutes |
|
||||
|
||||
#### Does Spout support charts or formulas?
|
||||
|
||||
No. This is a compromise to keep memory usage low. Charts and formulas requires data to be kept in memory in order to be used.
|
||||
So the larger the file would be, the more memory would be consumed, preventing your code to scale well.
|
||||
|
||||
|
||||
## Support
|
||||
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "box/spout",
|
||||
"description": "PHP Library to read and write CSV and XLSX files, in a fast and scalable way",
|
||||
"type": "library",
|
||||
"version": "1.0.5",
|
||||
"version": "1.0.9",
|
||||
"keywords": ["php","read","write","csv","xlsx","excel","spreadsheet","scale","memory","stream","ooxml"],
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://www.github.com/box/spout",
|
||||
|
||||
@@ -91,6 +91,21 @@ class GlobalFunctionsHelper
|
||||
return fgetcsv($handle, $length, $delimiter, $enclosure);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around global function fputcsv()
|
||||
* @see fputcsv()
|
||||
*
|
||||
* @param resource $handle
|
||||
* @param array $fields
|
||||
* @param string|void $delimiter
|
||||
* @param string|void $enclosure
|
||||
* @return int
|
||||
*/
|
||||
public function fputcsv($handle, array $fields, $delimiter = null, $enclosure = null)
|
||||
{
|
||||
return fputcsv($handle, $fields, $delimiter, $enclosure);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around global function fclose()
|
||||
* @see fclose()
|
||||
@@ -119,7 +134,7 @@ class GlobalFunctionsHelper
|
||||
* Wrapper around global function file_exists()
|
||||
* @see file_exists()
|
||||
*
|
||||
* @param string $filename
|
||||
* @param string $fileName
|
||||
* @return bool
|
||||
*/
|
||||
public function file_exists($fileName)
|
||||
@@ -127,11 +142,23 @@ class GlobalFunctionsHelper
|
||||
return file_exists($fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around global function file_get_contents()
|
||||
* @see file_get_contents()
|
||||
*
|
||||
* @param string $filePath
|
||||
* @return bool
|
||||
*/
|
||||
public function file_get_contents($filePath)
|
||||
{
|
||||
return file_get_contents($filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around global function is_readable()
|
||||
* @see is_readable()
|
||||
*
|
||||
* @param string $filename
|
||||
* @param string $fileName
|
||||
* @return bool
|
||||
*/
|
||||
public function is_readable($fileName)
|
||||
@@ -144,11 +171,12 @@ class GlobalFunctionsHelper
|
||||
* @see basename()
|
||||
*
|
||||
* @param string $path
|
||||
* @param string|void $suffix
|
||||
* @return string
|
||||
*/
|
||||
public function basename($path)
|
||||
public function basename($path, $suffix = null)
|
||||
{
|
||||
return basename($path);
|
||||
return basename($path, $suffix);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,7 +14,7 @@ use Box\Spout\Reader\Exception\EndOfFileReachedException;
|
||||
*/
|
||||
abstract class AbstractReader implements ReaderInterface
|
||||
{
|
||||
/** @var int Used to keep track of the row number */
|
||||
/** @var int Used to keep track of the row index */
|
||||
protected $currentRowIndex = 0;
|
||||
|
||||
/** @var bool Indicates whether the stream is currently open */
|
||||
|
||||
@@ -71,6 +71,24 @@ class SharedStringsHelper
|
||||
$this->tempFolder = $this->fileSystemHelper->createFolder($rootTempFolder, uniqid('sharedstrings'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the XLSX file contains a shared strings XML file
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasSharedStrings()
|
||||
{
|
||||
$hasSharedStrings = false;
|
||||
$zip = new \ZipArchive();
|
||||
|
||||
if ($zip->open($this->filePath) === true) {
|
||||
$hasSharedStrings = ($zip->locateName(self::SHARED_STRINGS_XML_FILE_PATH) !== false);
|
||||
$zip->close();
|
||||
}
|
||||
|
||||
return $hasSharedStrings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an in-memory array containing all the shared strings of the worksheet.
|
||||
* All the strings are stored in a XML file, located at 'xl/sharedStrings.xml'.
|
||||
@@ -93,7 +111,7 @@ class SharedStringsHelper
|
||||
$this->tempFilePointer = null;
|
||||
$escaper = new \Box\Spout\Common\Escaper\XLSX();
|
||||
|
||||
$sharedStringsFilePath = 'zip://' . $this->filePath . '#' . self::SHARED_STRINGS_XML_FILE_PATH;
|
||||
$sharedStringsFilePath = $this->getSharedStringsFilePath();
|
||||
if ($xmlReader->open($sharedStringsFilePath, null, LIBXML_NONET) === false) {
|
||||
throw new IOException('Could not open "' . self::SHARED_STRINGS_XML_FILE_PATH . '".');
|
||||
}
|
||||
@@ -143,6 +161,15 @@ class SharedStringsHelper
|
||||
$xmlReader->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string The path to the shared strings XML file, working cross-platforms
|
||||
*/
|
||||
protected function getSharedStringsFilePath()
|
||||
{
|
||||
$sharedStringsXmlFilePath = str_replace('/', DIRECTORY_SEPARATOR, self::SHARED_STRINGS_XML_FILE_PATH);
|
||||
return 'zip://' . $this->filePath . '#' . $sharedStringsXmlFilePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace Box\Spout\Reader\Helper\XLSX;
|
||||
|
||||
use Box\Spout\Reader\Internal\XLSX\Worksheet;
|
||||
use Box\Spout\Reader\Sheet;
|
||||
|
||||
/**
|
||||
* Class WorksheetHelper
|
||||
@@ -12,11 +13,18 @@ use Box\Spout\Reader\Internal\XLSX\Worksheet;
|
||||
*/
|
||||
class WorksheetHelper
|
||||
{
|
||||
/** Path of Content_Types XML file inside the XLSX file */
|
||||
const CONTENT_TYPES_XML_FILE_PATH = '[Content_Types].xml';
|
||||
/** Extension for XML files */
|
||||
const XML_EXTENSION = '.xml';
|
||||
|
||||
/** Main namespace for the [Content_Types].xml file */
|
||||
/** Paths of XML files relative to the XLSX file root */
|
||||
const CONTENT_TYPES_XML_FILE_PATH = '[Content_Types].xml';
|
||||
const WORKBOOK_XML_RELS_FILE_PATH = 'xl/_rels/workbook.xml.rels';
|
||||
const WORKBOOK_XML_FILE_PATH = 'xl/workbook.xml';
|
||||
|
||||
/** Namespaces for the XML files */
|
||||
const MAIN_NAMESPACE_FOR_CONTENT_TYPES_XML = 'http://schemas.openxmlformats.org/package/2006/content-types';
|
||||
const MAIN_NAMESPACE_FOR_WORKBOOK_XML_RELS = 'http://schemas.openxmlformats.org/package/2006/relationships';
|
||||
const MAIN_NAMESPACE_FOR_WORKBOOK_XML = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
|
||||
|
||||
/** 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';
|
||||
@@ -24,12 +32,23 @@ class WorksheetHelper
|
||||
/** @var string Path of the XLSX file being read */
|
||||
protected $filePath;
|
||||
|
||||
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
|
||||
protected $globalFunctionsHelper;
|
||||
|
||||
/** @var \SimpleXMLElement XML element representing the workbook.xml.rels file */
|
||||
protected $workbookXMLRelsAsXMLElement;
|
||||
|
||||
/** @var \SimpleXMLElement XML element representing the workbook.xml file */
|
||||
protected $workbookXMLAsXMLElement;
|
||||
|
||||
/**
|
||||
* @param string $filePath Path of the XLSX file being read
|
||||
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
|
||||
*/
|
||||
public function __construct($filePath)
|
||||
public function __construct($filePath, $globalFunctionsHelper)
|
||||
{
|
||||
$this->filePath = $filePath;
|
||||
$this->globalFunctionsHelper = $globalFunctionsHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,23 +61,140 @@ class WorksheetHelper
|
||||
{
|
||||
$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);
|
||||
$contentTypesAsXMLElement = $this->getFileAsXMLElementWithNamespace(
|
||||
self::CONTENT_TYPES_XML_FILE_PATH,
|
||||
self::MAIN_NAMESPACE_FOR_CONTENT_TYPES_XML
|
||||
);
|
||||
|
||||
// find all nodes defining a worksheet
|
||||
$sheetNodes = $contentTypes->xpath('//ns:Override[@ContentType="' . self::OVERRIDE_CONTENT_TYPES_ATTRIBUTE . '"]');
|
||||
$sheetNodes = $contentTypesAsXMLElement->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);
|
||||
|
||||
$sheet = $this->getSheet($sheetDataXMLFilePath, $i);
|
||||
$worksheets[] = new Worksheet($sheet, $i, $sheetDataXMLFilePath);
|
||||
}
|
||||
|
||||
return $worksheets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an instance of a sheet, given the path of its data XML file.
|
||||
* We first look at "xl/_rels/workbook.xml.rels" to find the relationship ID of the sheet.
|
||||
* Then we look at "xl/worbook.xml" to find the sheet entry associated to the found ID.
|
||||
* The entry contains the ID and name of the sheet.
|
||||
*
|
||||
* If this piece of data can't be found by parsing the different XML files, the ID will default
|
||||
* to the sheet index, based on order in [Content_Types].xml. Similarly, the sheet's name will
|
||||
* default to the data sheet XML file name ("xl/worksheets/sheet2.xml" => "sheet2").
|
||||
*
|
||||
* @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml
|
||||
* @param int $sheetIndexZeroBased Index of the sheet, based on order in [Content_Types].xml (zero-based)
|
||||
* @return \Box\Spout\Reader\Sheet Sheet instance
|
||||
*/
|
||||
protected function getSheet($sheetDataXMLFilePath, $sheetIndexZeroBased)
|
||||
{
|
||||
$sheetId = $sheetIndexZeroBased + 1;
|
||||
$sheetName = $this->getDefaultSheetName($sheetDataXMLFilePath);
|
||||
|
||||
/*
|
||||
* In [Content_Types].xml, the path is "/xl/worksheets/sheet1.xml"
|
||||
* In workbook.xml.rels, it is only "worksheets/sheet1.xml"
|
||||
*/
|
||||
$sheetDataXMLFilePathInWorkbookXMLRels = ltrim($sheetDataXMLFilePath, '/xl/');
|
||||
|
||||
// find the node associated to the given file path
|
||||
$workbookXMLResElement = $this->getWorkbookXMLRelsAsXMLElement();
|
||||
$relationshipNodes = $workbookXMLResElement->xpath('//ns:Relationship[@Target="' . $sheetDataXMLFilePathInWorkbookXMLRels . '"]');
|
||||
|
||||
if (count($relationshipNodes) === 1) {
|
||||
$relationshipNode = $relationshipNodes[0];
|
||||
$sheetId = (string) $relationshipNode->attributes()->Id;
|
||||
|
||||
$workbookXMLElement = $this->getWorkbookXMLAsXMLElement();
|
||||
$sheetNodes = $workbookXMLElement->xpath('//ns:sheet[@r:id="' . $sheetId . '"]');
|
||||
|
||||
if (count($sheetNodes) === 1) {
|
||||
$sheetNode = $sheetNodes[0];
|
||||
$sheetId = (int) $sheetNode->attributes()->sheetId;
|
||||
$escapedSheetName = (string) $sheetNode->attributes()->name;
|
||||
|
||||
$escaper = new \Box\Spout\Common\Escaper\XLSX();
|
||||
$sheetName = $escaper->unescape($escapedSheetName);
|
||||
}
|
||||
}
|
||||
|
||||
return new Sheet($sheetId, $sheetIndexZeroBased, $sheetName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default name of the sheet whose data is located
|
||||
* at the given path.
|
||||
*
|
||||
* @param $sheetDataXMLFilePath
|
||||
* @return string The default sheet name
|
||||
*/
|
||||
protected function getDefaultSheetName($sheetDataXMLFilePath)
|
||||
{
|
||||
return $this->globalFunctionsHelper->basename($sheetDataXMLFilePath, self::XML_EXTENSION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a representation of the workbook.xml.rels file, ready to be parsed.
|
||||
* The returned value is cached.
|
||||
*
|
||||
* @return \SimpleXMLElement XML element representating the workbook.xml.rels file
|
||||
*/
|
||||
protected function getWorkbookXMLRelsAsXMLElement()
|
||||
{
|
||||
if (!$this->workbookXMLRelsAsXMLElement) {
|
||||
$this->workbookXMLRelsAsXMLElement = $this->getFileAsXMLElementWithNamespace(
|
||||
self::WORKBOOK_XML_RELS_FILE_PATH,
|
||||
self::MAIN_NAMESPACE_FOR_WORKBOOK_XML_RELS
|
||||
);
|
||||
}
|
||||
|
||||
return $this->workbookXMLRelsAsXMLElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a representation of the workbook.xml file, ready to be parsed.
|
||||
* The returned value is cached.
|
||||
*
|
||||
* @return \SimpleXMLElement XML element representating the workbook.xml.rels file
|
||||
*/
|
||||
protected function getWorkbookXMLAsXMLElement()
|
||||
{
|
||||
if (!$this->workbookXMLAsXMLElement) {
|
||||
$this->workbookXMLAsXMLElement = $this->getFileAsXMLElementWithNamespace(
|
||||
self::WORKBOOK_XML_FILE_PATH,
|
||||
self::MAIN_NAMESPACE_FOR_WORKBOOK_XML
|
||||
);
|
||||
}
|
||||
|
||||
return $this->workbookXMLAsXMLElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the contents of the given file in an XML parser and register the given XPath namespace.
|
||||
*
|
||||
* @param string $xmlFilePath The path of the XML file inside the XLSX file
|
||||
* @param string $mainNamespace The main XPath namespace to register
|
||||
* @return \SimpleXMLElement The XML element representing the file
|
||||
*/
|
||||
protected function getFileAsXMLElementWithNamespace($xmlFilePath, $mainNamespace)
|
||||
{
|
||||
$normalizedXmlFilePath = str_replace('/', DIRECTORY_SEPARATOR, $xmlFilePath);
|
||||
$xmlContents = $this->globalFunctionsHelper->file_get_contents('zip://' . $this->filePath . '#' . $normalizedXmlFilePath);
|
||||
|
||||
$xmlElement = new \SimpleXMLElement($xmlContents);
|
||||
$xmlElement->registerXPathNamespace('ns', $mainNamespace);
|
||||
|
||||
return $xmlElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether another worksheet exists after the current worksheet.
|
||||
* The order is determined by the order of appearance in the [Content_Types].xml file.
|
||||
@@ -69,6 +205,6 @@ class WorksheetHelper
|
||||
*/
|
||||
public function hasNextWorksheet($currentWorksheet, $allWorksheets)
|
||||
{
|
||||
return ($currentWorksheet === null || ($currentWorksheet->getWorksheetNumber() + 1 < count($allWorksheets)));
|
||||
return ($currentWorksheet === null || ($currentWorksheet->getWorksheetIndex() + 1 < count($allWorksheets)));
|
||||
}
|
||||
}
|
||||
@@ -10,35 +10,50 @@ namespace 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 \Box\Spout\Reader\Sheet The "external" sheet */
|
||||
protected $externalSheet;
|
||||
|
||||
/** @var int Worksheet index, based on the order of appareance in [Content_Types].xml (zero-based) */
|
||||
protected $worksheetIndex;
|
||||
|
||||
/** @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 \Box\Spout\Reader\Sheet $externalSheet The associated "external" sheet
|
||||
* @param int $worksheetIndex Worksheet index, 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)
|
||||
public function __construct($externalSheet, $worksheetIndex, $dataXmlFilePath)
|
||||
{
|
||||
$this->worksheetNumber = $worksheetNumber;
|
||||
$this->externalSheet = $externalSheet;
|
||||
$this->worksheetIndex = $worksheetIndex;
|
||||
$this->dataXmlFilePath = $dataXmlFilePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Path of the XML file containing the worksheet data, without the leading slash
|
||||
* @return string Path of the XML file containing the worksheet data,
|
||||
* without the leading slash and working cross-platforms.
|
||||
*/
|
||||
public function getDataXmlFilePath()
|
||||
{
|
||||
return ltrim($this->dataXmlFilePath, '/');
|
||||
$dataXmlFilePath = ltrim($this->dataXmlFilePath, '/');
|
||||
return str_replace('/', DIRECTORY_SEPARATOR, $dataXmlFilePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Box\Spout\Reader\Sheet The "external" sheet
|
||||
*/
|
||||
public function getExternalSheet()
|
||||
{
|
||||
return $this->externalSheet;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getWorksheetNumber()
|
||||
public function getWorksheetIndex()
|
||||
{
|
||||
return $this->worksheetNumber;
|
||||
return $this->worksheetIndex;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader;
|
||||
|
||||
/**
|
||||
* Class Sheet
|
||||
* Represents a worksheet within a XLSX file
|
||||
*
|
||||
* @package Box\Spout\Reader
|
||||
*/
|
||||
class Sheet
|
||||
{
|
||||
/** @var int ID of the sheet */
|
||||
protected $id;
|
||||
|
||||
/** @var int Index of the sheet, based on order of creation (zero-based) */
|
||||
protected $index;
|
||||
|
||||
/** @var string Name of the sheet */
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* @param int $sheetId ID of the sheet
|
||||
* @param int $sheetIndex Index of the sheet, based on order of creation (zero-based)
|
||||
* @param string $sheetName Name of the sheet
|
||||
*/
|
||||
function __construct($sheetId, $sheetIndex, $sheetName)
|
||||
{
|
||||
$this->id = $sheetId;
|
||||
$this->index = $sheetIndex;
|
||||
$this->name = $sheetName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int ID of the sheet
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int Index of the sheet, based on order of creation (zero-based)
|
||||
*/
|
||||
public function getIndex()
|
||||
{
|
||||
return $this->index;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Name of the sheet
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
}
|
||||
+41
-36
@@ -72,11 +72,15 @@ class XLSX extends AbstractReader
|
||||
$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);
|
||||
$this->sharedStringsHelper = new SharedStringsHelper($filePath, $this->tempFolder);
|
||||
|
||||
if ($this->sharedStringsHelper->hasSharedStrings()) {
|
||||
// Extracts all the strings from the worksheets for easy access in the future
|
||||
$this->sharedStringsHelper->extractSharedStrings();
|
||||
}
|
||||
|
||||
// Fetch all available worksheets
|
||||
$this->worksheetHelper = new WorksheetHelper($filePath);
|
||||
$this->worksheetHelper = new WorksheetHelper($filePath, $this->globalFunctionsHelper);
|
||||
$this->worksheets = $this->worksheetHelper->getWorksheets($filePath);
|
||||
|
||||
if (count($this->worksheets) === 0) {
|
||||
@@ -87,19 +91,6 @@ class XLSX extends AbstractReader
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
@@ -119,29 +110,31 @@ class XLSX extends AbstractReader
|
||||
* Moves the pointer to the current worksheet.
|
||||
* Moving to another worksheet will stop the reading in the current worksheet.
|
||||
*
|
||||
* @return void
|
||||
* @return \Box\Spout\Reader\Sheet The next sheet
|
||||
* @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 {
|
||||
if (!$this->hasNextSheet()) {
|
||||
throw new EndOfWorksheetsReachedException('End of worksheets was reached. Cannot read more worksheets.');
|
||||
}
|
||||
|
||||
if ($this->currentWorksheet === null) {
|
||||
$nextWorksheet = $this->worksheets[0];
|
||||
} else {
|
||||
$currentWorksheetIndex = $this->currentWorksheet->getWorksheetIndex();
|
||||
$nextWorksheet = $this->worksheets[$currentWorksheetIndex + 1];
|
||||
}
|
||||
|
||||
$this->initXmlReaderForWorksheetData($nextWorksheet);
|
||||
$this->currentWorksheet = $nextWorksheet;
|
||||
|
||||
// make sure that we are ready to read more rows
|
||||
$this->hasReachedEndOfFile = false;
|
||||
$this->emptyRowDataBuffer();
|
||||
|
||||
return $this->currentWorksheet->getExternalSheet();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -213,13 +206,25 @@ class XLSX extends AbstractReader
|
||||
$currentColumnIndex = CellHelper::getColumnIndexFromCellIndex($currentCellIndex);
|
||||
$node = $this->xmlReader->expand();
|
||||
|
||||
$hasInlineString = ($this->xmlReader->getAttribute('t') === 'inlineStr');
|
||||
$hasSharedString = ($this->xmlReader->getAttribute('t') === 's');
|
||||
if ($hasSharedString) {
|
||||
$sharedStringIndex = intval($node->nodeValue);
|
||||
|
||||
if ($hasInlineString) {
|
||||
// inline strings are formatted this way:
|
||||
// <c r="A1" t="inlineStr"><is><t>[INLINE_STRING]</t></is></c>
|
||||
$tNode = $node->getElementsByTagName('t')->item(0);
|
||||
$rowData[$currentColumnIndex] = trim($tNode->nodeValue);
|
||||
} else if ($hasSharedString) {
|
||||
// shared strings are formatted this way:
|
||||
// <c r="A1" t="s"><v>[SHARED_STRING_INDEX]</v></c>
|
||||
$vNode = $node->getElementsByTagName('v')->item(0);
|
||||
$sharedStringIndex = intval($vNode->nodeValue);
|
||||
$rowData[$currentColumnIndex] = $this->sharedStringsHelper->getStringAtIndex($sharedStringIndex);
|
||||
} else {
|
||||
// for inline strings or numbers, just get the value
|
||||
$rowData[$currentColumnIndex] = trim($node->nodeValue);
|
||||
// other values are formatted this way:
|
||||
// <c r="A1"><v>[VALUE]</v></c>
|
||||
$vNode = $node->getElementsByTagName('v')->item(0);
|
||||
$rowData[$currentColumnIndex] = intval($vNode->nodeValue);
|
||||
}
|
||||
} else if ($this->xmlReader->nodeType == \XMLReader::END_ELEMENT && $this->xmlReader->name === 'row') {
|
||||
// End of the row description
|
||||
|
||||
@@ -73,7 +73,7 @@ class CSV extends AbstractWriter
|
||||
*/
|
||||
protected function addRowToWriter(array $dataRow)
|
||||
{
|
||||
$wasWriteSuccessful = fputcsv($this->filePointer, $dataRow, $this->fieldDelimiter, $this->fieldEnclosure);
|
||||
$wasWriteSuccessful = $this->globalFunctionsHelper->fputcsv($this->filePointer, $dataRow, $this->fieldDelimiter, $this->fieldEnclosure);
|
||||
if ($wasWriteSuccessful === false) {
|
||||
throw new IOException('Unable to write data');
|
||||
}
|
||||
|
||||
@@ -67,8 +67,8 @@ class Workbook
|
||||
*/
|
||||
public function addNewSheet()
|
||||
{
|
||||
$newSheetNumber = count($this->worksheets);
|
||||
$sheet = new Sheet($newSheetNumber);
|
||||
$newSheetIndex = count($this->worksheets);
|
||||
$sheet = new Sheet($newSheetIndex);
|
||||
|
||||
$worksheetFilesFolder = $this->fileSystemHelper->getXlWorksheetsFolder();
|
||||
$worksheet = new Worksheet($sheet, $worksheetFilesFolder, $this->sharedStringsHelper, $this->shouldUseInlineStrings);
|
||||
|
||||
@@ -96,8 +96,8 @@ EOD;
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
// sheet number is zero-based, while ID is 1-based
|
||||
return $this->externalSheet->getSheetNumber() + 1;
|
||||
// sheet index is zero-based, while ID is 1-based
|
||||
return $this->externalSheet->getIndex() + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,27 +12,27 @@ class Sheet
|
||||
{
|
||||
const DEFAULT_SHEET_NAME_PREFIX = 'Sheet';
|
||||
|
||||
/** @var int Number of the sheet, based on order of creation (zero-based) */
|
||||
protected $sheetNumber;
|
||||
/** @var int Index of the sheet, based on order of creation (zero-based) */
|
||||
protected $index;
|
||||
|
||||
/** @var string Name of the sheet */
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* @param $sheetNumber Number of the sheet, based on order of creation (zero-based)
|
||||
* @param int $sheetIndex Index of the sheet, based on order of creation (zero-based)
|
||||
*/
|
||||
function __construct($sheetNumber)
|
||||
function __construct($sheetIndex)
|
||||
{
|
||||
$this->sheetNumber = $sheetNumber;
|
||||
$this->name = self::DEFAULT_SHEET_NAME_PREFIX . ($sheetNumber + 1);
|
||||
$this->index = $sheetIndex;
|
||||
$this->name = self::DEFAULT_SHEET_NAME_PREFIX . ($sheetIndex + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int Number of the sheet, based on order of creation (zero-based)
|
||||
* @return int Index of the sheet, based on order of creation (zero-based)
|
||||
*/
|
||||
public function getSheetNumber()
|
||||
public function getIndex()
|
||||
{
|
||||
return $this->sheetNumber;
|
||||
return $this->index;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,4 +42,14 @@ class Sheet
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name Name of the sheet
|
||||
* @return \Box\Spout\Writer\Sheet
|
||||
*/
|
||||
public function setName($name)
|
||||
{
|
||||
$this->name = $name;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader;
|
||||
|
||||
use Box\Spout\Common\Type;
|
||||
use Box\Spout\TestUsingResource;
|
||||
|
||||
/**
|
||||
* Class SheetTest
|
||||
*
|
||||
* @package Box\Spout\Reader
|
||||
*/
|
||||
class SheetTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
use TestUsingResource;
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testNextSheetShouldReturnCorrectSheetInfos()
|
||||
{
|
||||
$sheets = $this->openFileAndReturnSheets('two_sheets_with_custom_names.xlsx');
|
||||
|
||||
$this->assertEquals('CustomName1', $sheets[0]->getName());
|
||||
$this->assertEquals(0, $sheets[0]->getIndex());
|
||||
$this->assertEquals(1, $sheets[0]->getId());
|
||||
|
||||
$this->assertEquals('CustomName2', $sheets[1]->getName());
|
||||
$this->assertEquals(1, $sheets[1]->getIndex());
|
||||
$this->assertEquals(2, $sheets[1]->getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $fileName
|
||||
* @return Sheet[]
|
||||
*/
|
||||
private function openFileAndReturnSheets($fileName)
|
||||
{
|
||||
$resourcePath = $this->getResourcePath($fileName);
|
||||
$reader = ReaderFactory::create(Type::XLSX);
|
||||
$reader->open($resourcePath);
|
||||
|
||||
$sheets = [];
|
||||
while ($reader->hasNextSheet()) {
|
||||
$sheets[] = $reader->nextSheet();
|
||||
}
|
||||
|
||||
$reader->close();
|
||||
|
||||
return $sheets;
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,37 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
|
||||
$this->getAllRowsForFile($filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Box\Spout\Reader\Exception\ReaderNotOpenedException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testHasNextSheetShouldThrowExceptionIfReaderNotOpened()
|
||||
{
|
||||
$reader = ReaderFactory::create(Type::XLSX);
|
||||
$reader->hasNextSheet();
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Box\Spout\Reader\Exception\EndOfWorksheetsReachedException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testNextSheetShouldThrowExceptionIfNoMoreSheetsToRead()
|
||||
{
|
||||
$fileName = 'one_sheet_with_shared_strings.xlsx';
|
||||
$resourcePath = $this->getResourcePath($fileName);
|
||||
|
||||
$reader = ReaderFactory::create(Type::XLSX);
|
||||
$reader->open($resourcePath);
|
||||
|
||||
while ($reader->hasNextSheet()) {
|
||||
$reader->nextSheet();
|
||||
}
|
||||
|
||||
$reader->nextSheet();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
@@ -69,6 +100,20 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testReadShouldSupportFilesWithoutSharedStringsFile()
|
||||
{
|
||||
$allRows = $this->getAllRowsForFile('sheet_with_no_shared_strings_file.xlsx');
|
||||
|
||||
$expectedRows = [
|
||||
[10, 11],
|
||||
[20, 21],
|
||||
];
|
||||
$this->assertEquals($expectedRows, $allRows);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
@@ -200,6 +245,21 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals([], $allRows, 'Sheet with no cells should be correctly processed.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testReadShouldSkipFormulas()
|
||||
{
|
||||
$allRows = $this->getAllRowsForFile('sheet_with_formulas.xlsx');
|
||||
|
||||
$expectedRows = [
|
||||
['val1', 'val2', 'total1', 'total2'],
|
||||
[10, 20, 30, 21],
|
||||
[11, 21, 32, 41],
|
||||
];
|
||||
$this->assertEquals($expectedRows, $allRows);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $fileName
|
||||
* @return array All the read rows the given file
|
||||
|
||||
@@ -17,13 +17,13 @@ class SheetTest extends \PHPUnit_Framework_TestCase
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testGetSheetNumber()
|
||||
public function testGetSheetIndex()
|
||||
{
|
||||
$sheets = $this->writeDataAndReturnSheets('test_get_sheet_number.xlsx');
|
||||
$sheets = $this->writeDataAndReturnSheets('test_get_sheet_index.xlsx');
|
||||
|
||||
$this->assertEquals(2, count($sheets), '2 sheets should have been created');
|
||||
$this->assertEquals(0, $sheets[0]->getSheetNumber(), 'The first sheet should be number 0');
|
||||
$this->assertEquals(1, $sheets[1]->getSheetNumber(), 'The second sheet should be number 1');
|
||||
$this->assertEquals(0, $sheets[0]->getIndex(), 'The first sheet should be index 0');
|
||||
$this->assertEquals(1, $sheets[1]->getIndex(), 'The second sheet should be index 1');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -38,6 +38,28 @@ class SheetTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals('Sheet2', $sheets[1]->getName(), 'Invalid name for the second sheet');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testSetSheetNameShouldCreateSheetWithCustomName()
|
||||
{
|
||||
$fileName = 'test_set_name_should_create_sheet_with_custom_name.xlsx';
|
||||
$this->createGeneratedFolderIfNeeded($fileName);
|
||||
$resourcePath = $this->getGeneratedResourcePath($fileName);
|
||||
|
||||
$writer = WriterFactory::create(Type::XLSX);
|
||||
$writer->openToFile($resourcePath);
|
||||
|
||||
$customSheetName = 'CustomName';
|
||||
$sheet = $writer->getCurrentSheet();
|
||||
$sheet->setName($customSheetName);
|
||||
|
||||
$writer->addRow(['xlsx--11', 'xlsx--12']);
|
||||
$writer->close();
|
||||
|
||||
$this->assertSheetNameEquals($customSheetName, $resourcePath, "The sheet name should have been changed to '$customSheetName'");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $fileName
|
||||
* @return Sheet[]
|
||||
@@ -59,4 +81,27 @@ class SheetTest extends \PHPUnit_Framework_TestCase
|
||||
|
||||
return $writer->getSheets();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $expectedName
|
||||
* @param string $resourcePath
|
||||
* @param string $message
|
||||
* @return void
|
||||
*/
|
||||
private function assertSheetNameEquals($expectedName, $resourcePath, $message = '')
|
||||
{
|
||||
$pathToWorkbookFile = $resourcePath . $this->normalizePath('#xl/workbook.xml');
|
||||
$xmlContents = file_get_contents('zip://' . $pathToWorkbookFile);
|
||||
|
||||
$this->assertContains('<sheet name="' . $expectedName . '"', $xmlContents, $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @return string The path with the correct directory separators, as defined for the current platform
|
||||
*/
|
||||
private function normalizePath($path)
|
||||
{
|
||||
return str_replace('/', DIRECTORY_SEPARATOR, $path);
|
||||
}
|
||||
}
|
||||
@@ -343,7 +343,6 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertInlineDataWasWrittenToSheet($fileName, 1, 'control's _x0015_ "character"');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param array $allRows
|
||||
* @param string $fileName
|
||||
@@ -401,15 +400,15 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
|
||||
|
||||
/**
|
||||
* @param string $fileName
|
||||
* @param int $sheetNumber
|
||||
* @param int $sheetIndex
|
||||
* @param mixed $inlineData
|
||||
* @param string $message
|
||||
* @return void
|
||||
*/
|
||||
private function assertInlineDataWasWrittenToSheet($fileName, $sheetNumber, $inlineData, $message = '')
|
||||
private function assertInlineDataWasWrittenToSheet($fileName, $sheetIndex, $inlineData, $message = '')
|
||||
{
|
||||
$resourcePath = $this->getGeneratedResourcePath($fileName);
|
||||
$pathToSheetFile = $resourcePath . '#xl/worksheets/sheet' . $sheetNumber . '.xml';
|
||||
$pathToSheetFile = $resourcePath . $this->normalizePath('#xl/worksheets/sheet' . $sheetIndex . '.xml');
|
||||
$xmlContents = file_get_contents('zip://' . $pathToSheetFile);
|
||||
|
||||
$this->assertContains((string)$inlineData, $xmlContents, $message);
|
||||
@@ -417,15 +416,15 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
|
||||
|
||||
/**
|
||||
* @param string $fileName
|
||||
* @param int $sheetNumber
|
||||
* @param int $sheetIndex
|
||||
* @param mixed $inlineData
|
||||
* @param string $message
|
||||
* @return void
|
||||
*/
|
||||
private function assertInlineDataWasNotWrittenToSheet($fileName, $sheetNumber, $inlineData, $message = '')
|
||||
private function assertInlineDataWasNotWrittenToSheet($fileName, $sheetIndex, $inlineData, $message = '')
|
||||
{
|
||||
$resourcePath = $this->getGeneratedResourcePath($fileName);
|
||||
$pathToSheetFile = $resourcePath . '#xl/worksheets/sheet' . $sheetNumber . '.xml';
|
||||
$pathToSheetFile = $resourcePath . $this->normalizePath('#xl/worksheets/sheet' . $sheetIndex . '.xml');
|
||||
$xmlContents = file_get_contents('zip://' . $pathToSheetFile);
|
||||
|
||||
$this->assertNotContains((string)$inlineData, $xmlContents, $message);
|
||||
@@ -440,9 +439,18 @@ class XLSXTest extends \PHPUnit_Framework_TestCase
|
||||
private function assertSharedStringWasWritten($fileName, $sharedString, $message = '')
|
||||
{
|
||||
$resourcePath = $this->getGeneratedResourcePath($fileName);
|
||||
$pathToSharedStringsFile = $resourcePath . '#xl/sharedStrings.xml';
|
||||
$pathToSharedStringsFile = $resourcePath . $this->normalizePath('#xl/sharedStrings.xml');
|
||||
$xmlContents = file_get_contents('zip://' . $pathToSharedStringsFile);
|
||||
|
||||
$this->assertContains($sharedString, $xmlContents, $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @return string The path with the correct directory separators, as defined for the current platform
|
||||
*/
|
||||
private function normalizePath($path)
|
||||
{
|
||||
return str_replace('/', DIRECTORY_SEPARATOR, $path);
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in new issue
Block a user