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

FileBasedStrategy   A

Complexity

Total Complexity 16

Size/Duplication

Total Lines 186
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 96.49%

Importance

Changes 0
Metric Value
wmc 16
lcom 1
cbo 4
dl 0
loc 186
rs 10
c 0
b 0
f 0
ccs 55
cts 57
cp 0.9649

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
A openCache() 0 15 2
A addStringForIndex() 0 13 1
A getSharedStringTempFilePath() 0 6 1
A closeCache() 0 11 3
A getStringAtIndex() 0 29 4
A escapeLineFeed() 0 4 1
A unescapeLineFeed() 0 4 1
A clearCache() 0 6 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 2
    	if (!array_key_exists($tempFilePath, $this->tempFilePointers)) {
64
			// Open index file and seek to end
65 2
			$index = $this->globalFunctionsHelper->fopen($tempFilePath  . '.index', 'c+');
66 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 65 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...
67
68
    		// Open data file and seek to end
69 2
			$data = $this->globalFunctionsHelper->fopen($tempFilePath, 'c+');
70 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 69 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...
71
72 2
			$this->tempFilePointers[$tempFilePath] = [$index, $data];
73
		}
74
75 2
    	return $this->tempFilePointers[$tempFilePath];
76
	}
77
78
    /**
79
     * Adds the given string to the cache.
80
     *
81
     * @param string $sharedString The string to be added to the cache
82
     * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
83
     * @return void
84
     */
85 2
    public function addStringForIndex($sharedString, $sharedStringIndex)
86
    {
87 2
        $tempFilePath = $this->getSharedStringTempFilePath($sharedStringIndex);
88
89 2
        list($index, $data) = $this->openCache($tempFilePath);
90
91
        // The shared string retrieval logic expects each cell data to be on one line only
92
        // Encoding the line feed character allows to preserve this assumption
93 2
        $lineFeedEncodedSharedString = $this->escapeLineFeed($sharedString);
94
95 2
		$this->globalFunctionsHelper->fwrite($index, pack('Nn', $this->globalFunctionsHelper->ftell($data), strlen($lineFeedEncodedSharedString) + strlen(PHP_EOL)));
96 2
        $this->globalFunctionsHelper->fwrite($data, $lineFeedEncodedSharedString . PHP_EOL);
97 2
    }
98
99
    /**
100
     * Returns the path for the temp file that should contain the string for the given index
101
     *
102
     * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
103
     * @return string The temp file path for the given index
104
     */
105 2
    protected function getSharedStringTempFilePath($sharedStringIndex)
106
    {
107 2
        $numTempFile = (int) ($sharedStringIndex / $this->maxNumStringsPerTempFile);
108
109 2
        return $this->tempFolder . '/sharedstrings' . $numTempFile;
110
    }
111
112
    /**
113
     * Closes the cache after the last shared string was added.
114
     * This prevents any additional string from being added to the cache.
115
     *
116
     * @return void
117
     */
118 4
    public function closeCache()
119
    {
120
        // close pointer to the last temp file that was written
121 4
        if (!empty($this->tempFilePointers)) {
122 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...
123 2
				$this->globalFunctionsHelper->fclose($pointer[0]);
124 2
				$this->globalFunctionsHelper->fclose($pointer[1]);
125
			}
126
        }
127 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...
128 4
    }
129
130
    /**
131
     * Returns the string located at the given index from the cache.
132
     *
133
     * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
134
     * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If no shared string found for the given index
135
     * @return string The shared string at the given index
136
     */
137 2
    public function getStringAtIndex($sharedStringIndex)
138
    {
139 2
        $tempFilePath = $this->getSharedStringTempFilePath($sharedStringIndex);
140 2
        $indexInFile = $sharedStringIndex % $this->maxNumStringsPerTempFile;
141
142 2
        if (!$this->globalFunctionsHelper->file_exists($tempFilePath)) {
143
            throw new SharedStringNotFoundException("Shared string temp file not found: $tempFilePath ; for index: $sharedStringIndex");
144
        }
145
146 2
        list($index, $data) = $this->openCache($tempFilePath);
147
148
        // Read index entry
149 2
		$this->globalFunctionsHelper->fseek($index, $indexInFile * self::INDEX_ENTRY_SIZE);
150 2
		$indexEntryBytes = $this->globalFunctionsHelper->fread($index, self::INDEX_ENTRY_SIZE);
151 2
		$indexEntry = unpack('Noffset/nlen', $indexEntryBytes);
152
153 2
		$sharedString = null;
154 2
		if ($indexEntry['offset'] + $indexEntry['len'] <= filesize($tempFilePath)) {
155 2
			$this->globalFunctionsHelper->fseek($data, $indexEntry['offset']);
156 2
			$escapedSharedString  = $this->globalFunctionsHelper->fread($data, $indexEntry['len']);
157 2
			$sharedString = $this->unescapeLineFeed($escapedSharedString);
158
		}
159
160 2
        if ($sharedString === null) {
161
            throw new SharedStringNotFoundException("Shared string not found for index: $sharedStringIndex");
162
        }
163
164 2
        return rtrim($sharedString, PHP_EOL);
165
    }
166
167
    /**
168
     * Escapes the line feed characters (\n)
169
     *
170
     * @param string $unescapedString
171
     * @return string
172
     */
173 2
    private function escapeLineFeed($unescapedString)
174
    {
175 2
        return str_replace("\n", self::ESCAPED_LINE_FEED_CHARACTER, $unescapedString);
176
    }
177
178
    /**
179
     * Unescapes the line feed characters (\n)
180
     *
181
     * @param string $escapedString
182
     * @return string
183
     */
184 2
    private function unescapeLineFeed($escapedString)
185
    {
186 2
        return str_replace(self::ESCAPED_LINE_FEED_CHARACTER, "\n", $escapedString);
187
    }
188
189
    /**
190
     * Destroys the cache, freeing memory and removing any created artifacts
191
     *
192
     * @return void
193
     */
194 9
    public function clearCache()
195
    {
196 9
        if ($this->tempFolder) {
197 9
            $this->fileSystemHelper->deleteFolderRecursively($this->tempFolder);
198
        }
199 9
    }
200
}
201