Completed
Pull Request — master (#557)
by Adrien
03:10
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\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 9
    public function __construct($tempFolder, $maxNumStringsPerTempFile, $helperFactory)
56
    {
57 9
        $this->fileSystemHelper = $helperFactory->createFileSystemHelper($tempFolder);
58 9
        $this->tempFolder = $this->fileSystemHelper->createFolder($tempFolder, uniqid('sharedstrings'));
59
60 9
        $this->maxNumStringsPerTempFile = $maxNumStringsPerTempFile;
61
62 9
        $this->globalFunctionsHelper = $helperFactory->createGlobalFunctionsHelper();
63 9
        $this->tempFilePointer = null;
64 9
    }
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 2
    public function addStringForIndex($sharedString, $sharedStringIndex)
74
    {
75 2
        $tempFilePath = $this->getSharedStringTempFilePath($sharedStringIndex);
76
77 2
        if (!$this->globalFunctionsHelper->file_exists($tempFilePath)) {
78 2
            if ($this->tempFilePointer) {
79 1
                $this->globalFunctionsHelper->fclose($this->tempFilePointer);
80
            }
81 2
            $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 2
        $lineFeedEncodedSharedString = $this->escapeLineFeed($sharedString);
87
88 2
        $this->globalFunctionsHelper->fwrite($this->tempFilePointer, $lineFeedEncodedSharedString . PHP_EOL);
89 2
    }
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 2
    protected function getSharedStringTempFilePath($sharedStringIndex)
98
    {
99 2
        $numTempFile = (int) ($sharedStringIndex / $this->maxNumStringsPerTempFile);
100
101 2
        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 4
    public function closeCache()
111
    {
112
        // close pointer to the last temp file that was written
113 4
        if ($this->tempFilePointer) {
114 2
            $this->globalFunctionsHelper->fclose($this->tempFilePointer);
115
        }
116 4
    }
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 2
    public function getStringAtIndex($sharedStringIndex)
126
    {
127 2
        $tempFilePath = $this->getSharedStringTempFilePath($sharedStringIndex);
128 2
        $indexInFile = $sharedStringIndex % $this->maxNumStringsPerTempFile;
129
130 2
        if (!$this->globalFunctionsHelper->file_exists($tempFilePath)) {
131
            throw new SharedStringNotFoundException("Shared string temp file not found: $tempFilePath ; for index: $sharedStringIndex");
132
        }
133
134 2
        if ($this->inMemoryTempFilePath !== $tempFilePath) {
135
            // free memory
136 2
            unset($this->inMemoryTempFileContents);
137
138 2
            $this->inMemoryTempFileContents = explode(PHP_EOL, $this->globalFunctionsHelper->file_get_contents($tempFilePath));
139 2
            $this->inMemoryTempFilePath = $tempFilePath;
140
        }
141
142 2
        $sharedString = null;
143
144
        // Using isset here because it is way faster than array_key_exists...
145 2
        if (isset($this->inMemoryTempFileContents[$indexInFile])) {
146 2
            $escapedSharedString = $this->inMemoryTempFileContents[$indexInFile];
147 2
            $sharedString = $this->unescapeLineFeed($escapedSharedString);
148
        }
149
150 2
        if ($sharedString === null) {
151
            throw new SharedStringNotFoundException("Shared string not found for index: $sharedStringIndex");
152
        }
153
154 2
        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 2
    private function escapeLineFeed($unescapedString)
164
    {
165 2
        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 2
    private function unescapeLineFeed($escapedString)
175
    {
176 2
        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 9
    public function clearCache()
185
    {
186 9
        if ($this->tempFolder) {
187 9
            $this->fileSystemHelper->deleteFolderRecursively($this->tempFolder);
188
        }
189 9
    }
190
}
191