Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Bypass Finals Laravel Package

dg/bypass-finals

dg/bypass-finals is a PHP library that removes the final keyword at runtime so you can mock final classes and methods in tests. It works with PHPUnit and other mocking tools, making legacy or third-party code easier to test without changing source.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install as a dev dependency

composer require --dev dg/bypass-finals

It must never reach production. Keep it in require-dev and deploy with composer install --no-dev; that alone is your first safety net.

2. Enable it once, early

There are two supported entry points. Pick one, not both.

Option A — PHPUnit extension (recommended, v1.6+):

<!-- phpunit.xml -->
<phpunit bootstrap="vendor/autoload.php" ...>
    <extensions>
        <bootstrap class="DG\BypassFinals\PHPUnitExtension"/>
    </extensions>
</phpunit>

On PHPUnit 9, the element is still <extension class="DG\BypassFinals\PHPUnitExtension"/>. The extension self-registers before test classes are loaded and already denies vendor/phpunit (v1.8+) so PHPUnit's own internals are never rewritten.

Option B — custom bootstrap file:

<?php // tests/bootstrap.php
require __DIR__ . '/../vendor/autoload.php';

DG\BypassFinals::enable();

Then set bootstrap="tests/bootstrap.php" in phpunit.xml. This is the route to use with Pest, since tests/Pest.php runs too late — the stream wrapper must be installed before the first include of a final class.

3. First use case

Mock something you previously couldn't:

$gateway = Mockery::mock(PaymentSdk\Client::class); // final class
$this->app->instance(PaymentSdk\Client::class, $gateway);

$gateway->shouldReceive('charge')->once()->andReturn(new ChargeResult(true));

$this->post('/checkout')->assertOk();

If that mock now constructs instead of throwing Class ... is declared final and cannot be mocked, you're done. For a quick health check, DG\BypassFinals::debugInfo() returns the current wrapper/whitelist/cache state — dump it in a failing test.

Implementation Patterns

Narrow the surface with a whitelist. Day-to-day, don't bypass your whole codebase — you want app code to stay honest. Restrict to the vendor packages you actually need to mock:

DG\BypassFinals::enable();
DG\BypassFinals::allowPaths([
    __DIR__ . '/../vendor/acme/payments-sdk/*',
]);
DG\BypassFinals::denyPath(__DIR__ . '/../vendor/phpunit');

Smaller scope = less rewriting, faster tests, and final on your own classes keeps its design intent.

Mock a final collaborator behind a container binding. The bypass is about class generation; the wiring is still plain Laravel:

$this->mock(FinalReportGenerator::class, function ($mock) {
    $mock->shouldReceive('generate')->andReturn('stub');
});

Partial mocks for final methods. A single final method on an otherwise mockable class is the most common real-world case:

$mock = Mockery::mock(FinalClass::class)->makePartial();
$mock->shouldReceive('finalMethod')->andReturn('faked');

Keep a "with bypass" and "without bypass" split. Give the fast unit suite its own config with no extension, and use bypass only in the integration suite that touches vendor SDKs. That keeps accidental reliance on the rewrite out of your core suite, and gives you a canary when a package stops being final.

Pest:

// tests/Pest.php is too late — use the bootstrap/extension instead.
// Then in a test:
$mock = Mockery::mock(FinalJob::class);

Warm the cache in CI. Rewrites are cached on disk (v1.4+, keyed on settings since v1.7). Point the cache at a CI-cached directory so parallel jobs don't each pay the parse cost:

DG\BypassFinals::setCacheDirectory(__DIR__ . '/../storage/framework/testing/bypass-finals');

Guard against production exposure. Add a cheap assertion in your deploy/CI script, e.g. fail if dg/bypass-finals appears in the production autoloader, or wrap enable() in if (getenv('APP_ENV') === 'testing').

Gotchas and Tips

  • Load order is everything. If a class was already included (by a preload script, an early service provider, or a require in artisan), the wrapper never sees it and the "declared final" error returns. Symptom: bypass works for one class but not another. Fix: enable earlier, or check class_exists(Foo::class, false) before enabling.
  • Turn off OPcache in CLI tests. opcache.enable_cli=1 can serve bytecode compiled from the original file, bypassing the stream wrapper's rewritten content. Set opcache.enable_cli=0 (and disable JIT) in your test environment.
  • readonly is a semantic change, not just a keyword. Bypassing it lets tests write to properties that production code can't. If you only need final removed, call DG\BypassFinals::enable(bypassReadOnly: false); — this is the safer default for new adopters.
  • Don't rewrite PHPUnit. The extension denies vendor/phpunit for a reason. If you use the manual bootstrap, add that deny yourself.
  • Stale cache after upgrades or config changes. Settings are hashed into the cache key, but if behaviour gets weird after a Composer update, delete the cache directory first — that's the fastest debug step.
  • Other stream wrappers and non-file:// loads. The package chains to a previously registered file:// wrapper (v1.4+), so tools like vfsStream or PHP-VCR generally coexist — but files loaded from a phar:// or a custom wrapper are untouched. Same for any code that compares realpath() results.
  • Stream-wrapper edge cases — file_exists, is_file, and directory opens have had bugs fixed over time (v1.7). Pin a recent minor version; open on a directory used to fail.
  • Mocking a final class does not guarantee identical production behaviour. You're testing against a rewritten class. For anything where final implies a contract (e.g. an SDK enforcing immutability), keep one integration test against the unmodified class.
  • Mockery aliases/overloads (Mockery::mock('alias:Foo')) define a new class, so they don't need the bypass — but they conflict with a real class already being loaded. Prefer the bypass + normal class mock in Laravel tests.
  • When it fails silently, DG\BypassFinals::debugInfo() (v1.9+) plus phpunit --debug will tell you whether the wrapper is active and which paths matched. Print the whitelist — nine times out of ten the glob is wrong.
  • Treat it as an escape hatch with a TODO. If you find yourself bypassing your own final classes, that's a design smell; file the refactor (extract an interface, drop final) and remove the entry from allowPaths() when it lands.
  • Verify version-specific features. PHP 8.1 final const and PHP 8.2 readonly classes are newer surface area than the core final class/final method handling — write one throwaway test per construct you depend on rather than assuming coverage.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
sanjid29/laravel-starter-core
vanda/core
mimon/kafka-logger
webup/laravel-helium
hadiabedzadeh/laravel-journey-builder
shadowbane/laravel-gelf-logger
devloopsnet/bagisto-mastercard
sunxyw/laravel-admin
voxyfy/anadolupay
emild/laravel-metable
timedoor/baskito
larva/tencentcloud-sdk-php
jund-fauz/filament-kompact
atannex/panels
sahanranasingha/sri-lanka-holidays
alan2k2/cna-framework
moztopia/access
itorgov/laravel-forge-bot
backstage/laravel-ok
mr-swapan/applinks