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

Phpunit Bridge Laravel Package

symfony/phpunit-bridge

PHPUnit Bridge adds utilities on top of PHPUnit, with a focus on managing deprecation notices in your test suite. It helps you track, report, and fix deprecations so your code stays compatible with future Symfony and PHP versions.

View on GitHub
Deep Wiki
Context7

Getting Started

symfony/phpunit-bridge is a dev-only package. In a Laravel app it won't replace phpunit/phpunit (Laravel pins that), but it sits alongside it to give you three things Laravel doesn't ship: structured deprecation accounting, deprecation assertion helpers, and ClockMock/DnsMock for non-deterministic code.

composer require --dev symfony/phpunit-bridge

Pick the bridge major that matches your PHP runtime: ^5.4 for PHP 7.x, ^6.4/^7.x for PHP 8.1–8.3, ^8.x for PHP 8.4+. Mixing an 8.x bridge into an 8.1 app fails at install time, not at test time.

Where to look first: phpunit.xml / phpunit.xml.dist. On PHPUnit 10+ the bridge attaches itself as a PHPUnit extension, not via a bootstrap file:

<phpunit>
    <extensions>
        <bootstrap class="Symfony\Bridge\PhpUnit\SymfonyExtension"/>
    </extensions>
</phpunit>

Then run your normal command (php artisan test or vendor/bin/phpunit) and read the new footer:

Remaining direct deprecations  12
Remaining indirect deprecations 340
Remaining self deprecations     3

First use case: before a Laravel major upgrade (11 → 12) or a PHP bump, this footer tells you which deprecations you wrote (self), which come from packages you call directly (direct), and which are buried transitively (indirect). You fix self, upgrade or patch to clear direct, and set a threshold for the rest — instead of scrolling thousands of identical PHP Deprecated: lines.

Implementation Patterns

Gate CI on your own deprecations only. SYMFONY_DEPRECATIONS_HELPER is the control surface:

# .github/workflows/tests.yml
- run: php artisan test
  env:
    SYMFONY_DEPRECATIONS_HELPER: "max[self]=0&max[direct]=0&max[indirect]=999"

The classic ratchet is max[total]=0 for a clean app, then loosen to max[self]=0 once your vendor tree lags. Values: disabled, verbose=1 (prints the triggering stack trace — essential for anonymous deprecations), quiet[]=indirect to drop a bucket from the output, and max[...]=N per bucket. For a legacy suite, point the helper at a generated baseline JSON file so existing noise doesn't block the build while new deprecations still fail it.

Assert expected deprecations instead of ignoring them. When you deliberately call a deprecated path (a legacy driver, a shim for a package you don't control), use the bridge traits:

use Symfony\Bridge\PhpUnit\ExpectUserDeprecationMessageTrait;

class LegacyPaymentGatewayTest extends TestCase
{
    use ExpectUserDeprecationMessageTrait;

    public function test_legacy_charge_path_warns(): void
    {
        $this->expectUserDeprecationMessage('Since app 2.0: charge() is deprecated, use PaymentIntent.');
        (new LegacyPaymentGateway)->charge(100);
    }
}

ExpectDeprecationTrait::expectDeprecation() covers trigger_error(..., E_USER_DEPRECATED); ExpectUserDeprecationMessageTrait matches on the message and integrates with the bridge collector, so the assertion consumes the notice instead of counting it toward the threshold — which is what makes max[self]=0 sustainable.

Freeze time for code that calls globals. Laravel code should use Carbon::setTestNow(), but third-party code and old helpers often call time()/date()/strtotime() directly. ClockMock overrides those functions in a registered namespace:

use Symfony\Bridge\PhpUnit\ClockMock;

/**
 * @group time-sensitive
 */
class TrialExpiryTest extends TestCase
{
    protected function setUp(): void
    {
        parent::setUp();
        ClockMock::register(TrialExpiry::class);   // must precede first load of the class
        ClockMock::withClockMock(1700000000);
    }

    public function test_trial_expires_after_14_days(): void
    {
        $this->assertFalse((new TrialExpiry)->isActive());
    }
}

On PHPUnit 10+ use #[Group('time-sensitive')] instead of the annotation — the group is what triggers the mock lifecycle and the cleanup between tests. DnsMock::register()/withMockingEnabled() is the equivalent for gethostbyname(), dns_get_record(), etc.; use it with @group dns-sensitive for any test that would otherwise hit the network.

Scope it to one suite, not the whole app. If your full suite is noisy, add the SymfonyExtension only to a dedicated phpunit-deprecations.xml and run it as a separate CI job (vendor/bin/phpunit -c phpunit-deprecations.xml). That keeps php artisan test --parallel fast and your deprecation report actionable.

Gotchas and Tips

  • simple-phpunit is not your binary in a Laravel app. vendor/bin/simple-phpunit downloads and manages its own PHPUnit copy (default dir bin/.phpunit, cacheable in CI via SYMFONY_PHPUNIT_DIR). That copy won't match Laravel's pinned phpunit/phpunit, and php artisan test resolves vendor/bin/phpunit, not simple-phpunit. Use the SymfonyExtension route and skip simple-phpunit unless you're pinning PHPUnit versions across a matrix with SYMFONY_PHPUNIT_VERSION.
  • Laravel rebinds the error handler. Illuminate\Foundation\Bootstrap\HandleExceptions calls set_error_handler() when the app boots, once per test. Deprecations raised during application bootstrap may be attributed to Laravel's handler rather than the bridge's collector. Assertions inside the test body are reliable; don't trust the bucket counts alone for framework-internal deprecations — run one test with SYMFONY_DEPRECATIONS_HELPER=verbose to confirm attribution.
  • self classification depends on project-root detection. The bridge decides what counts as "your code" from the Composer autoloader paths. Symlinked vendor/ directories (common with path repositories, DDEV, or Docker bind mounts) historically broke this — fixed in 7.4.17/8.1.5. If local counts differ wildly from CI, that's the first thing to check.
  • Register mocks before the class loads. ClockMock::register(Foo::class) only works if Foo's calls to time() resolve to the bridge's namespaced override at first compilation. Calling it after the class was autoloaded silently does nothing — no error, just a flaky test.
  • Parallel testing flattens the report. php artisan test --parallel (ParaTest) runs workers in separate processes; the bridge's per-run counters don't aggregate meaningfully, and threshold breaches can surface as worker exit codes without the nice summary. Run the deprecation job serially.
  • <env> in phpunit.xml vs real env. Put SYMFONY_DEPRECATIONS_HELPER in the <php><env name="..." value="..."/></php> block for local runs, but set it as a genuine environment variable in CI (Docker/Kubernetes env, GitHub env:) — that path is unambiguous regardless of PHPUnit's force handling.
  • Extension points beyond thresholds: use DebugClassLoader::enable() in a bootstrap if you maintain a package that extends Symfony classes — it warns on PSR-4 mismatches and on overriding @final/@internal methods, catching breakage that would otherwise appear only on the next Symfony minor. For programmatic control, DeprecationErrorHandler::register($config) accepts the same string you'd pass to the env var; useful in a custom tests/bootstrap.php when you need different thresholds per directory.
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.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle