Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

Differences Between: [Versions 402 and 403]

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