Failed Conditions
Push — perf-tests ( 50942d...2fc93e )
by Adrien
14:53
created

FileBasedStrategy::getStringAtIndex()   B

Complexity

Conditions 5
Paths 9

Size

Total Lines 31
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

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