|
| 1 | +# To My Agents! |
| 2 | + |
| 3 | +It is my fervent wish that this file guide every AI coding agent working with code in this repository. |
| 4 | + |
| 5 | +## Project Overview |
| 6 | + |
| 7 | +BypassFinals is a PHP library that removes `final` and `readonly` keywords from source code on-the-fly, enabling mocking of final classes and methods for testing purposes. It works by intercepting PHP's file stream operations and modifying code before it's parsed. |
| 8 | + |
| 9 | +## Key Commands |
| 10 | + |
| 11 | +### Testing |
| 12 | +```bash |
| 13 | +# Run all tests (composer script runs `tester tests -s`) |
| 14 | +composer run tester |
| 15 | +# or |
| 16 | +vendor/bin/tester tests -s |
| 17 | + |
| 18 | +# Run specific test file |
| 19 | +vendor/bin/tester tests/BypassFinals/BypassFinals.phpt -s |
| 20 | +``` |
| 21 | + |
| 22 | +### Static Analysis |
| 23 | +```bash |
| 24 | +# Run PHPStan static analysis |
| 25 | +composer run phpstan |
| 26 | +# or |
| 27 | +vendor/bin/phpstan analyse |
| 28 | +``` |
| 29 | + |
| 30 | +### Development Setup |
| 31 | +```bash |
| 32 | +# Install dependencies |
| 33 | +composer install --dev |
| 34 | + |
| 35 | +# Run tests in different PHP versions (see .github/workflows/tests.yml for supported versions) |
| 36 | +# Supports PHP 8.2 through 8.5 |
| 37 | +``` |
| 38 | + |
| 39 | +## Architecture Overview |
| 40 | + |
| 41 | +### Core Components |
| 42 | + |
| 43 | +1. **BypassFinals** (`src/BypassFinals.php`) - Main API class that: |
| 44 | + - Manages configuration (allow/deny paths, cache directory) |
| 45 | + - Registers custom stream wrapper |
| 46 | + - Provides token removal logic |
| 47 | + - Tracks debugging information |
| 48 | + |
| 49 | +2. **MutatingWrapper** (`src/MutatingWrapper.php`) - Stream wrapper that: |
| 50 | + - Intercepts file operations for `.php` files |
| 51 | + - Modifies PHP source code on-the-fly if path is allowed |
| 52 | + - Delegates non-PHP operations to underlying wrapper |
| 53 | + |
| 54 | +3. **NativeWrapper** (`src/NativeWrapper.php`) - Native file operations wrapper that: |
| 55 | + - Implements all PHP stream wrapper methods |
| 56 | + - Temporarily restores native protocol for actual file operations |
| 57 | + - Handles file, directory, and metadata operations |
| 58 | + |
| 59 | +4. **StreamWrapper** (`src/StreamWrapper.php`) - `@internal` interface that: |
| 60 | + - Defines the prototype of the underlying wrapper used by `MutatingWrapper` |
| 61 | + - Mirrors PHP's documentation-only `streamWrapper` prototype, used for type-checking |
| 62 | + |
| 63 | +5. **PHPUnitExtension** (`src/PHPUnitExtension.php`) - PHPUnit 10+ integration that: |
| 64 | + - Automatically denies the PHPUnit vendor path (`*/vendor/phpunit/*`) |
| 65 | + - Configures BypassFinals from phpunit.xml parameters (`bypassReadOnly`, `bypassFinal`, `cacheDirectory`) |
| 66 | + - Enables the library during test bootstrap |
| 67 | + |
| 68 | +6. **bootstrap.php** (`src/bootstrap.php`) - Standalone bootstrap that requires the |
| 69 | + wrapper classes and calls `enable()`. Designed to be loaded *before* |
| 70 | + `vendor/autoload.php` so classes registered in autoload files are also processed. |
| 71 | + |
| 72 | +### How It Works |
| 73 | + |
| 74 | +1. **Stream Wrapper Registration**: Replaces PHP's native `file://` protocol handler with `MutatingWrapper` |
| 75 | +2. **File Interception**: When PHP loads a `.php` file, `MutatingWrapper` intercepts the operation |
| 76 | +3. **Code Modification**: Uses PHP's tokenizer to remove `final` and `readonly` tokens from source |
| 77 | +4. **Caching**: Optional filesystem caching of modified code using SHA1 hashes |
| 78 | +5. **Path Filtering**: Allow/deny path rules control which files get modified |
| 79 | + |
| 80 | +### Testing Strategy |
| 81 | + |
| 82 | +- **Nette Tester**: Uses `.phpt` files for comprehensive test coverage |
| 83 | +- **Fixtures**: Test files in `tests/BypassFinals/fixtures/` demonstrate actual `final` class removal |
| 84 | +- **Stream Operations**: Tests verify that custom stream wrapper doesn't break normal file operations |
| 85 | +- **Path Filtering**: Tests ensure allow/deny path rules work correctly |
| 86 | +- **Caching**: Tests verify cache functionality with hash-based storage |
| 87 | +- **Edge Cases**: Tests handle syntax errors, missing files, and other error conditions |
| 88 | + |
| 89 | +### Configuration Patterns |
| 90 | + |
| 91 | +The library supports multiple configuration approaches: |
| 92 | + |
| 93 | +1. **Direct PHP API** (`enable()` defaults: both `$bypassReadOnly` and `$bypassFinal` are `true`): |
| 94 | + ```php |
| 95 | + DG\BypassFinals::enable(); |
| 96 | + DG\BypassFinals::allowPaths(['*/src/*']); |
| 97 | + DG\BypassFinals::setCacheDirectory('/tmp/cache'); |
| 98 | + ``` |
| 99 | + |
| 100 | +2. **PHPUnit XML Extension** (parameters are optional; omitted boolean params default to `true`): |
| 101 | + ```xml |
| 102 | + <extensions> |
| 103 | + <bootstrap class="DG\BypassFinals\PHPUnitExtension"> |
| 104 | + <parameter name="bypassFinal" value="true"/> |
| 105 | + <parameter name="bypassReadOnly" value="false"/> |
| 106 | + <parameter name="cacheDirectory" value="./cache"/> |
| 107 | + </bootstrap> |
| 108 | + </extensions> |
| 109 | + ``` |
| 110 | + |
| 111 | +3. **Standalone bootstrap** (load before `vendor/autoload.php` to also process autoload-registered classes): |
| 112 | + ```php |
| 113 | + // tests/bootstrap.php |
| 114 | + require __DIR__ . '/../vendor/dg/bypass-finals/src/bootstrap.php'; |
| 115 | + require __DIR__ . '/../vendor/autoload.php'; |
| 116 | + ``` |
| 117 | + |
| 118 | +### Performance Considerations |
| 119 | + |
| 120 | +- **Early Initialization**: Must be enabled before classes are loaded |
| 121 | +- **Caching**: Use `setCacheDirectory()` to avoid repeated tokenization |
| 122 | +- **Path Filtering**: Use allow/deny paths to limit scope and improve performance |
| 123 | +- **Stream Wrapper Overhead**: Minimal overhead for non-PHP files due to delegation |
| 124 | + |
| 125 | +### Debugging |
| 126 | + |
| 127 | +Use `DG\BypassFinals::debugInfo()` to troubleshoot issues. This outputs: |
| 128 | +- Configuration status (final/readonly bypass enabled) |
| 129 | +- Call stack showing where `enable()` was called |
| 130 | +- Classes loaded before BypassFinals was started (these cannot be modified) |
| 131 | +- List of files that were successfully modified |
| 132 | + |
| 133 | +### Compatibility |
| 134 | + |
| 135 | +- **PHP Versions**: 8.2 through 8.5 (minimum is now `>=8.2`, per `composer.json`) |
| 136 | +- **readonly Support**: enabled by default (the 8.2 minimum guarantees the `T_READONLY` token exists) |
| 137 | +- **Testing Frameworks**: PHPUnit, Mockery, Nette Tester |
| 138 | +- **Internal Classes**: Cannot modify PHP internal classes like `Closure` |
0 commit comments