Completed
Pull Request — develop_3.0 (#460)
by Adrien
02:25
created

FileBasedStrategy::escapeLineFeed()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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