Completed
Pull Request — develop_3.0 (#617)
by
unknown
02:32
created

FileBasedStrategy::openCache()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 16
ccs 8
cts 8
cp 1
rs 9.7333
c 0
b 0
f 0
cc 2
nc 2
nop 1
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
    /** Index entry size uint32 for offset and uint16 for length */
21
    const INDEX_ENTRY_SIZE = 6;
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 $tempFilePointers;
40
41
    /**
42
     * @param string $tempFolder Temporary folder where the temporary files to store shared strings will be stored
43
     * @param int $maxNumStringsPerTempFile Maximum number of strings that can be stored in one temp file
44
     * @param HelperFactory $helperFactory Factory to create helpers
45
     */
46 9
    public function __construct($tempFolder, $maxNumStringsPerTempFile, $helperFactory)
47
    {
48 9
        $this->fileSystemHelper = $helperFactory->createFileSystemHelper($tempFolder);
49 9
        $this->tempFolder = $this->fileSystemHelper->createFolder($tempFolder, uniqid('sharedstrings'));
50
51 9
        $this->maxNumStringsPerTempFile = $maxNumStringsPerTempFile;
52
53 9
        $this->globalFunctionsHelper = $helperFactory->createGlobalFunctionsHelper();
54 9
        $this->tempFilePointers = [];
0 ignored issues
show
Documentation Bug introduced by
It seems like array() of type array is incompatible with the declared type resource of property $tempFilePointers.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
55 9
    }
56
57
    /**
58
     * Open file with cache
59
     *
60
     * @param string $tempFilePath filename with shared strings
61
     */
62 2
    private function openCache($tempFilePath)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
63
    {
64 2
        if (!array_key_exists($tempFilePath, $this->tempFilePointers)) {
65
            // Open index file and seek to end
66 2
            $index = $this->globalFunctionsHelper->fopen($tempFilePath . '.index', 'c+');
67 2
            $this->globalFunctionsHelper->fseek($index, 0, SEEK_END);
0 ignored issues
show
Bug introduced by
It seems like $index defined by $this->globalFunctionsHe...ePath . '.index', 'c+') on line 66 can also be of type boolean; however, Box\Spout\Common\Helper\...unctionsHelper::fseek() does only seem to accept resource, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
68
69
            // Open data file and seek to end
70 2
            $data = $this->globalFunctionsHelper->fopen($tempFilePath, 'c+');
71 2
            $this->globalFunctionsHelper->fseek($data, 0, SEEK_END);
0 ignored issues
show
Bug introduced by
It seems like $data defined by $this->globalFunctionsHe...en($tempFilePath, 'c+') on line 70 can also be of type boolean; however, Box\Spout\Common\Helper\...unctionsHelper::fseek() does only seem to accept resource, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
72
73 2
            $this->tempFilePointers[$tempFilePath] = [$index, $data];
74
        }
75
76 2
        return $this->tempFilePointers[$tempFilePath];
77
    }
78
79
    /**
80
     * Adds the given string to the cache.
81
     *
82
     * @param string $sharedString The string to be added to the cache
83
     * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
84
     * @return void
85
     */
86 2
    public function addStringForIndex($sharedString, $sharedStringIndex)
87
    {
88 2
        $tempFilePath = $this->getSharedStringTempFilePath($sharedStringIndex);
89
90 2
        list($index, $data) = $this->openCache($tempFilePath);
91
92
        // The shared string retrieval logic expects each cell data to be on one line only
93
        // Encoding the line feed character allows to preserve this assumption
94 2
        $lineFeedEncodedSharedString = $this->escapeLineFeed($sharedString);
95
96 2
        $this->globalFunctionsHelper->fwrite($index, pack('Nn', $this->globalFunctionsHelper->ftell($data), strlen($lineFeedEncodedSharedString) + strlen(PHP_EOL)));
97 2
        $this->globalFunctionsHelper->fwrite($data, $lineFeedEncodedSharedString . PHP_EOL);
98 2
    }
99
100
    /**
101
     * Returns the path for the temp file that should contain the string for the given index
102
     *
103
     * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
104
     * @return string The temp file path for the given index
105
     */
