Completed
Pull Request — develop_3.0 (#457)
by Adrien
02:34
created

FileBasedStrategy::clearCache()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

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