spout/src/Spout/Writer/ODS/Helper/FileSystemHelper.php
Adrien Loison 156fd29a44 Improve ODS Writer
Remove num-columns-repeated and num-rows-repeated as it does not seem to be required (LibreOffice does not add them).
This greatly simplifies the writer and the XML output.
Added some optional attributes to help LibreOffice with cell values caching ("calcext")
2015-08-31 09:55:17 -07:00

284 lines
10 KiB
PHP

<?php
namespace Box\Spout\Writer\ODS\Helper;
use Box\Spout\Writer\Common\Helper\ZipHelper;
use Box\Spout\Writer\ODS\Internal\Worksheet;
/**
* Class FileSystemHelper
* This class provides helper functions to help with the file system operations
* like files/folders creation & deletion for ODS files
*
* @package Box\Spout\Writer\ODS\Helper
*/
class FileSystemHelper extends \Box\Spout\Common\Helper\FileSystemHelper
{
const APP_NAME = 'Spout';
const MIMETYPE = 'application/vnd.oasis.opendocument.spreadsheet';
const META_INF_FOLDER_NAME = 'META-INF';
const SHEETS_CONTENT_TEMP_FOLDER_NAME = 'worksheets-temp';
const MANIFEST_XML_FILE_NAME = 'manifest.xml';
const CONTENT_XML_FILE_NAME = 'content.xml';
const META_XML_FILE_NAME = 'meta.xml';
const MIMETYPE_FILE_NAME = 'mimetype';
const STYLES_XML_FILE_NAME = 'styles.xml';
/** @var string Path to the root folder inside the temp folder where the files to create the ODS will be stored */
protected $rootFolder;
/** @var string Path to the "META-INF" folder inside the root folder */
protected $metaInfFolder;
/** @var string Path to the temp folder, inside the root folder, where specific sheets content will be written to */
protected $sheetsContentTempFolder;
/**
* @return string
*/
public function getRootFolder()
{
return $this->rootFolder;
}
/**
* @return string
*/
public function getSheetsContentTempFolder()
{
return $this->sheetsContentTempFolder;
}
/**
* Creates all the folders needed to create a ODS file, as well as the files that won't change.
*
* @return void
* @throws \Box\Spout\Common\Exception\IOException If unable to create at least one of the base folders
*/
public function createBaseFilesAndFolders()
{
$this
->createRootFolder()
->createMetaInfoFolderAndFile()
->createSheetsContentTempFolder()
->createMetaFile()
->createMimetypeFile();
}
/**
* Creates the folder that will be used as root
*
* @return FileSystemHelper
* @throws \Box\Spout\Common\Exception\IOException If unable to create the folder
*/
protected function createRootFolder()
{
$this->rootFolder = $this->createFolder($this->baseFolderPath, uniqid('ods'));
return $this;
}
/**
* Creates the "META-INF" folder under the root folder as well as the "manifest.xml" file in it
*
* @return FileSystemHelper
* @throws \Box\Spout\Common\Exception\IOException If unable to create the folder or the "manifest.xml" file
*/
protected function createMetaInfoFolderAndFile()
{
$this->metaInfFolder = $this->createFolder($this->rootFolder, self::META_INF_FOLDER_NAME);
$this->createManifestFile();
return $this;
}
/**
* Creates the "manifest.xml" file under the "META-INF" folder (under root)
*
* @return FileSystemHelper
* @throws \Box\Spout\Common\Exception\IOException If unable to create the file
*/
protected function createManifestFile()
{
$manifestXmlFileContents = <<<EOD
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<manifest:manifest xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0">
<manifest:file-entry manifest:full-path="/" manifest:media-type="application/vnd.oasis.opendocument.spreadsheet"/>
<manifest:file-entry manifest:full-path="styles.xml" manifest:media-type="text/xml"/>
<manifest:file-entry manifest:full-path="content.xml" manifest:media-type="text/xml"/>
<manifest:file-entry manifest:full-path="meta.xml" manifest:media-type="text/xml"/>
<manifest:file-entry manifest:full-path="mimetype" manifest:media-type="text/plain"/>
<manifest:file-entry manifest:full-path="META-INF/manifest.xml" manifest:media-type="text/xml"/>
</manifest:manifest>
EOD;
$this->createFileWithContents($this->metaInfFolder, self::MANIFEST_XML_FILE_NAME, $manifestXmlFileContents);
return $this;
}
/**
* Creates the temp folder where specific sheets content will be written to.
* This folder is not part of the final ODS file and is only used to be able to jump between sheets.
*
* @return FileSystemHelper
* @throws \Box\Spout\Common\Exception\IOException If unable to create the folder
*/
protected function createSheetsContentTempFolder()
{
$this->sheetsContentTempFolder = $this->createFolder($this->rootFolder, self::SHEETS_CONTENT_TEMP_FOLDER_NAME);
return $this;
}
/**
* Creates the "meta.xml" file under the root folder
*
* @return FileSystemHelper
* @throws \Box\Spout\Common\Exception\IOException If unable to create the file
*/
protected function createMetaFile()
{
$appName = self::APP_NAME;
$createdDate = (new \DateTime())->format(\DateTime::W3C);
$metaXmlFileContents = <<<EOD
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<office:document-meta office:version="1.2" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:xlink="http://www.w3.org/1999/xlink">
<office:meta>
<dc:creator>$appName</dc:creator>
<meta:creation-date>$createdDate</meta:creation-date>
<dc:date>$createdDate</dc:date>
</office:meta>
</office:document-meta>
EOD;
$this->createFileWithContents($this->rootFolder, self::META_XML_FILE_NAME, $metaXmlFileContents);
return $this;
}
/**
* Creates the "mimetype" file under the root folder
*
* @return FileSystemHelper
* @throws \Box\Spout\Common\Exception\IOException If unable to create the file
*/
protected function createMimetypeFile()
{
$this->createFileWithContents($this->rootFolder, self::MIMETYPE_FILE_NAME, self::MIMETYPE);
return $this;
}
/**
* Creates the "content.xml" file under the root folder
*
* @param Worksheet[] $worksheets
* @param StyleHelper $styleHelper
* @return FileSystemHelper
*/
public function createContentFile($worksheets, $styleHelper)
{
$contentXmlFileContents = <<<EOD
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<office:document-content office:version="1.2" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:calcext="urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0" xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:msoxl="http://schemas.microsoft.com/office/excel/formula" xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:xlink="http://www.w3.org/1999/xlink">
EOD;
$contentXmlFileContents .= $styleHelper->getContentXmlFontFaceSectionContent();
$contentXmlFileContents .= $styleHelper->getContentXmlAutomaticStylesSectionContent(count($worksheets));
$contentXmlFileContents .= <<<EOD
<office:body>
<office:spreadsheet>
EOD;
$this->createFileWithContents($this->rootFolder, self::CONTENT_XML_FILE_NAME, $contentXmlFileContents);
// Append sheets content to "content.xml"
$contentXmlFilePath = $this->rootFolder . '/' . self::CONTENT_XML_FILE_NAME;
$contentXmlHandle = fopen($contentXmlFilePath, 'a');
foreach ($worksheets as $worksheet) {
// write the "<table:table>" node, with the final sheet's name
fwrite($contentXmlHandle, $worksheet->getTableElementStartAsString() . PHP_EOL);
$worksheetFilePath = $worksheet->getWorksheetFilePath();
$this->copyFileContentsToTarget($worksheetFilePath, $contentXmlHandle);
fwrite($contentXmlHandle, '</table:table>' . PHP_EOL);
}
$contentXmlFileContents = <<<EOD
</office:spreadsheet>
</office:body>
</office:document-content>
EOD;
fwrite($contentXmlHandle, $contentXmlFileContents);
fclose($contentXmlHandle);
return $this;
}
/**
* Streams the content of the file at the given path into the target resource.
* Depending on which mode the target resource was created with, it will truncate then copy
* or append the content to the target file.
*
* @param string $sourceFilePath Path of the file whose content will be copied
* @param resource $targetResource Target resource that will receive the content
* @return void
*/
protected function copyFileContentsToTarget($sourceFilePath, $targetResource)
{
$sourceHandle = fopen($sourceFilePath, 'r');
stream_copy_to_stream($sourceHandle, $targetResource);
fclose($sourceHandle);
}
/**
* Deletes the temporary folder where sheets content was stored.
*
* @return FileSystemHelper
*/
public function deleteWorksheetTempFolder()
{
$this->deleteFolderRecursively($this->sheetsContentTempFolder);
return $this;
}
/**
* Creates the "styles.xml" file under the root folder
*
* @param StyleHelper $styleHelper
* @param int $numWorksheets Number of created worksheets
* @return FileSystemHelper
*/
public function createStylesFile($styleHelper, $numWorksheets)
{
$stylesXmlFileContents = $styleHelper->getStylesXMLFileContent($numWorksheets);
$this->createFileWithContents($this->rootFolder, self::STYLES_XML_FILE_NAME, $stylesXmlFileContents);
return $this;
}
/**
* Zips the root folder and streams the contents of the zip into the given stream
*
* @param resource $streamPointer Pointer to the stream to copy the zip
* @return void
*/
public function zipRootFolderAndCopyToStream($streamPointer)
{
$zipHelper = new ZipHelper();
$zipHelper->zipFolderAndCopyToStream($this->rootFolder, $streamPointer);
// once the zip is copied, remove it
$this->deleteFile($zipHelper->getZipFilePath($this->rootFolder));
}
}