106 2
    protected function getSharedStringTempFilePath($sharedStringIndex)
107
    {
108 2
        $numTempFile = (int) ($sharedStringIndex / $this->maxNumStringsPerTempFile);
109
110 2
        return $this->tempFolder . '/sharedstrings' . $numTempFile;
111
    }
112
113
    /**
114
     * Closes the cache after the last shared string was added.
115
     * This prevents any additional string from being added to the cache.
116
     *
117
     * @return void
118
     */
119 4
    public function closeCache()
120
    {
121
        // close pointer to the last temp file that was written
122 4
        if (!empty($this->tempFilePointers)) {
123 2
            foreach ($this->tempFilePointers as $pointer) {
0 ignored issues
show
Bug introduced by
The expression $this->tempFilePointers of type resource is not traversable.
Loading history...
124 2
                $this->globalFunctionsHelper->fclose($pointer[0]);
125 2
                $this->globalFunctionsHelper->fclose($pointer[1]);
126
            }
127
        }
128 4
        $this->tempFilePointers = [];
0 ignored issues
show
Documentation Bug introduced by
It seems like array() of type array is incompatible with the declared type resource of property $tempFilePointers.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
129 4
    }
130
131
    /**
132
     * Returns the string located at the given index from the cache.
133
     *
134
     * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
135
     * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If no shared string found for the given index
136
     * @return string The shared string at the given index
137
     */
138 2
    public function getStringAtIndex($sharedStringIndex)
139
    {
140 2
        $tempFilePath = $this->getSharedStringTempFilePath($sharedStringIndex);
141 2
        $indexInFile = $sharedStringIndex % $this->maxNumStringsPerTempFile;
142
143 2
        if (!$this->globalFunctionsHelper->file_exists($tempFilePath)) {
144
            throw new SharedStringNotFoundException("Shared string temp file not found: $tempFilePath ; for index: $sharedStringIndex");
145
        }
146
147 2
        list($index, $data) = $this->openCache($tempFilePath);
148
149
        // Read index entry
150 2
        $this->globalFunctionsHelper->fseek($index, $indexInFile * self::INDEX_ENTRY_SIZE);
151 2
        $indexEntryBytes = $this->globalFunctionsHelper->fread($index, self::INDEX_ENTRY_SIZE);
152 2
        $indexEntry = unpack('Noffset/nlen', $indexEntryBytes);
153
154 2
        $sharedString = null;
155 2
        if ($indexEntry['offset'] + $indexEntry['len'] <= filesize($tempFilePath)) {
156 2
            $this->globalFunctionsHelper->fseek($data, $indexEntry['offset']);
157 2
            $escapedSharedString  = $this->globalFunctionsHelper->fread($data, $indexEntry['len']);
158 2
            $sharedString = $this->unescapeLineFeed($escapedSharedString);
159
        }
160
161 2
        if ($sharedString === null) {
162
            throw new SharedStringNotFoundException("Shared string not found for index: $sharedStringIndex");
163
        }
164
165 2
        return rtrim($sharedString, PHP_EOL);
166
    }
167
168
    /**
169
     * Escapes the line feed characters (\n)
170
     *
171
     * @param string $unescapedString
172
     * @return string
173
     */
174 2
    private function escapeLineFeed($unescapedString)
175
    {
176 2
        return str_replace("\n", self::ESCAPED_LINE_FEED_CHARACTER, $unescapedString);
177
    }
178
179
    /**
180
     * Unescapes the line feed characters (\n)
181
     *
182
     * @param string $escapedString
183
     * @return string
184
     */
185 2
    private function unescapeLineFeed($escapedString)
186
    {
187 2
        return str_replace(self::ESCAPED_LINE_FEED_CHARACTER, "\n", $escapedString);
188
    }
189
190
    /**
191
     * Destroys the cache, freeing memory and removing any created artifacts
192
     *
193
     * @return void
194
     */
195 9
    public function clearCache()
196
    {
197 9
        if ($this->tempFolder) {
198 9
            $this->fileSystemHelper->deleteFolderRecursively($this->tempFolder);
199
        }
200 9
    }
201
}
202