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

Deprecation Contracts Laravel Package

symfony/deprecation-contracts

This package provides a single global function, trigger_deprecation(), that triggers silenced deprecation notices so a custom PHP error handler can catch and log them in dev and prod environments. It takes the package name, version, and a printf-style message.

View on GitHub
Deep Wiki
Context7

Getting Started

You almost certainly already have it. symfony/deprecation-contracts arrives transitively with symfony/console, symfony/http-foundation, symfony/process, symfony/error-handler — i.e. every Laravel app. Confirm before adding anything:

composer show symfony/deprecation-contracts
composer why symfony/deprecation-contracts   # shows which Symfony component pulled it in

Add it explicitly only when your code calls trigger_deprecation() — a transitive dependency reshuffle should never be able to delete a function you depend on:

composer require symfony/deprecation-contracts

Read the whole package first — it's one file. vendor/symfony/deprecation-contracts/function.php is a function_exists() guard wrapping a single @trigger_error("Since $package $version: " . vsprintf($message, $args), E_USER_DEPRECATED). There is no service provider, no facade, no config to publish, no container binding. Everything below follows from that one line.

Then do the part everyone skips: make deprecations visible. Laravel's Illuminate\Foundation\Bootstrap\HandleExceptions special-cases E_DEPRECATED/E_USER_DEPRECATED and routes them to config('logging.deprecations.channel') — which defaults to the null channel, i.e. discarded. Add a real channel in config/logging.php:

'channels' => [
    // ...
    'deprecations' => [
        'driver' => 'daily',
        'path' => storage_path('logs/deprecations.log'),
        'level' => 'info',
        'days' => 14,
        'replace_placeholders' => true,
    ],
],
LOG_DEPRECATIONS_CHANNEL=deprecations
LOG_DEPRECATIONS_TRACE=false   # true in local/staging only — it's noisy

First use case — mark one legacy method you already want gone:

public function total(): int
{
    trigger_deprecation(
        'acme/billing',
        '2.4.0',
        'The "%s" method is deprecated, call "%s" instead.',
        'Invoice::total()',
        'Invoice::amount()->get()',
    );

    return $this->amount()->get();
}

Verify in 20 seconds:

php artisan tinker
>>> trigger_deprecation('acme/billing', '2.4.0', 'Test deprecation from %s', 'tinker');
>>> exit

tail -n 5 storage/logs/deprecations.log
# local.INFO: Since acme/billing 2.4.0: Test deprecation from tinker

Nothing in the log means your channel is misconfigured — that accounts for nearly every "the package does nothing" report.

Implementation Patterns

1. Call it on the first line, not the last. The notice must reach the caller before the legacy path executes, so it still fires if the method later throws or returns early.

2. Centralise the package name and version. You'll write the first two arguments a hundred times; give yourself one seam to edit and one place to swap for Log::warning() later:

namespace App\Support;

final class Deprecations
{
    public static function since(string $version, string $message, mixed ...$args): void
    {
        trigger_deprecation(config('app.slug', 'acme/app'), $version, $message, ...$args);
    }
}

// Deprecations::since('2.4.0', 'Config key "%s" is deprecated; use "%s".', 'billing.legacy_currency', 'billing.currency');

3. Deprecate at the resolution point. Config keys, routes, Blade directives, container aliases — anything a user can still use deserves a notice exactly where it is read:

// AppServiceProvider::register()
if (config()->has('billing.legacy_currency')) {
    Deprecations::since('2.4.0', 'The "%s" config key is deprecated; use "%s".', 'billing.legacy_currency', 'billing.currency');
}

4. Guard hot paths with once(). A deprecation inside a loop or an accessor floods the log. Laravel 11+ once() is the clean fix — the notice is about log volume, not CPU cost:

once(fn () => Deprecations::since('2.4.0', 'Attribute access on "%s" is deprecated.', static::class));

