Skip to content

Commit 74cd0a4

Browse files
committed
Enhance PathMatcher and StreamWrapper performance; streamline path checks and remove unused static cache
1 parent de7d938 commit 74cd0a4

2 files changed

Lines changed: 38 additions & 57 deletions

File tree

src/Internal/PathMatcher.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ public static function isDynamicWritablePath(string $normalizedPath): bool
212212
public static function mayPathBeIncluded(string $normalizedPath): bool
213213
{
214214
$canon = self::canonicalizePath($normalizedPath);
215-
215+
216216
if (str_contains($canon, '/node_modules/') || str_starts_with($canon, 'node_modules/')) {
217217
return false;
218218
}
@@ -231,7 +231,8 @@ public static function mayPathBeIncluded(string $normalizedPath): bool
231231
}
232232
}
233233

234-
if (str_contains($canon, '/var/') || str_starts_with($canon, 'var/')) {
234+
if (str_contains($canon, '/var/cache/') || str_starts_with($canon, 'var/cache/')
235+
|| str_contains($canon, '/var/log/') || str_starts_with($canon, 'var/log/')) {
235236
if (! self::hasIncludeMatchingPrefix('var/', $includes)) {
236237
return false;
237238
}
@@ -369,4 +370,4 @@ private static function getCompiledPatterns(array $globs, string $baseDir, strin
369370

370371
return self::$compiledExcludesCache[$cacheKey] = $compiled;
371372
}
372-
}
373+
}

src/Internal/StreamWrapper.php

Lines changed: 34 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,6 @@ final class StreamWrapper implements StreamWrapperInterface
4848
*/
4949
private static array $statCache = [];
5050

