Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

   1  <?php
   2  
   3  namespace Box\Spout\Writer\ODS\Manager;
   4  
   5  use Box\Spout\Common\Entity\Cell;
   6  use Box\Spout\Common\Entity\Row;
   7  use Box\Spout\Common\Entity\Style\Style;
   8  use Box\Spout\Common\Exception\InvalidArgumentException;
   9  use Box\Spout\Common\Exception\IOException;
  10  use Box\Spout\Common\Helper\Escaper\ODS as ODSEscaper;
  11  use Box\Spout\Common\Helper\StringHelper;
  12  use Box\Spout\Writer\Common\Entity\Worksheet;
  13  use Box\Spout\Writer\Common\Manager\RegisteredStyle;
  14  use Box\Spout\Writer\Common\Manager\Style\StyleMerger;
  15  use Box\Spout\Writer\Common\Manager\WorksheetManagerInterface;
  16  use Box\Spout\Writer\ODS\Manager\Style\StyleManager;
  17  
  18  /**
  19   * Class WorksheetManager
  20   * ODS worksheet manager, providing the interfaces to work with ODS worksheets.
  21   */
  22  class WorksheetManager implements WorksheetManagerInterface
  23  {
  24      /** @var \Box\Spout\Common\Helper\Escaper\ODS Strings escaper */
  25      private $stringsEscaper;
  26  
  27      /** @var StringHelper String helper */
  28      private $stringHelper;
  29  
  30      /** @var StyleManager Manages styles */
  31      private $styleManager;
  32  
  33      /** @var StyleMerger Helper to merge styles together */
  34      private $styleMerger;
  35  
  36      /**
  37       * WorksheetManager constructor.
  38       *
  39       * @param StyleManager $styleManager
  40       * @param StyleMerger $styleMerger
  41       * @param ODSEscaper $stringsEscaper
  42       * @param StringHelper $stringHelper
  43       */
  44      public function __construct(
  45          StyleManager $styleManager,
  46          StyleMerger $styleMerger,
  47          ODSEscaper $stringsEscaper,
  48          StringHelper $stringHelper
  49      ) {
  50          $this->styleManager = $styleManager;
  51          $this->styleMerger = $styleMerger;
  52          $this->stringsEscaper = $stringsEscaper;
  53          $this->stringHelper = $stringHelper;
  54      }
  55  
  56      /**
  57       * Prepares the worksheet to accept data
  58       *
  59       * @param Worksheet $worksheet The worksheet to start
  60       * @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing
  61       * @return void
  62       */
  63      public function startSheet(Worksheet $worksheet)
  64      {
  65          $sheetFilePointer = \fopen($worksheet->getFilePath(), 'w');
  66          $this->throwIfSheetFilePointerIsNotAvailable($sheetFilePointer);
  67  
  68          $worksheet->setFilePointer($sheetFilePointer);
  69      }
  70  
  71      /**
  72       * Checks if the sheet has been sucessfully created. Throws an exception if not.
  73       *
  74       * @param bool|resource $sheetFilePointer Pointer to the sheet data file or FALSE if unable to open the file
  75       * @throws IOException If the sheet data file cannot be opened for writing
  76       * @return void
  77       */
  78      private function throwIfSheetFilePointerIsNotAvailable($sheetFilePointer)
  79      {
  80          if (!$sheetFilePointer) {
  81              throw new IOException('Unable to open sheet for writing.');
  82          }
  83      }
  84  
  85      /**
  86       * Returns the table XML root node as string.
  87       *
  88       * @param Worksheet $worksheet
  89       * @return string <table> node as string
  90       */
  91      public function getTableElementStartAsString(Worksheet $worksheet)
  92      {
  93          $externalSheet = $worksheet->getExternalSheet();
  94          $escapedSheetName = $this->stringsEscaper->escape($externalSheet->getName());
  95          $tableStyleName = 'ta' . ($externalSheet->getIndex() + 1);
  96  
  97          $tableElement = '<table:table table:style-name="' . $tableStyleName . '" table:name="' . $escapedSheetName . '">';
  98          $tableElement .= '<table:table-column table:default-cell-style-name="ce1" table:style-name="co1" table:number-columns-repeated="' . $worksheet->getMaxNumColumns() . '"/>';
  99  
 100          return $tableElement;
 101      }
 102  
 103      /**
 104       * Adds a row to the given worksheet.
 105       *
 106       * @param Worksheet $worksheet The worksheet to add the row to
 107       * @param Row $row The row to be added
 108       * @throws InvalidArgumentException If a cell value's type is not supported
 109       * @throws IOException If the data cannot be written
 110       * @return void
 111       */
 112      public function addRow(Worksheet $worksheet, Row $row)
 113      {
 114          $cells = $row->getCells();
 115          $rowStyle = $row->getStyle();
 116  
 117          $data = '<table:table-row table:style-name="ro1">';
 118  
 119          $currentCellIndex = 0;
 120          $nextCellIndex = 1;
 121  
 122          for ($i = 0; $i < $row->getNumCells(); $i++) {
 123              /** @var Cell $cell */
 124              $cell = $cells[$currentCellIndex];
 125              /** @var Cell|null $nextCell */
 126              $nextCell = isset($cells[$nextCellIndex]) ? $cells[$nextCellIndex] : null;
 127  
 128              if ($nextCell === null || $cell->getValue() !== $nextCell->getValue()) {
 129                  $registeredStyle = $this->applyStyleAndRegister($cell, $rowStyle);
 130                  $cellStyle = $registeredStyle->getStyle();
 131                  if ($registeredStyle->isMatchingRowStyle()) {
 132                      $rowStyle = $cellStyle; // Replace actual rowStyle (possibly with null id) by registered style (with id)
 133                  }
 134  
 135                  $data .= $this->getCellXMLWithStyle($cell, $cellStyle, $currentCellIndex, $nextCellIndex);
 136                  $currentCellIndex = $nextCellIndex;
 137              }
 138  
 139              $nextCellIndex++;
 140          }
 141  
 142          $data .= '</table:table-row>';
 143  
 144          $wasWriteSuccessful = \fwrite($worksheet->getFilePointer(), $data);
 145          if ($wasWriteSuccessful === false) {
 146              throw new IOException("Unable to write data in {$worksheet->getFilePath()}");
 147          }
 148  
 149          // only update the count if the write worked
 150          $lastWrittenRowIndex = $worksheet->getLastWrittenRowIndex();
 151          $worksheet->setLastWrittenRowIndex($lastWrittenRowIndex + 1);
 152      }
 153  
 154      /**
 155       * Applies styles to the given style, merging the cell's style with its row's style
 156       *
 157       * @param Cell $cell
 158       * @param Style $rowStyle
 159       * @throws InvalidArgumentException If a cell value's type is not supported
 160       * @return RegisteredStyle
 161       */
 162      private function applyStyleAndRegister(Cell $cell, Style $rowStyle) : RegisteredStyle
 163      {
 164          $isMatchingRowStyle = false;
 165          if ($cell->getStyle()->isEmpty()) {
 166              $cell->setStyle($rowStyle);
 167  
 168              $possiblyUpdatedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell);
 169  
 170              if ($possiblyUpdatedStyle->isUpdated()) {
 171                  $registeredStyle = $this->styleManager->registerStyle($possiblyUpdatedStyle->getStyle());
 172              } else {
 173                  $registeredStyle = $this->styleManager->registerStyle($rowStyle);
 174                  $isMatchingRowStyle = true;
 175              }
 176          } else {
 177              $mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle);
 178              $cell->setStyle($mergedCellAndRowStyle);
 179  
 180              $possiblyUpdatedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell);
 181              if ($possiblyUpdatedStyle->isUpdated()) {
 182                  $newCellStyle = $possiblyUpdatedStyle->getStyle();
 183              } else {
 184                  $newCellStyle = $mergedCellAndRowStyle;
 185              }
 186  
 187              $registeredStyle = $this->styleManager->registerStyle($newCellStyle);
 188          }
 189  
 190          return new RegisteredStyle($registeredStyle, $isMatchingRowStyle);
 191      }
 192  
 193      private function getCellXMLWithStyle(Cell $cell, Style $style, int $currentCellIndex, int $nextCellIndex) : string
 194      {
 195          $styleIndex = $style->getId() + 1; // 1-based
 196  
 197          $numTimesValueRepeated = ($nextCellIndex - $currentCellIndex);
 198  
 199          return $this->getCellXML($cell, $styleIndex, $numTimesValueRepeated);
 200      }
 201  
 202      /**
 203       * Returns the cell XML content, given its value.
 204       *
 205       * @param Cell $cell The cell to be written
 206       * @param int $styleIndex Index of the used style
 207       * @param int $numTimesValueRepeated Number of times the value is consecutively repeated
 208       * @throws InvalidArgumentException If a cell value's type is not supported
 209       * @return string The cell XML content
 210       */
 211      private function getCellXML(Cell $cell, $styleIndex, $numTimesValueRepeated)
 212      {
 213          $data = '<table:table-cell table:style-name="ce' . $styleIndex . '"';
 214  
 215          if ($numTimesValueRepeated !== 1) {
 216              $data .= ' table:number-columns-repeated="' . $numTimesValueRepeated . '"';
 217          }
 218  
 219          if ($cell->isString()) {
 220              $data .= ' office:value-type="string" calcext:value-type="string">';
 221  
 222              $cellValueLines = \explode("\n", $cell->getValue());
 223              foreach ($cellValueLines as $cellValueLine) {
 224                  $data .= '<text:p>' . $this->stringsEscaper->escape($cellValueLine) . '</text:p>';
 225              }
 226  
 227              $data .= '</table:table-cell>';
 228          } elseif ($cell->isBoolean()) {
 229              $value = $cell->getValue() ? 'true' : 'false'; // boolean-value spec: http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#datatype-boolean
 230              $data .= ' office:value-type="boolean" calcext:value-type="boolean" office:boolean-value="' . $value . '">';
 231              $data .= '<text:p>' . $cell->getValue() . '</text:p>';
 232              $data .= '</table:table-cell>';
 233          } elseif ($cell->isNumeric()) {
 234              $cellValue = $this->stringHelper->formatNumericValue($cell->getValue());
 235              $data .= ' office:value-type="float" calcext:value-type="float" office:value="' . $cellValue . '">';
 236              $data .= '<text:p>' . $cellValue . '</text:p>';
 237              $data .= '</table:table-cell>';
 238          } elseif ($cell->isError() && is_string($cell->getValueEvenIfError())) {
 239              // only writes the error value if it's a string
 240              $data .= ' office:value-type="string" calcext:value-type="error" office:value="">';
 241              $data .= '<text:p>' . $cell->getValueEvenIfError() . '</text:p>';
 242              $data .= '</table:table-cell>';
 243          } elseif ($cell->isEmpty()) {
 244              $data .= '/>';
 245          } else {
 246              throw new InvalidArgumentException('Trying to add a value with an unsupported type: ' . \gettype($cell->getValue()));
 247          }
 248  
 249          return $data;
 250      }
 251  
 252      /**
 253       * Closes the worksheet
 254       *
 255       * @param Worksheet $worksheet
 256       * @return void
 257       */
 258      public function close(Worksheet $worksheet)
 259      {
 260          $worksheetFilePointer = $worksheet->getFilePointer();
 261  
 262          if (!\is_resource($worksheetFilePointer)) {
 263              return;
 264          }
 265  
 266          \fclose($worksheetFilePointer);
 267      }
 268  }