5. Follow the Symfony convention exactly in packages. Package name from composer.json, version = the release that introduces the deprecation, message phrased "X is deprecated, use Y instead". Consumers grep Since vendor/pkg 2.4.0: to know which upgrade introduced the noise.

6. Test that the notice actually fires. PHP still invokes user error handlers for @-suppressed errors, so a temporary handler catches it:

public function test_legacy_helper_emits_deprecation(): void
{
    $captured = [];

    set_error_handler(function (int $errno, string $errstr) use (&$captured) {
        if ($errno === E_USER_DEPRECATED) {
            $captured[] = $errstr;
            return true;
        }
        return false;
    }, E_USER_DEPRECATED);

    try {
        legacyHelper();
    } finally {
        restore_error_handler();
    }

    $this->assertCount(1, $captured);
    $this->assertStringContainsString('Since acme/billing 2.4.0:', $captured[0]);
}

7. Gate CI on new deprecations. Laravel's base TestCase installs a deprecation-to-exception handler by default, so php artisan test already surfaces them. Harden it further with failOnDeprecation="true" on the <phpunit> root element (PHPUnit 10/11) or --fail-on-deprecation. Reach for withoutDeprecationHandling() sparingly — it's a silencer, and silencers metastasise.

8. Pair the runtime notice with a @deprecated docblock. PHPStan/Psalm and IDEs read the annotation; the function is what actually logs. Do both, one line apart.

9. Use the log as a removal signal. Ship deprecations.log to your aggregator and alert on first occurrence per Since … string (something still calls it) and last occurrence (safe to delete). That converts "we think nobody uses Invoice::total()" into a decision backed by data.

Gotchas and Tips

  • ⚠️ Silent by default, twice over. The function is @-suppressed and Laravel's deprecation channel defaults to null. Both must be addressed before you see anything. The @ only zeroes error_reporting() during the call — Laravel's handler deliberately ignores that gate for deprecations — but the null channel drops them outright.
  • ⚠️ The README's symfony/error-handler advice is not your setup. Laravel has its own handler; don't install the Symfony one. And if you ever write a custom handler, never gate on error_reporting() & $level for E_USER_DEPRECATED or the @ will make every notice look disabled.
  • ⚠️ Don't redeclare trigger_deprecation() from your root composer.json files autoload. Composer loads dependencies' files before the root package's, so your empty stub runs second → fatal "cannot redeclare". The README's "declare an empty function" trick only works for projects that don't install the package. To silence globally, point the deprecations channel at null.
  • ⚠️ Version strings must be real releases. 'next', 'upcoming', 'TBD' destroy the whole point of the prefix. Use the version you are about to tag.
  • ⚠️ Argument mismatches crash the request. Fewer than three arguments → ArgumentCountError; a %s with no matching argument → ValueError from vsprintf(). Treat deprecation messages like any other formatted string and cover them with a test.
  • ⚠️ "It's not firing" is usually "the code path never runs". Cached config (config:cache), cached routes, and opcache in FPM will all hide a provider-level notice from you. php artisan optimize:clear before debugging, and prove reachability with a logger() next to the call.
  • 🛠️ Pass arguments instead of interpolating. trigger_deprecation('acme/billing', '2.4.0', 'Use "%s"', $class) keeps the Since <pkg> <version>: prefix machine-parseable and defers formatting.
  • 🛠️ Filter vendor noise. Once enabled you'll see Symfony's own deprecations. Locally, grep -v 'Since symfony/' isolates yours; in production, route your own calls through App\Support\Deprecations so you can split streams.
  • 🛠️ Queues and Octane lose attribution. Job deprecations log against the worker, not the originating request — log surrounding context yourself if usage attribution matters. Long-lived Octane workers keep one handler across many requests; verify your channel rotates files as expected before trusting daily logs.
  • 📦 It genuinely costs nothing. A function_exists()-guarded global emitting a silenced E_USER_DEPRECATED is cheap enough for hot paths. The only real cost is log volume, which is what once() is for.
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.
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk