Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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 readonly FileSystemHelper $fileSystemHelper;
26
 
27
    /** @var string Temporary folder where the temporary files will be created */
28
    private readonly 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 readonly 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
 
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
     */
72
    public function addStringForIndex(string $sharedString, int $sharedStringIndex): void
73
    {
74
        $tempFilePath = $this->getSharedStringTempFilePath($sharedStringIndex);
75
 
76
        if (!file_exists($tempFilePath)) {
77
            if (null !== $this->tempFilePointer) {
78
                fclose($this->tempFilePointer);
79
            }
80
            $resource = fopen($tempFilePath, 'w');
81
            \assert(false !== $resource);
82
            $this->tempFilePointer = $resource;
83
        }
84
 
85
        // The shared string retrieval logic expects each cell data to be on one line only
86
        // Encoding the line feed character allows to preserve this assumption
87
        $lineFeedEncodedSharedString = $this->escapeLineFeed($sharedString);
88
 
89
        fwrite($this->tempFilePointer, $lineFeedEncodedSharedString.PHP_EOL);
90
    }
91
 
92
    /**
93
     * Closes the cache after the last shared string was added.
94
     * This prevents any additional string from being added to the cache.
95
     */
96
    public function closeCache(): void
97
    {
98
        // close pointer to the last temp file that was written
99
        if (null !== $this->tempFilePointer) {
100
            fclose($this->tempFilePointer);
101
        }
102
    }
103
 
104
    /**
105
     * Returns the string located at the given index from the cache.
106
     *
107
     * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
108
     *
109
     * @return string The shared string at the given index
110
     *
111
     * @throws SharedStringNotFoundException If no shared string found for the given index
112
     */
113
    public function getStringAtIndex(int $sharedStringIndex): string
114
    {
115
        $tempFilePath = $this->getSharedStringTempFilePath($sharedStringIndex);
116
        $indexInFile = $sharedStringIndex % $this->maxNumStringsPerTempFile;
117
 
118
        if (!file_exists($tempFilePath)) {
119
            throw new SharedStringNotFoundException("Shared string temp file not found: {$tempFilePath} ; for index: {$sharedStringIndex}");
120
        }
121
 
122
        if ($this->inMemoryTempFilePath !== $tempFilePath) {
123
            $tempFilePath = realpath($tempFilePath);
124
            \assert(false !== $tempFilePath);
125
            $contents = file_get_contents($tempFilePath);
126
            \assert(false !== $contents);
127
            $this->inMemoryTempFileContents = explode(PHP_EOL, $contents);
128
            $this->inMemoryTempFilePath = $tempFilePath;
129
        }
130
 
131
        $sharedString = null;
132
 
133
        // Using isset here because it is way faster than array_key_exists...
134
        if (isset($this->inMemoryTempFileContents[$indexInFile])) {
135
            $escapedSharedString = $this->inMemoryTempFileContents[$indexInFile];
136
            $sharedString = $this->unescapeLineFeed($escapedSharedString);
137
        }
138
 
139
        if (null === $sharedString) {
140
            throw new SharedStringNotFoundException("Shared string not found for index: {$sharedStringIndex}");
141
        }
142
 
143
        return rtrim($sharedString, PHP_EOL);
144
    }
145
 
146
    /**
147
     * Destroys the cache, freeing memory and removing any created artifacts.
148
     */
149
    public function clearCache(): void
150
    {
151
        $this->fileSystemHelper->deleteFolderRecursively($this->tempFolder);
152
    }
153
 
154
    /**
155
     * Returns the path for the temp file that should contain the string for the given index.
156
     *
157
     * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
158
     *
159
     * @return string The temp file path for the given index
160
     */
161
    private function getSharedStringTempFilePath(int $sharedStringIndex): string
162
    {
163
        $numTempFile = (int) ($sharedStringIndex / $this->maxNumStringsPerTempFile);
164
 
165
        return $this->tempFolder.\DIRECTORY_SEPARATOR.'sharedstrings'.$numTempFile;
166
    }
167
 
168
    /**
169
     * Escapes the line feed characters (\n).
170
     */
171
    private function escapeLineFeed(string $unescapedString): string
172
    {
173
        return str_replace("\n", self::ESCAPED_LINE_FEED_CHARACTER, $unescapedString);
174
    }
175
 
176
    /**
177
     * Unescapes the line feed characters (\n).
178
     */
179
    private function unescapeLineFeed(string $escapedString): string
180
    {
181
        return str_replace(self::ESCAPED_LINE_FEED_CHARACTER, "\n", $escapedString);
182
    }
183
}