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

Product Decisions This Supports

  • Test strategy for vendor and legacy code. The most concrete decision this enables: whether you can test around a 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.
  • Build vs. buy vs. workaround. Three options exist for an untestable 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.
  • Refactor-now vs. refactor-later. For your own domain classes marked 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.
  • Technical-debt reduction. Retires bespoke workarounds teams accumulate — reflection hacks, 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.
  • Roadmap unblocking for integrations. New vendor SDK evaluations often stall on "can we actually test this?" Being able to answer yes without forking lowers the friction of adding integrations and shortens the time from "approved dependency" to "first tested integration."
  • Reversibility profile makes it a good spike. One dev-only Composer dependency, one line in the test bootstrap. If it doesn't earn its place, it comes out in an afternoon. That makes it a low-ceremony trial rather than an architecture commitment.
  • Concrete use cases worth naming in a ticket: mocking a 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.

When to Consider This Package

Adopt when:

  • You have a specific, named final class that is blocking a test you actually want to write. Adoption without a concrete target is speculative tooling.
  • The class is third-party, vendored, or legacy — i.e., you cannot reasonably change its declaration.
  • Your stack is PHPUnit, Pest, or Mockery and you already control the test bootstrap.
  • You can enforce test-only scope mechanically: 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.
  • The team accepts a stream-wrapper / on-the-fly source-rewriting approach. It works, but it's an unusual mechanism and someone must own understanding it.

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:

  • The class is yours. Removing final or extracting an interface is the correct fix; a test-time bytecode rewrite of your own code hides a design decision from reviewers.
  • There is any path to production loading. Hard stop. This must never be resolvable in a production build. If your deployment or packaging process can't guarantee that, don't adopt.
  • A single port/adapter would solve it. If exactly one or two vendor classes are in play, wrapping them behind your own interface is more work up front and dramatically less weird forever. This package is most justified when the final surface is broad and diffuse.
  • The license can't be cleared. Tooling reports 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.
  • The environment fights you. Known friction with OPcache and other stream wrappers. If you can't disable OPcache in the test environment, or you already have a wrapper in the chain, expect debugging time.
  • Maintenance signals look thin. 580 stars and a recent, well-documented release history (v1.4 caching, v1.5 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.

How to Pitch It (Stakeholders)

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:

  • Integration is trivial. 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.
  • It's fast enough. Source rewriting is cached (since v1.4), so the overhead is a one-time pass plus a small constant in CI. Not free, but not a reason to avoid it.
  • Scope it deliberately. Use 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."
  • Guardrails are non-negotiable. Dev dependency only; environment check; CI assertion that the package is not loaded outside tests. Accidental production use is the one failure mode that matters, and it's preventable with a two-line check.
  • Expect specific friction. Disable OPcache in the test environment to avoid stale bytecode; watch memory on very large suites; if a mock behaves oddly, the first thing to check is whether the bypass registered before the class was first loaded.
  • It's a testing tool, not an architecture. Treat every use as a small, documented exception with a note on why refactoring wasn't the right call. If that list grows, that's a signal about our dependency choices, not a reason to expand the allow list.

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.

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