dg/bypass-finals
DG\BypassFinals lets you bypass PHP’s final classes and methods at runtime so you can mock, extend, or patch code that’s otherwise locked down—useful for testing legacy dependencies. Lightweight, Composer-ready, and works with popular test frameworks.
## Getting Started
1. **Installation**:
```bash
composer require --dev dg/bypass-finals
Add the package as a dev dependency to ensure it’s excluded from production.
Bootstrap Integration:
Register the loader in your test bootstrap file (e.g., tests/bootstrap.php or phpunit.xml):
<!-- phpunit.xml -->
<php>
<autoload>
<classmap prefix="BypassFinals"/>
</autoload>
</php>
<bootstrap>vendor/autoload.php</bootstrap>
Or in tests/bootstrap.php:
require __DIR__ . '/../vendor/autoload.php';
(new \BypassFinals\Loader())->register();
First Use Case:
Mock a final class in PHPUnit:
use PHPUnit\Framework\TestCase;
class FinalClassTest extends TestCase
{
public function testMockFinalClass()
{
$mock = $this->createMock(FinalClass::class);
$mock->method('finalMethod')->willReturn('mocked');
$this->assertEquals('mocked', $mock->finalMethod());
}
}
Now test methods/properties in final classes as if they weren’t final.
Restrict to Tests/CI Only:
Wrap registration in an environment check (e.g., tests/bootstrap.php):
if (app()->environment('testing') || PHP_SAPI === 'cli') {
(new \BypassFinals\Loader())->register();
}
Or use a CI-specific flag (e.g., env('RUNNING_TESTS')).
PHPUnit Configuration:
Add to phpunit.xml to auto-register:
<php>
<ini name="auto_prepend_file" value="vendor/dg/bypass-finals/bootstrap.php"/>
</php>
$loader = new \BypassFinals\Loader();
$loader->allowPaths([
__DIR__ . '/../vendor/symfony/http-foundation',
__DIR__ . '/../vendor/laravel/framework'
]);
$loader->denyPath(__DIR__ . '/../vendor/phpunit');
$loader->register();
createMock() or getMockBuilder() as usual for final classes.$mock = Mockery::mock(FinalClass::class)->makePartial();
$mock->shouldReceive('finalMethod')->andReturn('mocked');
use function Pest\Laravel\actingAs;
test('mock final class', function () {
$mock = Mockery::mock(FinalClass::class)->makePartial();
$mock->shouldReceive('finalMethod')->andReturn('mocked');
});
BypassFinals::debugInfo() to check if a class was bypassed:
if (BypassFinals::debugInfo(FinalClass::class)['bypassed']) {
echo "FinalClass was bypassed!";
}
spl_autoload_register(function ($class) {
if (str_contains($class, 'FinalClass')) {
error_log("Loading: $class");
}
});
$loader = new \BypassFinals\Loader();
$loader->setCacheDir(__DIR__ . '/../storage/bypass-finals-cache');
$loader->register();
Clear cache between major PHP version updates.Accidental Production Use:
final classes rely on their immutability.bypass-finals is loaded outside testing):
if (!app()->environment('testing') && PHP_SAPI !== 'cli') {
throw new \RuntimeException('BypassFinals loaded in non-test environment!');
}
Opcache Conflicts:
# php.ini (test environment)
opcache.enable=0
PHP 8.2+ readonly Quirks:
readonly properties are bypassed only in PHP 8.1+. In PHP 8.2+, the package may not handle them correctly.readonly bypass if needed:
BypassFinals::enable(bypassReadOnly: false);
Check Bypass Status:
Use BypassFinals::debugInfo() to verify if a class/method was bypassed:
var_dump(BypassFinals::debugInfo(FinalClass::class));
Output:
array(
'bypassed' => true,
'original' => 'final class FinalClass { ... }',
'rewritten' => 'class FinalClass { ... }',
)
Trace Autoloading: If mocks fail, trace which loader is active:
spl_autoload_register(function ($class) {
if (str_contains($class, 'Final')) {
error_log("Autoloading: $class via " . get_class(Loader::getInstance()));
}
});
php -d memory_limit=-1 vendor/bin/phpunit
Service Container Conflicts:
final classes are Laravel services (e.g., Illuminate\Foundation\Application), bypass may interfere with container resolution.Pest Integration:
pest.php:
uses(Tests\TestCase::class)->in('Feature');
beforeAll(function () {
if (app()->environment('testing')) {
(new \BypassFinals\Loader())->register();
}
});
BypassFinals\Rewriter:
class CustomRewriter extends \BypassFinals\Rewriter
{
protected function shouldBypassMethod($methodName)
{
return parent::shouldBypassMethod($methodName) && $methodName !== 'protectedMethod';
}
}
Register it:
$loader = new \BypassFinals\Loader();
$loader->setRewriter(new CustomRewriter());
$loader->register();
Warning: Internal API is unstable; prefer configuration over extension.composer.json script to block production installs:
"scripts": {
"post-autoload-dump": "if [ \"$(php -r 'echo php_sapi_name();')\" != \"cli\" ]; then echo \"ERROR: bypass-finals loaded in non-CLI environment!\" >&2; exit 1; fi"
}
if (!app()->runningUnitTests() && !app()->runningInConsole()) {
throw new \RuntimeException('BypassFinals must only run in tests!');
}
final bypass using eval() or opcache_compile_file().How can I help you explore Laravel packages today?