51-
/**
52-
* In-memory cache for static-path negative misses only (e.g. vendor).
53-
* Never stores negative misses for dynamic paths (var/cache, storage).
54-
*
55-
* @var array<string, true>
56-
*/
57-
private static array $staticNegativeStatCache = [];
58-
5951
/**
6052
* In-memory cache for isApplicationFile path decisions.
6153
*
@@ -83,7 +75,6 @@ final class StreamWrapper implements StreamWrapperInterface
8375
public static function reset(): void
8476
{
8577
self::$statCache = [];
86-
self::$staticNegativeStatCache = [];
8778
self::$appFileDecisionCache = [];
8879
PathMatcher::reset();
8980
}
@@ -194,24 +185,29 @@ public static function transformSource(string $source, string $filePath = ''): s
194185
*/
195186
public function stream_open(string $path, string $mode, int $options, ?string &$openedPath): bool
196187
{
188+
// Fast-path 1: Write and append modes are never transformed
197189
if ($mode !== 'r' && $mode !== 'rb' && $mode !== 'rt') {
198190
return $this->openDirectHandle($path, $mode);
199191
}
200192

193+
// Fast-path 2: Non-PHP files are never transformed
201194
if (! str_ends_with(strtolower($path), '.php')) {
202195
return $this->openDirectHandle($path, $mode);
203196
}
204197

198+
// Fast-path 3: Master switch disabled
205199
if (! Config::isEnabled()) {
206200
return $this->openDirectHandle($path, $mode);
207201
}
208202

209203
$normalizedRaw = str_replace('\\', '/', $path);
210204

211-
if (! PathMatcher::mayPathBeIncluded($normalizedRaw)) {
205+
// Fast-path 4: Node modules is never PHP app code
206+
if (str_contains($normalizedRaw, '/node_modules/') || str_starts_with($normalizedRaw, 'node_modules/')) {
212207
return $this->openDirectHandle($path, $mode);
213208
}
214209

210+
// Fast-path 5: Check in-memory decision cache
215211
if (isset(self::$appFileDecisionCache[$normalizedRaw])) {
216212
if (! self::$appFileDecisionCache[$normalizedRaw]) {
217213
return $this->openDirectHandle($path, $mode);
@@ -233,6 +229,7 @@ public function stream_open(string $path, string $mode, int $options, ?string &$
233229
return $this->openDirectHandle($normalizedResolved, $mode);
234230
}
235231

232+
// Fast-path 6: Return clean untransformed code for error screens / source viewers
236233
if (self::isReadOnlyCall()) {
237234
return $this->openDirectHandle($normalizedResolved, $mode);
238235
}
@@ -276,6 +273,26 @@ private function openDirectHandle(string $targetFile, string $mode): bool
276273
return $this->handle !== null;
277274
}
278275

276+
/**
277+
* Executes a callback while temporarily suppressing PHP error and warning handlers.
278+
*
279+
* @template T
280+
*
281+
* @param callable(): T $callback
282+
*
283+
* @return T
284+
*/
285+
private static function silent(callable $callback): mixed
286+
{
287+
set_error_handler(fn () => true);
288+
289+
try {
290+
return $callback();
291+
} finally {
292+
restore_error_handler();
293+
}
294+
}
295+
279296
public function stream_read(int $count): string
280297
{
281298
if ($this->handle === null || $count <= 0) {
@@ -396,10 +413,6 @@ public function stream_close(): void
396413

397414
/**
398415
* High-speed stat resolution with $O(1)$ memoization cache.
399-
* Caches positive stat hits.
400-
* Only caches negative misses for STATIC directories (vendor, tests).
401-
* NEVER caches negative misses for dynamic writable paths (var/cache, storage),
402-
* guaranteeing Symfony/Shopware cache creation is detected immediately.
403416
*
404417
* @return array<int|string, int>|false
405418
*/
@@ -411,21 +424,13 @@ public function url_stat(string $path, int $flags): array|false
411424
return self::$statCache[$normalized];
412425
}
413426

414-
if (isset(self::$staticNegativeStatCache[$normalized])) {
415-
return false;
416-
}
417-
418427
self::unregister();
419428
/** @var array<int|string, int>|false $result */
420429
$result = self::silent(fn () => stat($path));
421430
self::register();
422431

423432
if ($result !== false) {
424433
self::$statCache[$normalized] = $result;
425-
} else {
426-
if (! PathMatcher::isDynamicWritablePath($normalized)) {
427-
self::$staticNegativeStatCache[$normalized] = true;
428-
}
429434
}
430435

431436
return $result;
@@ -434,7 +439,7 @@ public function url_stat(string $path, int $flags): array|false
434439
public function stream_metadata(string $path, int $option, mixed $value): bool
435440
{
436441
$normalized = str_replace('\\', '/', $path);
437-
unset(self::$statCache[$normalized], self::$staticNegativeStatCache[$normalized]);
442+
unset(self::$statCache[$normalized]);
438443

439444
self::unregister();
440445
$result = false;
@@ -458,7 +463,7 @@ public function dir_opendir(string $path, int $options): bool
458463
{
459464
self::unregister();
460465
/** @var resource|false $dh */
461-
$dh = self::silent(fn () => opendir($path));
466+
$dh = self::silent(fn () => @opendir($path));
462467
$this->dirHandle = $dh !== false ? $dh : null;
463468
self::register();
464469

@@ -498,7 +503,7 @@ public function dir_closedir(): bool
498503
public function mkdir(string $path, int $mode, int $options): bool
499504
{
500505
$normalized = str_replace('\\', '/', $path);
501-
unset(self::$statCache[$normalized], self::$staticNegativeStatCache[$normalized]);
506+
unset(self::$statCache[$normalized]);
502507

503508
self::unregister();
504509
$result = (bool) self::silent(fn () => mkdir($path, $mode, ($options & STREAM_MKDIR_RECURSIVE) !== 0));
@@ -510,7 +515,7 @@ public function mkdir(string $path, int $mode, int $options): bool
510515
public function rmdir(string $path, int $options): bool
511516
{
512517
$normalized = str_replace('\\', '/', $path);
513-
unset(self::$statCache[$normalized], self::$staticNegativeStatCache[$normalized]);
518+
unset(self::$statCache[$normalized]);
514519

515520
self::unregister();
516521
$result = (bool) self::silent(fn () => rmdir($path));
@@ -522,7 +527,7 @@ public function rmdir(string $path, int $options): bool
522527
public function unlink(string $path): bool
523528
{
524529
$normalized = str_replace('\\', '/', $path);
525-
unset(self::$statCache[$normalized], self::$staticNegativeStatCache[$normalized]);
530+
unset(self::$statCache[$normalized]);
526531

527532
self::unregister();
528533
$result = (bool) self::silent(fn () => unlink($path));
@@ -535,12 +540,7 @@ public function rename(string $pathFrom, string $pathTo): bool
535540
{
536541
$normFrom = str_replace('\\', '/', $pathFrom);
537542
$normTo = str_replace('\\', '/', $pathTo);
538-
unset(
539-
self::$statCache[$normFrom],
540-
self::$statCache[$normTo],
541-
self::$staticNegativeStatCache[$normFrom],
542-
self::$staticNegativeStatCache[$normTo]
543-
);
543+
unset(self::$statCache[$normFrom], self::$statCache[$normTo]);
544544

545545
self::unregister();
546546
$result = (bool) self::silent(fn () => rename($pathFrom, $pathTo));
@@ -549,26 +549,6 @@ public function rename(string $pathFrom, string $pathTo): bool
549549
return $result;
550550
}
551551

552-
/**
553-
* Executes a callback while temporarily suppressing PHP error and warning handlers.
554-
*
555-
* @template T
556-
*
557-
* @param callable(): T $callback
558-
*
559-
* @return T
560-
*/
561-
private static function silent(callable $callback): mixed
562-
{
563-
set_error_handler(fn () => true);
564-
565-
try {
566-
return $callback();
567-
} finally {
568-
restore_error_handler();
569-
}
570-
}
571-
572552
/**
573553
* Determines whether a target PHP file path should be intercepted with $O(1)$ caching.
574554
*/
@@ -715,4 +695,4 @@ private static function extractAndSeedFileMetadata(array $stmts, string $filePat
715695

716696
SpecialTypeResolver::seedFileMetadata($filePath, $namespace, $imports, $classTraitUseDocs);
717697
}
718-
}
698+
}

0 commit comments

Comments
 (0)