├── composer.json
├── license.md
├── readme.md
└── src
└── RobotLoader
└── RobotLoader.php
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nette/robot-loader",
3 | "description": "🍀 Nette RobotLoader: high performance and comfortable autoloader that will search and autoload classes within your application.",
4 | "keywords": ["nette", "autoload", "class", "trait", "interface"],
5 | "homepage": "https://nette.org",
6 | "license": ["BSD-3-Clause", "GPL-2.0-only", "GPL-3.0-only"],
7 | "authors": [
8 | {
9 | "name": "David Grudl",
10 | "homepage": "https://davidgrudl.com"
11 | },
12 | {
13 | "name": "Nette Community",
14 | "homepage": "https://nette.org/contributors"
15 | }
16 | ],
17 | "require": {
18 | "php": "8.0 - 8.4",
19 | "ext-tokenizer": "*",
20 | "nette/utils": "^4.0"
21 | },
22 | "require-dev": {
23 | "nette/tester": "^2.4",
24 | "tracy/tracy": "^2.9",
25 | "phpstan/phpstan-nette": "^2.0@stable"
26 | },
27 | "autoload": {
28 | "classmap": ["src/"],
29 | "psr-4": {
30 | "Nette\\": "src"
31 | }
32 | },
33 | "minimum-stability": "dev",
34 | "scripts": {
35 | "phpstan": "phpstan analyse",
36 | "tester": "tester tests -s"
37 | },
38 | "extra": {
39 | "branch-alias": {
40 | "dev-master": "4.0-dev"
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/license.md:
--------------------------------------------------------------------------------
1 | Licenses
2 | ========
3 |
4 | Good news! You may use Nette Framework under the terms of either
5 | the New BSD License or the GNU General Public License (GPL) version 2 or 3.
6 |
7 | The BSD License is recommended for most projects. It is easy to understand and it
8 | places almost no restrictions on what you can do with the framework. If the GPL
9 | fits better to your project, you can use the framework under this license.
10 |
11 | You don't have to notify anyone which license you are using. You can freely
12 | use Nette Framework in commercial projects as long as the copyright header
13 | remains intact.
14 |
15 | Please be advised that the name "Nette Framework" is a protected trademark and its
16 | usage has some limitations. So please do not use word "Nette" in the name of your
17 | project or top-level domain, and choose a name that stands on its own merits.
18 | If your stuff is good, it will not take long to establish a reputation for yourselves.
19 |
20 |
21 | New BSD License
22 | ---------------
23 |
24 | Copyright (c) 2004, 2014 David Grudl (https://davidgrudl.com)
25 | All rights reserved.
26 |
27 | Redistribution and use in source and binary forms, with or without modification,
28 | are permitted provided that the following conditions are met:
29 |
30 | * Redistributions of source code must retain the above copyright notice,
31 | this list of conditions and the following disclaimer.
32 |
33 | * Redistributions in binary form must reproduce the above copyright notice,
34 | this list of conditions and the following disclaimer in the documentation
35 | and/or other materials provided with the distribution.
36 |
37 | * Neither the name of "Nette Framework" nor the names of its contributors
38 | may be used to endorse or promote products derived from this software
39 | without specific prior written permission.
40 |
41 | This software is provided by the copyright holders and contributors "as is" and
42 | any express or implied warranties, including, but not limited to, the implied
43 | warranties of merchantability and fitness for a particular purpose are
44 | disclaimed. In no event shall the copyright owner or contributors be liable for
45 | any direct, indirect, incidental, special, exemplary, or consequential damages
46 | (including, but not limited to, procurement of substitute goods or services;
47 | loss of use, data, or profits; or business interruption) however caused and on
48 | any theory of liability, whether in contract, strict liability, or tort
49 | (including negligence or otherwise) arising in any way out of the use of this
50 | software, even if advised of the possibility of such damage.
51 |
52 |
53 | GNU General Public License
54 | --------------------------
55 |
56 | GPL licenses are very very long, so instead of including them here we offer
57 | you URLs with full text:
58 |
59 | - [GPL version 2](http://www.gnu.org/licenses/gpl-2.0.html)
60 | - [GPL version 3](http://www.gnu.org/licenses/gpl-3.0.html)
61 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | [](https://doc.nette.org/en/robot-loader)
2 |
3 | [](https://packagist.org/packages/nette/robot-loader)
4 | [](https://github.com/nette/robot-loader/actions)
5 | [](https://coveralls.io/github/nette/robot-loader?branch=master)
6 | [](https://github.com/nette/robot-loader/releases)
7 | [](https://github.com/nette/robot-loader/blob/master/license.md)
8 |
9 |
10 | Introduction
11 | ------------
12 |
13 | RobotLoader is a tool that gives you comfort of automated class loading for your entire application including third-party libraries.
14 |
15 | ✅ get rid of all `require`
16 | ✅ doesn't require strict naming conventions for directories or files
17 | ✅ extremely fast
18 | ✅ no manual cache updates, everything runs automatically
19 | ✅ mature, stable and widely used library
20 |
21 | Thus, we can forget about these familiar code blocks:
22 |
23 | ```php
24 | require_once 'Utils/Page.php';
25 | require_once 'Utils/Style.php';
26 | require_once 'Utils/Paginator.php';
27 | ...
28 | ```
29 |
30 |
31 |
32 | [Support Me](https://github.com/sponsors/dg)
33 | --------------------------------------------
34 |
35 | Do you like RobotLoader? Are you looking forward to the new features?
36 |
37 | [](https://github.com/sponsors/dg)
38 |
39 | Thank you!
40 |
41 |
42 |
43 | Installation
44 | ------------
45 |
46 | You can download RobotLoader as a [single standalone file `RobotLoader.php`](https://github.com/nette/robot-loader/raw/standalone/src/RobotLoader/RobotLoader.php), which you include using `require` in your script, and instantly enjoy comfortable autoloading for the entire application.
47 |
48 | ```php
49 | require '/path/to/RobotLoader.php';
50 |
51 | $loader = new Nette\Loaders\RobotLoader;
52 | // ...
53 | ```
54 |
55 | If you're building an application using [Composer](https://doc.nette.org/en/best-practices/composer), you can install it via:
56 |
57 | ```shell
58 | composer require nette/robot-loader
59 | ```
60 |
61 | It requires PHP version 8.0 and supports PHP up to 8.4.
62 |
63 |
64 |
65 | Usage
66 | -----
67 |
68 | Similar to how the Google robot crawls and indexes web pages, the [RobotLoader](https://api.nette.org/robot-loader/master/Nette/Loaders/RobotLoader.html) goes through all PHP scripts and notes which classes, interfaces, traits and enums it found. It then stores the results in cache for use in subsequent requests. You just need to specify which directories it should go through and where to store the cache:
69 |
70 | ```php
71 | $loader = new Nette\Loaders\RobotLoader;
72 |
73 | // Directories for RobotLoader to index (including subdirectories)
74 | $loader->addDirectory(__DIR__ . '/app');
75 | $loader->addDirectory(__DIR__ . '/libs');
76 |
77 | // Set caching to the 'temp' directory
78 | $loader->setTempDirectory(__DIR__ . '/temp');
79 | $loader->register(); // Activate RobotLoader
80 | ```
81 |
82 | And that's it, from this point on, we don't need to use `require`. Awesome!
83 |
84 | If RobotLoader encounters a duplicate class name during indexing, it will throw an exception and notify you. RobotLoader also automatically updates the cache when it needs to load an unknown class. We recommend turning this off on production servers, see [#Caching].
85 |
86 | If you want RobotLoader to skip certain directories, use `$loader->excludeDirectory('temp')` (can be called multiple times or pass multiple directories).
87 |
88 | By default, RobotLoader reports errors in PHP files by throwing a `ParseError` exception. This can be suppressed using `$loader->reportParseErrors(false)`.
89 |
90 |
91 |
92 | PHP Files Analyzer
93 | ------------------
94 |
95 | RobotLoader can also be used purely for finding classes, interfaces, traits and enums in PHP files **without** using the autoloading function:
96 |
97 | ```php
98 | $loader = new Nette\Loaders\RobotLoader;
99 | $loader->addDirectory(__DIR__ . '/app');
100 |
101 | // Scans directories for classes/interfaces/traits/enums
102 | $loader->rebuild();
103 |
104 | // Returns an array of class => filename pairs
105 | $res = $loader->getIndexedClasses();
106 | ```
107 |
108 | Even with such usage, you can utilize caching. This ensures that unchanged files won't be rescanned:
109 |
110 | ```php
111 | $loader = new Nette\Loaders\RobotLoader;
112 | $loader->addDirectory(__DIR__ . '/app');
113 |
114 | // Set caching to the 'temp' directory
115 | $loader->setTempDirectory(__DIR__ . '/temp');
116 |
117 | // Scans directories using cache
118 | $loader->refresh();
119 |
120 | // Returns an array of class => filename pairs
121 | $res = $loader->getIndexedClasses();
122 | ```
123 |
124 |
125 |
126 | Caching
127 | -------
128 |
129 | RobotLoader is very fast because it cleverly uses caching.
130 |
131 | During development, you hardly notice it running in the background. It continuously updates its cache, considering that classes and files can be created, deleted, renamed, etc. And it doesn't rescan unchanged files.
132 |
133 | On a production server, on the other hand, we recommend turning off cache updates using `$loader->setAutoRefresh(false)` (in a Nette Application, this happens automatically), because files don't change. At the same time, it's necessary to **clear the cache** when uploading a new version to hosting.
134 |
135 | The initial file scanning, when the cache doesn't exist yet, can naturally take a moment for larger applications. RobotLoader has built-in prevention against [cache stampede](https://en.wikipedia.org/wiki/Cache_stampede).
136 | This is a situation where a large number of concurrent requests on a production server would trigger RobotLoader, and since the cache doesn't exist yet, they would all start scanning files, which would overload the server.
137 | Fortunately, RobotLoader works in such a way that only the first thread indexes the files, creates the cache, and the rest wait and then use the cache.
138 |
139 |
140 |
141 | PSR-4
142 | -----
143 |
144 | Nowadays, you can use [Composer for autoloading](https://doc.nette.org/en/best-practices/composer#toc-autoloading) while adhering to PSR-4. Simply put, it's a system where namespaces and class names correspond to the directory structure and file names, e.g., `App\Router\RouterFactory` will be in the file `/path/to/App/Router/RouterFactory.php`.
145 |
146 | RobotLoader isn't tied to any fixed structure, so it's useful in situations where you don't want to have the directory structure designed exactly like the PHP namespaces, or when developing an application that historically doesn't use such conventions. It's also possible to use both loaders together.
147 |
148 |
149 | If you like RobotLoader, **[please make a donation now](https://nette.org/donate)**. Thank you!
150 |
--------------------------------------------------------------------------------
/src/RobotLoader/RobotLoader.php:
--------------------------------------------------------------------------------
1 |
21 | * $loader = new Nette\Loaders\RobotLoader;
22 | * $loader->addDirectory('app');
23 | * $loader->excludeDirectory('app/exclude');
24 | * $loader->setTempDirectory('temp');
25 | * $loader->register();
26 | *
27 | */
28 | class RobotLoader
29 | {
30 | private const RetryLimit = 3;
31 |
32 | /** @var string[] */
33 | public array $ignoreDirs = ['.*', '*.old', '*.bak', '*.tmp', 'temp'];
34 |
35 | /** @var string[] */
36 | public array $acceptFiles = ['*.php'];
37 | private bool $autoRebuild = true;
38 | private bool $reportParseErrors = true;
39 |
40 | /** @var string[] */
41 | private array $scanPaths = [];
42 |
43 | /** @var string[] */
44 | private array $excludeDirs = [];
45 |
46 | /** @var array class => [file, time] */
47 | private array $classes = [];
48 | private bool $cacheLoaded = false;
49 | private bool $refreshed = false;
50 |
51 | /** @var array class => counter */
52 | private array $missingClasses = [];
53 |
54 | /** @var array file => mtime */
55 | private array $emptyFiles = [];
56 | private ?string $tempDirectory = null;
57 | private bool $needSave = false;
58 |
59 |
60 | public function __construct()
61 | {
62 | if (!extension_loaded('tokenizer')) {
63 | throw new Nette\NotSupportedException('PHP extension Tokenizer is not loaded.');
64 | }
65 | }
66 |
67 |
68 | public function __destruct()
69 | {
70 | if ($this->needSave) {
71 | $this->saveCache();
72 | }
73 | }
74 |
75 |
76 | /**
77 | * Register autoloader.
78 | */
79 | public function register(bool $prepend = false): static
80 | {
81 | spl_autoload_register([$this, 'tryLoad'], prepend: $prepend);
82 | return $this;
83 | }
84 |
85 |
86 | /**
87 | * Handles autoloading of classes, interfaces or traits.
88 | */
89 | public function tryLoad(string $type): void
90 | {
91 | $this->loadCache();
92 |
93 | $missing = $this->missingClasses[$type] ?? null;
94 | if ($missing >= self::RetryLimit) {
95 | return;
96 | }
97 |
98 | [$file, $mtime] = $this->classes[$type] ?? null;
99 |
100 | if ($this->autoRebuild) {
101 | if (!$this->refreshed) {
102 | if (!$file || !is_file($file)) {
103 | $this->refreshClasses();
104 | [$file] = $this->classes[$type] ?? null;
105 | $this->needSave = true;
106 |
107 | } elseif (filemtime($file) !== $mtime) {
108 | $this->updateFile($file);
109 | [$file] = $this->classes[$type] ?? null;
110 | $this->needSave = true;
111 | }
112 | }
113 |
114 | if (!$file || !is_file($file)) {
115 | $this->missingClasses[$type] = ++$missing;
116 | $this->needSave = $this->needSave || $file || ($missing <= self::RetryLimit);
117 | unset($this->classes[$type]);
118 | $file = null;
119 | }
120 | }
121 |
122 | if ($file) {
123 | (static function ($file) { require $file; })($file);
124 | }
125 | }
126 |
127 |
128 | /**
129 | * Add path or paths to list.
130 | */
131 | public function addDirectory(string ...$paths): static
132 | {
133 | $this->scanPaths = array_merge($this->scanPaths, $paths);
134 | return $this;
135 | }
136 |
137 |
138 | public function reportParseErrors(bool $state = true): static
139 | {
140 | $this->reportParseErrors = $state;
141 | return $this;
142 | }
143 |
144 |
145 | /**
146 | * Excludes path or paths from list.
147 | */
148 | public function excludeDirectory(string ...$paths): static
149 | {
150 | $this->excludeDirs = array_merge($this->excludeDirs, $paths);
151 | return $this;
152 | }
153 |
154 |
155 | /**
156 | * @return array class => filename
157 | */
158 | public function getIndexedClasses(): array
159 | {
160 | $this->loadCache();
161 | $res = [];
162 | foreach ($this->classes as $class => [$file]) {
163 | $res[$class] = $file;
164 | }
165 |
166 | return $res;
167 | }
168 |
169 |
170 | /**
171 | * Rebuilds class list cache.
172 | */
173 | public function rebuild(): void
174 | {
175 | $this->cacheLoaded = true;
176 | $this->classes = $this->missingClasses = $this->emptyFiles = [];
177 | $this->refreshClasses();
178 | if ($this->tempDirectory) {
179 | $this->saveCache();
180 | }
181 | }
182 |
183 |
184 | /**
185 | * Refreshes class list cache.
186 | */
187 | public function refresh(): void
188 | {
189 | $this->loadCache();
190 | if (!$this->refreshed) {
191 | $this->refreshClasses();
192 | $this->saveCache();
193 | }
194 | }
195 |
196 |
197 | /**
198 | * Refreshes $this->classes & $this->emptyFiles.
199 | */
200 | private function refreshClasses(): void
201 | {
202 | $this->refreshed = true; // prevents calling refreshClasses() or updateFile() in tryLoad()
203 | $files = $this->emptyFiles;
204 | $classes = [];
205 | foreach ($this->classes as $class => [$file, $mtime]) {
206 | $files[$file] = $mtime;
207 | $classes[$file][] = $class;
208 | }
209 |
210 | $this->classes = $this->emptyFiles = [];
211 |
212 | foreach ($this->scanPaths as $path) {
213 | $iterator = is_file($path)
214 | ? [new SplFileInfo($path)]
215 | : $this->createFileIterator($path);
216 |
217 | foreach ($iterator as $fileInfo) {
218 | $mtime = $fileInfo->getMTime();
219 | $file = $fileInfo->getPathname();
220 | $foundClasses = isset($files[$file]) && $files[$file] === $mtime
221 | ? ($classes[$file] ?? [])
222 | : $this->scanPhp($file);
223 |
224 | if (!$foundClasses) {
225 | $this->emptyFiles[$file] = $mtime;
226 | }
227 |
228 | $files[$file] = $mtime;
229 | $classes[$file] = []; // prevents the error when adding the same file twice
230 |
231 | foreach ($foundClasses as $class) {
232 | if (isset($this->classes[$class])) {
233 | throw new Nette\InvalidStateException(sprintf(
234 | 'Ambiguous class %s resolution; defined in %s and in %s.',
235 | $class,
236 | $this->classes[$class][0],
237 | $file,
238 | ));
239 | }
240 |
241 | $this->classes[$class] = [$file, $mtime];
242 | unset($this->missingClasses[$class]);
243 | }
244 | }
245 | }
246 | }
247 |
248 |
249 | /**
250 | * Creates an iterator scanning directory for PHP files and subdirectories.
251 | * @throws Nette\IOException if path is not found
252 | */
253 | private function createFileIterator(string $dir): Nette\Utils\Finder
254 | {
255 | if (!is_dir($dir)) {
256 | throw new Nette\IOException(sprintf("Directory '%s' not found.", $dir));
257 | }
258 |
259 | $dir = realpath($dir) ?: $dir; // realpath does not work in phar
260 | $disallow = [];
261 | foreach (array_merge($this->ignoreDirs, $this->excludeDirs) as $item) {
262 | if ($item = realpath($item)) {
263 | $disallow[$item] = true;
264 | }
265 | }
266 |
267 | return Nette\Utils\Finder::findFiles($this->acceptFiles)
268 | ->filter($filter = fn(SplFileInfo $file) => $file->getRealPath() === false || !isset($disallow[$file->getRealPath()]))
269 | ->descentFilter($filter)
270 | ->from($dir)
271 | ->exclude($this->ignoreDirs);
272 | }
273 |
274 |
275 | private function updateFile(string $file): void
276 | {
277 | foreach ($this->classes as $class => [$prevFile]) {
278 | if ($file === $prevFile) {
279 | unset($this->classes[$class]);
280 | }
281 | }
282 |
283 | $foundClasses = is_file($file) ? $this->scanPhp($file) : [];
284 |
285 | foreach ($foundClasses as $class) {
286 | [$prevFile, $prevMtime] = $this->classes[$class] ?? null;
287 |
288 | if (isset($prevFile) && @filemtime($prevFile) !== $prevMtime) { // @ file may not exist
289 | $this->updateFile($prevFile);
290 | [$prevFile] = $this->classes[$class] ?? null;
291 | }
292 |
293 | if (isset($prevFile)) {
294 | throw new Nette\InvalidStateException(sprintf(
295 | 'Ambiguous class %s resolution; defined in %s and in %s.',
296 | $class,
297 | $prevFile,
298 | $file,
299 | ));
300 | }
301 |
302 | $this->classes[$class] = [$file, filemtime($file)];
303 | }
304 | }
305 |
306 |
307 | /**
308 | * Searches classes, interfaces and traits in PHP file.
309 | * @return string[]
310 | */
311 | private function scanPhp(string $file): array
312 | {
313 | $code = file_get_contents($file);
314 | $expected = false;
315 | $namespace = $name = '';
316 | $level = $minLevel = 0;
317 | $classes = [];
318 |
319 | try {
320 | $tokens = \PhpToken::tokenize($code, TOKEN_PARSE);
321 | } catch (\ParseError $e) {
322 | if ($this->reportParseErrors) {
323 | $rp = new \ReflectionProperty($e, 'file');
324 | $rp->setAccessible(true);
325 | $rp->setValue($e, $file);
326 | throw $e;
327 | }
328 |
329 | $tokens = [];
330 | }
331 |
332 | foreach ($tokens as $token) {
333 | switch ($token->id) {
334 | case T_COMMENT:
335 | case T_DOC_COMMENT:
336 | case T_WHITESPACE:
337 | continue 2;
338 |
339 | case T_STRING:
340 | case T_NAME_QUALIFIED:
341 | if ($expected) {
342 | $name .= $token->text;
343 | }
344 |
345 | continue 2;
346 |
347 | case T_NAMESPACE:
348 | case T_CLASS:
349 | case T_INTERFACE:
350 | case T_TRAIT:
351 | case PHP_VERSION_ID < 80100
352 | ? T_CLASS
353 | : T_ENUM:
354 | $expected = $token->id;
355 | $name = '';
356 | continue 2;
357 | }
358 |
359 | if ($expected) {
360 | if ($expected === T_NAMESPACE) {
361 | $namespace = $name ? $name . '\\' : '';
362 | $minLevel = $token->text === '{' ? 1 : 0;
363 |
364 | } elseif ($name && $level === $minLevel) {
365 | $classes[] = $namespace . $name;
366 | }
367 |
368 | $expected = null;
369 | }
370 |
371 | if ($token->text === '{') {
372 | $level++;
373 | } elseif ($token->text === '}') {
374 | $level--;
375 | }
376 | }
377 |
378 | return $classes;
379 | }
380 |
381 |
382 | /********************* caching ****************d*g**/
383 |
384 |
385 | /**
386 | * Sets auto-refresh mode.
387 | */
388 | public function setAutoRefresh(bool $state = true): static
389 | {
390 | $this->autoRebuild = $state;
391 | return $this;
392 | }
393 |
394 |
395 | /**
396 | * Sets path to temporary directory.
397 | */
398 | public function setTempDirectory(string $dir): static
399 | {
400 | if (!FileSystem::isAbsolute($dir)) {
401 | throw new Nette\InvalidArgumentException("Temporary directory must be absolute, '$dir' given.");
402 | }
403 | FileSystem::createDir($dir);
404 | $this->tempDirectory = $dir;
405 | return $this;
406 | }
407 |
408 |
409 | /**
410 | * Loads class list from cache.
411 | */
412 | private function loadCache(): void
413 | {
414 | if ($this->cacheLoaded) {
415 | return;
416 | }
417 |
418 | $this->cacheLoaded = true;
419 |
420 | $file = $this->generateCacheFileName();
421 |
422 | // Solving atomicity to work everywhere is really pain in the ass.
423 | // 1) We want to do as little as possible IO calls on production and also directory and file can be not writable (#19)
424 | // so on Linux we include the file directly without shared lock, therefore, the file must be created atomically by renaming.
425 | // 2) On Windows file cannot be renamed-to while is open (ie by include() #11), so we have to acquire a lock.
426 | $lock = defined('PHP_WINDOWS_VERSION_BUILD')
427 | ? $this->acquireLock("$file.lock", LOCK_SH)
428 | : null;
429 |
430 | $data = @include $file; // @ file may not exist
431 | if (is_array($data)) {
432 | [$this->classes, $this->missingClasses, $this->emptyFiles] = $data;
433 | return;
434 | }
435 |
436 | if ($lock) {
437 | flock($lock, LOCK_UN); // release shared lock so we can get exclusive
438 | }
439 |
440 | $lock = $this->acquireLock("$file.lock", LOCK_EX);
441 |
442 | // while waiting for exclusive lock, someone might have already created the cache
443 | $data = @include $file; // @ file may not exist
444 | if (is_array($data)) {
445 | [$this->classes, $this->missingClasses, $this->emptyFiles] = $data;
446 | return;
447 | }
448 |
449 | $this->classes = $this->missingClasses = $this->emptyFiles = [];
450 | $this->refreshClasses();
451 | $this->saveCache($lock);
452 | // On Windows concurrent creation and deletion of a file can cause a 'permission denied' error,
453 | // therefore, we will not delete the lock file. Windows is really annoying.
454 | }
455 |
456 |
457 | /**
458 | * Writes class list to cache.
459 | * @param resource $lock
460 | */
461 | private function saveCache($lock = null): void
462 | {
463 | // we have to acquire a lock to be able safely rename file
464 | // on Linux: that another thread does not rename the same named file earlier
465 | // on Windows: that the file is not read by another thread
466 | $file = $this->generateCacheFileName();
467 | $lock = $lock ?: $this->acquireLock("$file.lock", LOCK_EX);
468 | $code = "classes, $this->missingClasses, $this->emptyFiles], true) . ";\n";
469 |
470 | if (file_put_contents("$file.tmp", $code) !== strlen($code) || !rename("$file.tmp", $file)) {
471 | @unlink("$file.tmp"); // @ file may not exist
472 | throw new \RuntimeException(sprintf("Unable to create '%s'.", $file));
473 | }
474 |
475 | if (function_exists('opcache_invalidate')) {
476 | @opcache_invalidate($file, force: true); // @ can be restricted
477 | }
478 | }
479 |
480 |
481 | /** @return resource */
482 | private function acquireLock(string $file, int $mode)
483 | {
484 | $handle = @fopen($file, 'w'); // @ is escalated to exception
485 | if (!$handle) {
486 | throw new \RuntimeException(sprintf("Unable to create file '%s'. %s", $file, error_get_last()['message']));
487 | } elseif (!@flock($handle, $mode)) { // @ is escalated to exception
488 | throw new \RuntimeException(sprintf(
489 | "Unable to acquire %s lock on file '%s'. %s",
490 | $mode & LOCK_EX ? 'exclusive' : 'shared',
491 | $file,
492 | error_get_last()['message'],
493 | ));
494 | }
495 |
496 | return $handle;
497 | }
498 |
499 |
500 | private function generateCacheFileName(): string
501 | {
502 | if (!$this->tempDirectory) {
503 | throw new \LogicException('Set path to temporary directory using setTempDirectory().');
504 | }
505 |
506 | return $this->tempDirectory . '/' . hash(PHP_VERSION_ID < 80100 ? 'md5' : 'xxh128', serialize($this->generateCacheKey())) . '.php';
507 | }
508 |
509 |
510 | protected function generateCacheKey(): array
511 | {
512 | return [$this->ignoreDirs, $this->acceptFiles, $this->scanPaths, $this->excludeDirs, 'v2'];
513 | }
514 | }
515 |
--------------------------------------------------------------------------------