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.
final class you don't own. If a third-party SDK, a legacy service, or an inherited model is final and therefore unmockable, this package converts "we can't cover that path" into "we can cover that path." That directly changes coverage targets and quality gates in CI.final dependency: (1) fork or patch the vendor code, (2) build an adapter/interface layer purely for testability, (3) adopt this dev dependency. This package is the cheapest of the three, but only option 2 is an architecture improvement. The real decision is per-case: use this for external code, refactor internal code.final, this defers a design change while keeping test coverage. Useful when a class is slated for extraction anyway and you don't want a blocking refactor in the current sprint.class_alias tricks, container overrides, wrapper classes that exist only to be mocked. Each of those is undocumented institutional knowledge; replacing them with one declared dev dependency is a net maintainability win.final third-party HTTP client to test retry/timeout branches; testing error handling around final value objects or readonly DTOs (PHP 8.1+); isolating a final class in a legacy monolith so its callers become testable.Adopt when:
final class that is blocking a test you actually want to write. Adoption without a concrete target is speculative tooling.require-dev in composer.json, a phpunit.xml bootstrap entry, an environment guard, and ideally a CI check that fails if the package is loaded outside the testing context.Use the scoping features, don't run wide open. Since v1.8.0 the package exposes allowPaths() / denyPath(), and since v1.6.0 it ships a configurable PHPUnit extension that already denies vendor/phpunit. Configure an allow list pointing only at the vendor namespaces you need. Wide-open rewriting is the primary source of surprise.
Look elsewhere when:
final or extracting an interface is the correct fix; a test-time bytecode rewrite of your own code hides a design decision from reviewers.final surface is broad and diffuse.NOASSERTION, which means automated compliance checks will flag it. The upstream project is permissively licensed, but someone in legal must confirm it in writing before this enters the dependency graph. Don't let a green CI pipeline implicitly make that decision.readonly support, v1.6 PHPUnit extension, v1.8 path filtering, v1.9 configurable extension) is genuinely encouraging. But "0 dependents" in the metadata is a signal worth sanity-checking rather than dismissing, and the package's internals carry no backward-compatibility guarantee — if you extend BypassFinals\Rewriter, you're on your own.One risk that belongs in the decision record: bypassed final semantics can produce false confidence. A mock of a class that was designed to be non-extensible may not behave like the real thing. A test that passes here is weaker evidence than a test that passes against a genuinely mockable seam. Use it on the periphery of your system, not as a substitute for well-placed seams in your core.
For executives — the one-paragraph version:
"We have a recurring testing gap: some third-party and legacy PHP classes are declared final, which technically prevents us from mocking them, so those integration paths go untested. This is a small, free, development-only package that removes that blocker at test time. It has no presence in production, it's a single line of configuration, and it's fully reversible. The ask is to approve it as a require-dev dependency with a test-only guard in CI. The expected return is coverage on paths we currently can't test, and removal of several custom test workarounds that only one or two engineers understand."
For engineering — what they actually need to know:
composer require --dev dg/bypass-finals, register the bypass in the test bootstrap before autoloading begins, and createMock() works on final classes like any other. No changes to existing tests.allowPaths() to target only the vendor namespaces that need it, and rely on the bundled PHPUnit extension's default denies. Don't enable globally "just in case."Success measures to watch after adoption: number of previously-untestable paths now covered, count of bespoke test workarounds deleted, time-to-first-test for new vendor integrations, and — the important counter-metric — any case where a test passed under bypass but the corresponding production behavior differed.
How can I help you explore Laravel packages today?