Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 39 and 401]

   1  <?php
   2  
   3  namespace Box\Spout\Writer\XLSX\Helper;
   4  
   5  use Box\Spout\Writer\Common\Entity\Worksheet;
   6  use Box\Spout\Writer\Common\Helper\FileSystemWithRootFolderHelperInterface;
   7  use Box\Spout\Writer\Common\Helper\ZipHelper;
   8  use Box\Spout\Writer\XLSX\Manager\Style\StyleManager;
   9  
  10  /**
  11   * Class FileSystemHelper
  12   * This class provides helper functions to help with the file system operations
  13   * like files/folders creation & deletion for XLSX files
  14   */
  15  class FileSystemHelper extends \Box\Spout\Common\Helper\FileSystemHelper implements FileSystemWithRootFolderHelperInterface
  16  {
  17      const APP_NAME = 'Spout';
  18  
  19      const RELS_FOLDER_NAME = '_rels';
  20      const DOC_PROPS_FOLDER_NAME = 'docProps';
  21      const XL_FOLDER_NAME = 'xl';
  22      const WORKSHEETS_FOLDER_NAME = 'worksheets';
  23  
  24      const RELS_FILE_NAME = '.rels';
  25      const APP_XML_FILE_NAME = 'app.xml';
  26      const CORE_XML_FILE_NAME = 'core.xml';
  27      const CONTENT_TYPES_XML_FILE_NAME = '[Content_Types].xml';
  28      const WORKBOOK_XML_FILE_NAME = 'workbook.xml';
  29      const WORKBOOK_RELS_XML_FILE_NAME = 'workbook.xml.rels';
  30      const STYLES_XML_FILE_NAME = 'styles.xml';
  31  
  32      /** @var ZipHelper Helper to perform tasks with Zip archive */
  33      private $zipHelper;
  34  
  35      /** @var \Box\Spout\Common\Helper\Escaper\XLSX Used to escape XML data */
  36      private $escaper;
  37  
  38      /** @var string Path to the root folder inside the temp folder where the files to create the XLSX will be stored */
  39      private $rootFolder;
  40  
  41      /** @var string Path to the "_rels" folder inside the root folder */
  42      private $relsFolder;
  43  
  44      /** @var string Path to the "docProps" folder inside the root folder */
  45      private $docPropsFolder;
  46  
  47      /** @var string Path to the "xl" folder inside the root folder */
  48      private $xlFolder;
  49  
  50      /** @var string Path to the "_rels" folder inside the "xl" folder */
  51      private $xlRelsFolder;
  52  
  53      /** @var string Path to the "worksheets" folder inside the "xl" folder */
  54      private $xlWorksheetsFolder;
  55  
  56      /**
  57       * @param string $baseFolderPath The path of the base folder where all the I/O can occur
  58       * @param ZipHelper $zipHelper Helper to perform tasks with Zip archive
  59       * @param \Box\Spout\Common\Helper\Escaper\XLSX $escaper Used to escape XML data
  60       */
  61      public function __construct($baseFolderPath, $zipHelper, $escaper)
  62      {
  63          parent::__construct($baseFolderPath);
  64          $this->zipHelper = $zipHelper;
  65          $this->escaper = $escaper;
  66      }
  67  
  68      /**
  69       * @return string
  70       */
  71      public function getRootFolder()
  72      {
  73          return $this->rootFolder;
  74      }
  75  
  76      /**
  77       * @return string
  78       */
  79      public function getXlFolder()
  80      {
  81          return $this->xlFolder;
  82      }
  83  
  84      /**
  85       * @return string
  86       */
  87      public function getXlWorksheetsFolder()
  88      {
  89          return $this->xlWorksheetsFolder;
  90      }
  91  
  92      /**
  93       * Creates all the folders needed to create a XLSX file, as well as the files that won't change.
  94       *
  95       * @throws \Box\Spout\Common\Exception\IOException If unable to create at least one of the base folders
  96       * @return void
  97       */
  98      public function createBaseFilesAndFolders()
  99      {
 100          $this
 101              ->createRootFolder()
 102              ->createRelsFolderAndFile()
 103              ->createDocPropsFolderAndFiles()
 104              ->createXlFolderAndSubFolders();
 105      }
 106  
 107      /**
 108       * Creates the folder that will be used as root
 109       *
 110       * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder
 111       * @return FileSystemHelper
 112       */
 113      private function createRootFolder()
 114      {
 115          $this->rootFolder = $this->createFolder($this->baseFolderRealPath, \uniqid('xlsx', true));
 116  
 117          return $this;
 118      }
 119  
 120      /**
 121       * Creates the "_rels" folder under the root folder as well as the ".rels" file in it
 122       *
 123       * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder or the ".rels" file
 124       * @return FileSystemHelper
 125       */
 126      private function createRelsFolderAndFile()
 127      {
 128          $this->relsFolder = $this->createFolder($this->rootFolder, self::RELS_FOLDER_NAME);
 129  
 130          $this->createRelsFile();
 131  
 132          return $this;
 133      }
 134  
 135      /**
 136       * Creates the ".rels" file under the "_rels" folder (under root)
 137       *
 138       * @throws \Box\Spout\Common\Exception\IOException If unable to create the file
 139       * @return FileSystemHelper
 140       */
 141      private function createRelsFile()
 142      {
 143          $relsFileContents = <<<'EOD'
 144  <?xml version="1.0" encoding="UTF-8"?>
 145  <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
 146      <Relationship Id="rIdWorkbook" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
 147      <Relationship Id="rIdCore" Type="http://schemas.openxmlformats.org/officedocument/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>
 148      <Relationship Id="rIdApp" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>
 149  </Relationships>
 150  EOD;
 151  
 152          $this->createFileWithContents($this->relsFolder, self::RELS_FILE_NAME, $relsFileContents);
 153  
 154          return $this;
 155      }
 156  
 157      /**
 158       * Creates the "docProps" folder under the root folder as well as the "app.xml" and "core.xml" files in it
 159       *
 160       * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder or one of the files
 161       * @return FileSystemHelper
 162       */
 163      private function createDocPropsFolderAndFiles()
 164      {
 165          $this->docPropsFolder = $this->createFolder($this->rootFolder, self::DOC_PROPS_FOLDER_NAME);
 166  
 167          $this->createAppXmlFile();
 168          $this->createCoreXmlFile();
 169  
 170          return $this;
 171      }
 172  
 173      /**
 174       * Creates the "app.xml" file under the "docProps" folder
 175       *
 176       * @throws \Box\Spout\Common\Exception\IOException If unable to create the file
 177       * @return FileSystemHelper
 178       */
 179      private function createAppXmlFile()
 180      {
 181          $appName = self::APP_NAME;
 182          $appXmlFileContents = <<<EOD
 183  <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 184  <Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties">
 185      <Application>$appName</Application>
 186      <TotalTime>0</TotalTime>
 187  </Properties>
 188  EOD;
 189  
 190          $this->createFileWithContents($this->docPropsFolder, self::APP_XML_FILE_NAME, $appXmlFileContents);
 191  
 192          return $this;
 193      }
 194  
 195      /**
 196       * Creates the "core.xml" file under the "docProps" folder
 197       *
 198       * @throws \Box\Spout\Common\Exception\IOException If unable to create the file
 199       * @return FileSystemHelper
 200       */
 201      private function createCoreXmlFile()
 202      {
 203          $createdDate = (new \DateTime())->format(\DateTime::W3C);
 204          $coreXmlFileContents = <<<EOD
 205  <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 206  <cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
 207      <dcterms:created xsi:type="dcterms:W3CDTF">$createdDate</dcterms:created>
 208      <dcterms:modified xsi:type="dcterms:W3CDTF">$createdDate</dcterms:modified>
 209      <cp:revision>0</cp:revision>
 210  </cp:coreProperties>
 211  EOD;
 212  
 213          $this->createFileWithContents($this->docPropsFolder, self::CORE_XML_FILE_NAME, $coreXmlFileContents);
 214  
 215          return $this;
 216      }
 217  
 218      /**
 219       * Creates the "xl" folder under the root folder as well as its subfolders
 220       *
 221       * @throws \Box\Spout\Common\Exception\IOException If unable to create at least one of the folders
 222       * @return FileSystemHelper
 223       */
 224      private function createXlFolderAndSubFolders()
 225      {
 226          $this->xlFolder = $this->createFolder($this->rootFolder, self::XL_FOLDER_NAME);
 227          $this->createXlRelsFolder();
 228          $this->createXlWorksheetsFolder();
 229  
 230          return $this;
 231      }
 232  
 233      /**
 234       * Creates the "_rels" folder under the "xl" folder
 235       *
 236       * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder
 237       * @return FileSystemHelper
 238       */
 239      private function createXlRelsFolder()
 240      {
 241          $this->xlRelsFolder = $this->createFolder($this->xlFolder, self::RELS_FOLDER_NAME);
 242  
 243          return $this;
 244      }
 245  
 246      /**
 247       * Creates the "worksheets" folder under the "xl" folder
 248       *
 249       * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder
 250       * @return FileSystemHelper
 251       */
 252      private function createXlWorksheetsFolder()
 253      {
 254          $this->xlWorksheetsFolder = $this->createFolder($this->xlFolder, self::WORKSHEETS_FOLDER_NAME);
 255  
 256          return $this;
 257      }
 258  
 259      /**
 260       * Creates the "[Content_Types].xml" file under the root folder
 261       *
 262       * @param Worksheet[] $worksheets
 263       * @return FileSystemHelper
 264       */
 265      public function createContentTypesFile($worksheets)
 266      {
 267          $contentTypesXmlFileContents = <<<'EOD'
 268  <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 269  <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
 270      <Default ContentType="application/xml" Extension="xml"/>
 271      <Default ContentType="application/vnd.openxmlformats-package.relationships+xml" Extension="rels"/>
 272      <Override ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml" PartName="/xl/workbook.xml"/>
 273  EOD;
 274  
 275          /** @var Worksheet $worksheet */
 276          foreach ($worksheets as $worksheet) {
 277              $contentTypesXmlFileContents .= '<Override ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml" PartName="/xl/worksheets/sheet' . $worksheet->getId() . '.xml"/>';
 278          }
 279  
 280          $contentTypesXmlFileContents .= <<<'EOD'
 281      <Override ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml" PartName="/xl/styles.xml"/>
 282      <Override ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml" PartName="/xl/sharedStrings.xml"/>
 283      <Override ContentType="application/vnd.openxmlformats-package.core-properties+xml" PartName="/docProps/core.xml"/>
 284      <Override ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml" PartName="/docProps/app.xml"/>
 285  </Types>
 286  EOD;
 287  
 288          $this->createFileWithContents($this->rootFolder, self::CONTENT_TYPES_XML_FILE_NAME, $contentTypesXmlFileContents);
 289  
 290          return $this;
 291      }
 292  
 293      /**
 294       * Creates the "workbook.xml" file under the "xl" folder
 295       *
 296       * @param Worksheet[] $worksheets
 297       * @return FileSystemHelper
 298       */
 299      public function createWorkbookFile($worksheets)
 300      {
 301          $workbookXmlFileContents = <<<'EOD'
 302  <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 303  <workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
 304      <sheets>
 305  EOD;
 306  
 307          /** @var Worksheet $worksheet */
 308          foreach ($worksheets as $worksheet) {
 309              $worksheetName = $worksheet->getExternalSheet()->getName();
 310              $worksheetVisibility = $worksheet->getExternalSheet()->isVisible() ? 'visible' : 'hidden';
 311              $worksheetId = $worksheet->getId();
 312              $workbookXmlFileContents .= '<sheet name="' . $this->escaper->escape($worksheetName) . '" sheetId="' . $worksheetId . '" r:id="rIdSheet' . $worksheetId . '" state="' . $worksheetVisibility . '"/>';
 313          }
 314  
 315          $workbookXmlFileContents .= <<<'EOD'
 316      </sheets>
 317  </workbook>
 318  EOD;
 319  
 320          $this->createFileWithContents($this->xlFolder, self::WORKBOOK_XML_FILE_NAME, $workbookXmlFileContents);
 321  
 322          return $this;
 323      }
 324  
 325      /**
 326       * Creates the "workbook.xml.res" file under the "xl/_res" folder
 327       *
 328       * @param Worksheet[] $worksheets
 329       * @return FileSystemHelper
 330       */
 331      public function createWorkbookRelsFile($worksheets)
 332      {
 333          $workbookRelsXmlFileContents = <<<'EOD'
 334  <?xml version="1.0" encoding="UTF-8"?>
 335  <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
 336      <Relationship Id="rIdStyles" Target="styles.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles"/>
 337      <Relationship Id="rIdSharedStrings" Target="sharedStrings.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings"/>
 338  EOD;
 339  
 340          /** @var Worksheet $worksheet */
 341          foreach ($worksheets as $worksheet) {
 342              $worksheetId = $worksheet->getId();
 343              $workbookRelsXmlFileContents .= '<Relationship Id="rIdSheet' . $worksheetId . '" Target="worksheets/sheet' . $worksheetId . '.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"/>';
 344          }
 345  
 346          $workbookRelsXmlFileContents .= '</Relationships>';
 347  
 348          $this->createFileWithContents($this->xlRelsFolder, self::WORKBOOK_RELS_XML_FILE_NAME, $workbookRelsXmlFileContents);
 349  
 350          return $this;
 351      }
 352  
 353      /**
 354       * Creates the "styles.xml" file under the "xl" folder
 355       *
 356       * @param StyleManager $styleManager
 357       * @return FileSystemHelper
 358       */
 359      public function createStylesFile($styleManager)
 360      {
 361          $stylesXmlFileContents = $styleManager->getStylesXMLFileContent();
 362          $this->createFileWithContents($this->xlFolder, self::STYLES_XML_FILE_NAME, $stylesXmlFileContents);
 363  
 364          return $this;
 365      }
 366  
 367      /**
 368       * Zips the root folder and streams the contents of the zip into the given stream
 369       *
 370       * @param resource $streamPointer Pointer to the stream to copy the zip
 371       * @return void
 372       */
 373      public function zipRootFolderAndCopyToStream($streamPointer)
 374      {
 375          $zip = $this->zipHelper->createZip($this->rootFolder);
 376  
 377          $zipFilePath = $this->zipHelper->getZipFilePath($zip);
 378  
 379          // In order to have the file's mime type detected properly, files need to be added
 380          // to the zip file in a particular order.
 381          // "[Content_Types].xml" then at least 2 files located in "xl" folder should be zipped first.
 382          $this->zipHelper->addFileToArchive($zip, $this->rootFolder, self::CONTENT_TYPES_XML_FILE_NAME);
 383          $this->zipHelper->addFileToArchive($zip, $this->rootFolder, self::XL_FOLDER_NAME . '/' . self::WORKBOOK_XML_FILE_NAME);
 384          $this->zipHelper->addFileToArchive($zip, $this->rootFolder, self::XL_FOLDER_NAME . '/' . self::STYLES_XML_FILE_NAME);
 385  
 386          $this->zipHelper->addFolderToArchive($zip, $this->rootFolder, ZipHelper::EXISTING_FILES_SKIP);
 387          $this->zipHelper->closeArchiveAndCopyToStream($zip, $streamPointer);
 388  
 389          // once the zip is copied, remove it
 390          $this->deleteFile($zipFilePath);
 391      }
 392  }