bypass-finals: making final and readonly PHP classes mockable at test time
Removes `final` and `readonly` keywords from source code on-the-fly and allows mocking of final methods and classes. It can be used together with any test tool such as PHPUnit or Mockery.
At a glance
- What is it?
- bypass-finals is a Composer library that strips final and readonly from PHP source on the fly by rewriting files as they are loaded, so PHPUnit, Mockery and Nette Tester can mock classes that were meant to be sealed. It suits a team that wants final removed from a specific package, and it is a code rewriter, not a reflection trick.
- Who is it for?
- bypass-finals fits a PHP team that has to test against a third party package whose classes are final, because the alternative is wrapping every collaborator in an interface by hand, and the library gives a targeted allow or deny list so you only touch the code you need.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 11 days ago.
- What is it written in?
- Mainly PHP, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
It rewrites the source as it loads, which is the whole trick
bypass-finals strips the final and readonly keywords from your PHP code on the fly, and the mechanism is what defines the tool. The library installs a stream wrapper, so files are transformed on their way into PHP rather than being patched on disk or bypassed through reflection at runtime. The stated purpose is to make mocking of final methods and classes possible, and it integrates with testing frameworks including PHPUnit, Mockery and Nette Tester. The important consequence is scope. A class that has already been defined cannot have its final keyword removed, so the library can only act on files loaded after it is switched on, and every document about it returns to that point. The alternative designs, which use reflection or the so-called uopz extension, either fail on internal classes or require a non standard extension, whereas this approach is a plain Composer package. It also means the transformation is textual, so what the interpreter sees is a source file with keywords removed, not a class with modified flags.
Enabling it is one call, and the timing is the whole game
Installation is a single Composer command in your project directory.
composer require dg/bypass-finals --devAnd enabling it is one static call.
DG\BypassFinals::enable();The documentation is emphatic about when to call it, telling you to do it early, preferably immediately after your vendor/autoload.php is loaded, so that all classes are processed before they are used. The final keyword is removed from classes and methods, and also from properties and from the property hooks introduced in PHP 8.4. One exclusion is stated plainly: final internal PHP classes such as Closure are not mockable, which is unsurprising given that the approach works on source text. If you want to keep readonly intact, the same call takes a named parameter.
DG\BypassFinals::enable(bypassReadOnly: false);allowPaths and denyPaths keep the blast radius small
The scoping controls are the part you should use first, because a library that rewrites every loaded file is a much larger idea than one that rewrites your own source. There is a whitelist method for specifying directories or files to allow.
DG\BypassFinals::allowPaths([
'*/Nette/*',
]);And a denyPaths method for the inverse, specifying which paths not to search. The documentation says this gives finer control and can solve issues with certain frameworks and libraries, which is the practical reason to reach for it: a package whose classes you want to mock is often loaded by a framework that also loads classes you do not want touched. The path masks accept multiple entries separated by semicolons when configured through the PHPUnit XML, and the same method names apply there. Note the ordering implication: if the same file is matched by both lists, the outcome depends on how the two are combined internally, and the documentation does not spell that out, so the safest practice is to keep the two lists disjoint.
Caching transformed files, and the security warning that comes with it
For repeated test runs the library can cache the transformed files, and the directory is created automatically.
DG\BypassFinals::setCacheDirectory(__DIR__ . '/cache');The security note attached to that option is the sharpest warning in the readme. Since the cached files are executed as PHP code, you must always use a private, trusted directory and never a shared world-writable location such as /tmp on a multi-user machine, where another local user could inject code into the cache. That is a real concern rather than a theoretical one, because a cache populated by a privileged test run and then writable by an unprivileged user is a straightforward privilege escalation path. It is also why the cache option is worth leaving off entirely on a developer laptop that only runs a small suite, since the transformation cost is small compared with the risk of getting the directory permissions wrong.
The PHPUnit extension, and the bootstrap order that fixes stubborn classes
For PHPUnit 10 or newer the library registers as an extension in the PHPUnit XML configuration file.
<extensions>
<bootstrap class="DG\BypassFinals\PHPUnitExtension"/>
</extensions>And the extension takes parameters, so the whole configuration can live in the XML rather than in code.
<extensions>
<bootstrap class="DG\BypassFinals\PHPUnitExtension">
<parameter name="bypassFinal" value="true"/>
<parameter name="bypassReadOnly" value="false"/>
<parameter name="cacheDirectory" value="./cache"/>
<parameter name="allowPaths" value="*/src/*;*/lib/*"/>
<parameter name="denyPaths" value="*/generated/*"/>
</bootstrap>
</extensions>The troubleshooting section explains the failure mode that catches everyone. BypassFinals can only strip the final keyword from files loaded after enable is called, and a class loaded before that, for example via a files entry in a package's autoload configuration which Composer loads during the autoload require, will remain final. The fix is to include the library's own bootstrap file before the Composer autoloader in your test bootstrap.
// tests/bootstrap.php
require __DIR__ . '/../vendor/dg/bypass-finals/src/bootstrap.php';
require __DIR__ . '/../vendor/autoload.php';That activates the stream wrapper early enough to intercept every file loaded during autoloading, which is the difference between a class you can mock and one you cannot.
debugInfo tells you why a class is still final
When something does not work, the library offers a diagnostic call rather than leaving you guessing.
DG\BypassFinals::debugInfo();The output is described as four things. First, the configuration, whether the library is enabled for removing final and or readonly keywords. Second, the BypassFinals startup call stack, the sequence of function calls that led to enable being called, which helps you verify when and where it was started. Third, a list of classes that were already loaded in PHP before the startup, with the note that keywords in those classes cannot be removed because the classes are already defined, which identifies conflicts and explains why certain classes are not being modified. Fourth, a list of the files that were successfully modified, and if the file containing a class you expect to be modified is not in that list, the cause is either path matching or the timing of the startup. That combination covers both of the ways this tool fails, which makes it a genuinely useful debugging aid rather than a formality.
Editorial conclusion
bypass-finals fits a PHP team that has to test against a third party package whose classes are final, because the alternative is wrapping every collaborator in an interface by hand, and the library gives a targeted allow or deny list so you only touch the code you need. It does not fit a project where the sealed design is deliberate and the answer is to design for injection instead, and it does not fit a codebase with a public cache directory, because the cached files are executed as PHP and a world writable location lets another local user inject code. Before you enable it, decide which paths you allow, put the cache somewhere private and trusted, and if a class is still final, read the bootstrap ordering in the troubleshooting notes, because a class loaded before enable is called cannot be rewritten. On the licence, the readme badge names New BSD while the repository listing records a custom licence GitHub cannot classify, so read the license.md file at the top of the repository before you copy the approach into a codebase of your own.
Frequently asked questions
What does bypass-finals do?
It removes the final and readonly keywords from your PHP code on the fly, so that final methods and classes can be mocked. The transformation happens as files are loaded, and it integrates with testing frameworks such as PHPUnit, Mockery and Nette Tester.
How do I install and enable bypass-finals?
Run composer require dg/bypass-finals --dev in your project, then call DG\BypassFinals::enable() as early as possible, preferably right after vendor/autoload.php is loaded, so all classes are processed before they are used.
Why is a class still final after I enable bypass-finals?
Because the library can only strip the keyword from files loaded after enable is called. A class loaded earlier, for example through a files entry in a package's autoload configuration, stays final, and the fix is to require the library's src/bootstrap.php before vendor/autoload.php in your test bootstrap file.
Can I limit which files bypass-finals touches?
Yes. DG\BypassFinals::allowPaths() takes a whitelist of directories or files, and DG\BypassFinals::denyPaths() excludes paths instead. In the PHPUnit XML configuration the same two settings are parameters, and the masks accept multiple entries separated by semicolons.
Is the bypass-finals cache directory safe to share?
No. The cached files are executed as PHP code, so the documentation says to always use a private, trusted directory and never a shared world-writable location such as /tmp on a multi-user machine, where another local user could inject code into the cache.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/dg-bypass-finals)Was this useful?