Repository navigation
Expand file tree
/
Copy pathFileCacheStorage.php
More file actions
206 lines (178 loc) · 5.8 KB
/
Copy pathFileCacheStorage.php
File metadata and controls
206 lines (178 loc) · 5.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
<?php declare(strict_types = 1);
namespace PHPStan\Cache;
use InvalidArgumentException;
use Nette\Utils\Random;
use PHPStan\File\CouldNotReadFileException;
use PHPStan\File\CouldNotWriteFileException;
use PHPStan\File\FileReader;
use PHPStan\File\FileWriter;
use PHPStan\Internal\DirectoryCreator;
use PHPStan\Internal\DirectoryCreatorException;
use PHPStan\ShouldNotHappenException;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use function array_keys;
use function closedir;
use function dirname;
use function error_get_last;
use function file_get_contents;
use function hash;
use function is_dir;
use function is_file;
use function opendir;
use function readdir;
use function rename;
use function rmdir;
use function serialize;
use function sprintf;
use function str_ends_with;
use function strlen;
use function substr;
use function uksort;
use function unlink;
use function unserialize;
use const DIRECTORY_SEPARATOR;
/**
* Entries are stored as serialize() payloads in *.dat files. They used to be
* var_export()ed PHP files loaded with include: compiling such a file costs
* more than unserializing the same data, every hydrated object goes through
* a userland __set_state() call, and the compiled code stays in memory for the
* rest of the process - 11 % of the peak memory of a single-process analysis.
* The separate extension keeps an older PHPStan sharing the directory from
* including a payload that is not PHP.
*/
final class FileCacheStorage implements CacheStorage
{
private const CACHED_CLEARED_VERSION = 'v3-serialized';
public function __construct(private string $directory)
{
}
public function load(string $key, string $variableKey): mixed
{
[,, $filePath] = $this->getFilePaths($key);
// Letting include() fail is an expensive way to learn that an entry was
// never written: PHP walks the include_path and builds two warnings only
// to have them discarded by the @, about 29us per miss, while the stat()
// that rules the file out costs about 1.4us and is free on the hit path
// (the include stats the file anyway). Misses are not rare - every class
// name that reaches the PHP-internal locator and turns out not to be a
// built-in is one, tens of thousands per run and per worker.
if (!is_file($filePath)) {
return null;
}
$contents = @file_get_contents($filePath);
if ($contents === false) {
return null;
}
$cacheItem = @unserialize($contents);
if (!$cacheItem instanceof CacheItem) {
return null;
}
if (!$cacheItem->isVariableKeyValid($variableKey)) {
return null;
}
return $cacheItem->getData();
}
/**
* @throws DirectoryCreatorException
*/
public function save(string $key, string $variableKey, mixed $data): void
{
[$firstDirectory, $secondDirectory, $path] = $this->getFilePaths($key);
DirectoryCreator::ensureDirectoryExists($this->directory, 0777);
DirectoryCreator::ensureDirectoryExists($firstDirectory, 0777);
DirectoryCreator::ensureDirectoryExists($secondDirectory, 0777);
$tmpPath = sprintf('%s/%s.tmp', $this->directory, Random::generate());
$errorBefore = error_get_last();
$serialized = @serialize(new CacheItem($variableKey, $data));
$errorAfter = error_get_last();
if ($errorAfter !== null && $errorBefore !== $errorAfter) {
throw new ShouldNotHappenException(sprintf('Error occurred while saving item %s (%s) to cache: %s', $key, $variableKey, $errorAfter['message']));
}
FileWriter::write($tmpPath, $serialized);
$renameSuccess = @rename($tmpPath, $path);
if ($renameSuccess) {
return;
}
@unlink($tmpPath);
if (DIRECTORY_SEPARATOR === '/' || !is_file($path)) {
throw new InvalidArgumentException(sprintf('Could not write data to cache file %s.', $path));
}
}
/**
* @param non-empty-string $key
*
* @return array{string, string, string}
*/
private function getFilePaths(string $key): array
{
$keyHash = hash('sha256', $key);
$firstDirectory = sprintf('%s/%s', $this->directory, substr($keyHash, 0, 2));
$secondDirectory = sprintf('%s/%s', $firstDirectory, substr($keyHash, 2, 2));
$filePath = sprintf('%s/%s.dat', $secondDirectory, $keyHash);
return [
$firstDirectory,
$secondDirectory,
$filePath,
];
}
public function clearUnusedFiles(): void
{
if (!is_dir($this->directory)) {
return;
}
$cachedClearedFile = $this->directory . '/cache-cleared';
if (is_file($cachedClearedFile)) {
try {
$cachedClearedContents = FileReader::read($cachedClearedFile);
if ($cachedClearedContents === self::CACHED_CLEARED_VERSION) {
return;
}
} catch (CouldNotReadFileException) {
return;
}
}
// every entry written before the serialize() format is a *.php file;
// none of them is read anymore
$iterator = new RecursiveDirectoryIterator($this->directory);
$iterator->setFlags(RecursiveDirectoryIterator::SKIP_DOTS);
$files = new RecursiveIteratorIterator($iterator);
$emptyDirectoriesToCheck = [];
foreach ($files as $file) {
$path = $file->getPathname();
if (!str_ends_with($path, '.php')) {
continue;
}
$emptyDirectoriesToCheck[dirname($path)] = true;
$emptyDirectoriesToCheck[dirname($path, 2)] = true;
@unlink($path);
}
uksort($emptyDirectoriesToCheck, static fn ($a, $b) => strlen($b) - strlen($a));
foreach (array_keys($emptyDirectoriesToCheck) as $directory) {
if (!$this->isDirectoryEmpty($directory)) {
continue;
}
@rmdir($directory);
}
try {
FileWriter::write($cachedClearedFile, self::CACHED_CLEARED_VERSION);
} catch (CouldNotWriteFileException) {
// pass
}
}
private function isDirectoryEmpty(string $directory): bool
{
$handle = opendir($directory);
if ($handle === false) {
return false;
}
while (($entry = readdir($handle)) !== false) {
if ($entry !== '.' && $entry !== '..') {
closedir($handle);
return false;
}
}
closedir($handle);
return true;
}
}