diff --git a/readme.md b/readme.md index c99e7b0..a0eda2d 100644 --- a/readme.md +++ b/readme.md @@ -57,7 +57,7 @@ DG\BypassFinals::allowPaths([ ]); ``` -Or, conversely, you can specify which paths not to search using `DG\BypassFinals::denyPaths()`. +Or, conversely, you can specify which paths not to search using `DG\BypassFinals::denyPaths()`. This gives you finer control and can solve issues with certain frameworks and libraries. Enhance performance by caching transformed files. Make sure the cache directory already exists: @@ -76,6 +76,26 @@ For integration with PHPUnit 10 or newer, simply add BypassFinals as an extensio   +Troubleshooting +--------------- + +If you encounter issues with BypassFinals not working as expected, you can use the `debugInfo()` method to gain insights into its internal state. Calling this method will output valuable information to help diagnose the problem: + +```php +DG\BypassFinals::debugInfo(); +``` + +This will display: + +- Configuration: Whether BypassFinals is enabled for removing `final` and/or `readonly` keywords. +- BypassFinals startup call stack: The sequence of function calls that led to `DG\BypassFinals::enable()`, helping you verify when and where BypassFinals was started. +- Classes loaded before BypassFinals startup: A list of classes that were already loaded in PHP before BypassFinals was started. Keywords in these classes cannot be removed, as the classes are already defined. This can help identify potential conflicts or reasons why certain classes aren't being modified. +- Modified files: A list of the files that BypassFinals has successfully modified. If the file containing a class you expect to be modified isn't in this list, it suggests a problem with path matching or the timing of the BypassFinals startup. + +By examining this output, you can better understand how BypassFinals is configured and whether it's operating on the intended files and classes. This can significantly speed up the process of identifying and resolving issues. + +  + Do you like this project? --------- diff --git a/src/BypassFinals.php b/src/BypassFinals.php index 3ddbdf5..2151ccf 100644 --- a/src/BypassFinals.php +++ b/src/BypassFinals.php @@ -22,12 +22,28 @@ final class BypassFinals /** @var array Tokens that represent 'readonly' and 'final' keywords */ private static $tokens = []; + /** @var array Call stack when enable() was called */ + private static $enableCallStack = []; + + /** @var array List of userland classes loaded before enable() was called */ + private static $classesLoadedBeforeEnable = []; + + /** @var array List of files that were modified */ + private static $modifiedFiles = []; + /** * Enables modification of the source code to bypass 'readonly' and 'final' restrictions. */ public static function enable(bool $bypassReadOnly = true, bool $bypassFinal = true): void { + if (!self::$enableCallStack) { + self::$enableCallStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + self::$classesLoadedBeforeEnable = array_filter(get_declared_classes(), function (string $class): bool { + return !(new \ReflectionClass($class))->isInternal() && $class !== self::class; + }); + } + if ($bypassReadOnly && PHP_VERSION_ID >= 80100) { self::$tokens[T_READONLY] = 'readonly'; } @@ -93,13 +109,18 @@ public static function setCacheDirectory(?string $dir): void * Modifies the PHP code by removing specified tokens if they exist. * @internal */ - public static function modifyCode(string $code): string + public static function modifyCode(string $code, ?string $file = null): string { foreach (self::$tokens as $text) { if (stripos($code, $text) !== false) { - return self::$cacheDir + $modifiedCode = self::$cacheDir ? self::removeTokensCached($code) : self::removeTokens($code); + if ($modifiedCode !== $code) { + self::$modifiedFiles[] = $file; + return $modifiedCode; + } + return $code; } } @@ -175,4 +196,50 @@ public static function isPathAllowed(string $path): bool return false; } + + + /** + * Returns debugging information to help diagnose issues. + */ + public static function debugInfo(): void + { + echo "\n"; + echo "BypassFinals Debug Information\n"; + echo "------------------------------\n\n"; + echo "Configuration:\n"; + echo " Bypass 'final': " . (PHP_VERSION_ID >= 80100 && isset(self::$tokens[T_READONLY]) ? 'enabled' : 'disabled') . "\n"; + echo " Bypass 'readonly': " . (isset(self::$tokens[T_FINAL]) ? 'enabled' : 'disabled') . "\n"; + + echo "\nFrom where BypassFinals::enable() was started:\n"; + foreach (self::$enableCallStack as $index => $frame) { + echo " #$index "; + if (isset($frame['class'])) { + echo $frame['class'] . $frame['type'] . $frame['function'] . '()'; + } elseif (isset($frame['function'])) { + echo $frame['function'] . '()'; + } + if (isset($frame['file'])) { + echo ' in ' . $frame['file'] . ':' . $frame['line']; + } + echo "\n"; + } + + echo "\nClasses already loaded before BypassFinals was started:\n"; + if (self::$classesLoadedBeforeEnable) { + foreach (self::$classesLoadedBeforeEnable as $class) { + echo " - $class\n"; + } + } else { + echo " no classes\n"; + } + + echo "\nFiles where BypassFinals removed final/readonly:\n"; + if (self::$modifiedFiles) { + foreach (self::$modifiedFiles as $file) { + echo " - $file\n"; + } + } else { + echo " no files were modified\n"; + } + } } diff --git a/src/MutatingWrapper.php b/src/MutatingWrapper.php index 3b17544..52f660b 100644 --- a/src/MutatingWrapper.php +++ b/src/MutatingWrapper.php @@ -43,7 +43,7 @@ public function stream_open(string $path, string $mode, int $options, ?string &$ $content .= $this->wrapper->stream_read(8192); } - $modified = BypassFinals::modifyCode($content); + $modified = BypassFinals::modifyCode($content, $path); if ($modified === $content) { $this->wrapper->stream_seek(0); } else {