@@ -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