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\Reader\XLSX\Manager\SharedStringsCaching;
   4  
   5  use Box\Spout\Reader\Exception\SharedStringNotFoundException;
   6  use Box\Spout\Reader\XLSX\Creator\HelperFactory;
   7  
   8  /**
   9   * Class FileBasedStrategy
  10   *
  11   * This class implements the file-based caching strategy for shared strings.
  12   * Shared strings are stored in small files (with a max number of strings per file).
  13   * This strategy is slower than an in-memory strategy but is used to avoid out of memory crashes.
  14   */
  15  class FileBasedStrategy implements CachingStrategyInterface
  16  {
  17      /** Value to use to escape the line feed character ("\n") */
  18      const ESCAPED_LINE_FEED_CHARACTER = '_x000A_';
  19  
  20      /** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
  21      protected $globalFunctionsHelper;
  22  
  23      /** @var \Box\Spout\Common\Helper\FileSystemHelper Helper to perform file system operations */
  24      protected $fileSystemHelper;
  25  
  26      /** @var string Temporary folder where the temporary files will be created */
  27      protected $tempFolder;
  28  
  29      /**
  30       * @var int Maximum number of strings that can be stored in one temp file
  31       * @see CachingStrategyFactory::MAX_NUM_STRINGS_PER_TEMP_FILE
  32       */
  33      protected $maxNumStringsPerTempFile;
  34  
  35      /** @var resource Pointer to the last temp file a shared string was written to */
  36      protected $tempFilePointer;
  37  
  38      /**
  39       * @var string Path of the temporary file whose contents is currently stored in memory
  40       * @see CachingStrategyFactory::MAX_NUM_STRINGS_PER_TEMP_FILE
  41       */
  42      protected $inMemoryTempFilePath;
  43  
  44      /**
  45       * @var array Contents of the temporary file that was last read
  46       * @see CachingStrategyFactory::MAX_NUM_STRINGS_PER_TEMP_FILE
  47       */
  48      protected $inMemoryTempFileContents;
  49  
  50      /**
  51       * @param string $tempFolder Temporary folder where the temporary files to store shared strings will be stored
  52       * @param int $maxNumStringsPerTempFile Maximum number of strings that can be stored in one temp file
  53       * @param HelperFactory $helperFactory Factory to create helpers
  54       */
  55      public function __construct($tempFolder, $maxNumStringsPerTempFile, $helperFactory)
  56      {
  57          $this->fileSystemHelper = $helperFactory->createFileSystemHelper($tempFolder);
  58          $this->tempFolder = $this->fileSystemHelper->createFolder($tempFolder, \uniqid('sharedstrings'));
  59  
  60          $this->maxNumStringsPerTempFile = $maxNumStringsPerTempFile;
  61  
  62          $this->globalFunctionsHelper = $helperFactory->createGlobalFunctionsHelper();
  63          $this->tempFilePointer = null;
  64      }
  65  
  66      /**
  67       * Adds the given string to the cache.
  68       *
  69       * @param string $sharedString The string to be added to the cache
  70       * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
  71       * @return void
  72       */
  73      public function addStringForIndex($sharedString, $sharedStringIndex)
  74      {
  75          $tempFilePath = $this->getSharedStringTempFilePath($sharedStringIndex);
  76  
  77          if (!$this->globalFunctionsHelper->file_exists($tempFilePath)) {
  78              if ($this->tempFilePointer) {
  79                  $this->globalFunctionsHelper->fclose($this->tempFilePointer);
  80              }
  81              $this->tempFilePointer = $this->globalFunctionsHelper->fopen($tempFilePath, 'w');
  82          }
  83  
  84          // The shared string retrieval logic expects each cell data to be on one line only
  85          // Encoding the line feed character allows to preserve this assumption
  86          $lineFeedEncodedSharedString = $this->escapeLineFeed($sharedString);
  87  
  88          $this->globalFunctionsHelper->fwrite($this->tempFilePointer, $lineFeedEncodedSharedString . PHP_EOL);
  89      }
  90  
  91      /**
  92       * Returns the path for the temp file that should contain the string for the given index
  93       *
  94       * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
  95       * @return string The temp file path for the given index
  96       */
  97      protected function getSharedStringTempFilePath($sharedStringIndex)
  98      {
  99          $numTempFile = (int) ($sharedStringIndex / $this->maxNumStringsPerTempFile);
 100  
 101          return $this->tempFolder . '/sharedstrings' . $numTempFile;
 102      }
 103  
 104      /**
 105       * Closes the cache after the last shared string was added.
 106       * This prevents any additional string from being added to the cache.
 107       *
 108       * @return void
 109       */
 110      public function closeCache()
 111      {
 112          // close pointer to the last temp file that was written
 113          if ($this->tempFilePointer) {
 114              $this->globalFunctionsHelper->fclose($this->tempFilePointer);
 115          }
 116      }
 117  
 118      /**
 119       * Returns the string located at the given index from the cache.
 120       *
 121       * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
 122       * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If no shared string found for the given index
 123       * @return string The shared string at the given index
 124       */
 125      public function getStringAtIndex($sharedStringIndex)
 126      {
 127          $tempFilePath = $this->getSharedStringTempFilePath($sharedStringIndex);
 128          $indexInFile = $sharedStringIndex % $this->maxNumStringsPerTempFile;
 129  
 130          if (!$this->globalFunctionsHelper->file_exists($tempFilePath)) {
 131              throw new SharedStringNotFoundException("Shared string temp file not found: $tempFilePath ; for index: $sharedStringIndex");
 132          }
 133  
 134          if ($this->inMemoryTempFilePath !== $tempFilePath) {
 135              // free memory
 136              unset($this->inMemoryTempFileContents);
 137  
 138              $this->inMemoryTempFileContents = \explode(PHP_EOL, $this->globalFunctionsHelper->file_get_contents($tempFilePath));
 139              $this->inMemoryTempFilePath = $tempFilePath;
 140          }
 141  
 142          $sharedString = null;
 143  
 144          // Using isset here because it is way faster than array_key_exists...
 145          if (isset($this->inMemoryTempFileContents[$indexInFile])) {
 146              $escapedSharedString = $this->inMemoryTempFileContents[$indexInFile];
 147              $sharedString = $this->unescapeLineFeed($escapedSharedString);
 148          }
 149  
 150          if ($sharedString === null) {
 151              throw new SharedStringNotFoundException("Shared string not found for index: $sharedStringIndex");
 152          }
 153  
 154          return \rtrim($sharedString, PHP_EOL);
 155      }
 156  
 157      /**
 158       * Escapes the line feed characters (\n)
 159       *
 160       * @param string $unescapedString
 161       * @return string
 162       */
 163      private function escapeLineFeed($unescapedString)
 164      {
 165          return \str_replace("\n", self::ESCAPED_LINE_FEED_CHARACTER, $unescapedString);
 166      }
 167  
 168      /**
 169       * Unescapes the line feed characters (\n)
 170       *
 171       * @param string $escapedString
 172       * @return string
 173       */
 174      private function unescapeLineFeed($escapedString)
 175      {
 176          return \str_replace(self::ESCAPED_LINE_FEED_CHARACTER, "\n", $escapedString);
 177      }
 178  
 179      /**
 180       * Destroys the cache, freeing memory and removing any created artifacts
 181       *
 182       * @return void
 183       */
 184      public function clearCache()
 185      {
 186          if ($this->tempFolder) {
 187              $this->fileSystemHelper->deleteFolderRecursively($this->tempFolder);
 188          }
 189      }
 190  }