Deprecations and Upgrades
A deprecation notice names a line that the next major version will break; the deprecation log is therefore the upgrade plan.
trigger_deprecation(string $package, string $version, string $message, mixed ...$args): voidA deprecation is a warning that something still works but will be removed. Symfony emits one as a silenced E_USER_DEPRECATED notice saying which package deprecated what, in which version, and what to use instead. Nothing fails. That is the point: you get a list before you get an outage.
The log is the plan
Symfony 8.1 routes deprecations to a dedicated log channel called deprecation, configured by the Monolog recipe. In dev they also appear in the web debug toolbar and on the Logs page of the profiler. Every distinct message is one upgrade task. When the log is empty, the major upgrade is mostly a version bump.
Minor, then major
Upgrade to the last minor release of your current major first, because deprecations are added in minors. Each major branch has five minors (8.0 to 8.4), so Symfony 8.4 is the one that tells you everything Symfony 9.0 will remove; Symfony 8.0 cannot. Then change the constraints and run composer update "symfony/*", and read UPGRADE-9.0.md for the breaks that were never deprecated.
Deprecating your own code
trigger_deprecation(), from the symfony/deprecation-contracts package, is a thin wrapper: it prepends Since <package> <version>: to your message and calls @trigger_error($message, \E_USER_DEPRECATED). Both forms end up in the same log. PHP 8.4 added the #[\Deprecated] attribute, which raises the same kind of notice when a marked function or method is called.
Example
// src/Service/InvoiceNumberer.php
declare(strict_types=1);
namespace App\Service;
final class InvoiceNumberer
{
public function next(int $year, int $sequence): string
{
return sprintf('INV-%d-%04d', $year, $sequence);
}
/**
* @deprecated since acme/billing 2.4, use next() instead
*/
public function nextNumber(int $year, int $sequence): string
{
trigger_deprecation(
'acme/billing',
'2.4',
'Calling "%s()" is deprecated, use "next()" instead.',
__METHOD__,
);
// The call above is this, with the "Since ..." prefix added for you.
// The @ matters: it silences the notice so it never reaches output,
// while an error handler can still see and log it.
//
// @trigger_error(
// 'Since acme/billing 2.4: Calling "'.__METHOD__.'()" is '
// .'deprecated, use "next()" instead.',
// \E_USER_DEPRECATED,
// );
return $this->next($year, $sequence);
}
}
// Output, on the "deprecation" log channel:
// Since acme/billing 2.4: Calling "App\Service\InvoiceNumberer::nextNumber()"
// is deprecated, use "next()" instead.When to use it
- Turn an upcoming major upgrade into a finite list of tasks by reading the deprecation log.
- Remove a method from a shared internal package without breaking the applications that still call it.
- Keep a test suite from accumulating new deprecations by failing the build when the count rises.
- Decide whether a dependency is maintained: check whether its deprecations name a replacement.
More examples
Where deprecations are logged
This is close to what the Monolog recipe installs. The separate handler is what makes deprecations countable in production: one JSON line per notice on stderr, which a log collector can group by message. Reach for this file when the toolbar shows deprecations in dev but production looks clean.
# config/packages/monolog.yaml
monolog:
channels:
- deprecation
when@dev:
monolog:
handlers:
main:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
channels: ["!event"]
when@prod:
monolog:
handlers:
main:
type: fingers_crossed
action_level: error
handler: nested
# Deprecations are deliberately kept out of the main handler.
channels: ["!deprecation"]
buffer_size: 50
nested:
type: stream
path: php://stderr
level: debug
formatter: monolog.formatter.json
deprecation:
type: stream
channels: [deprecation]
path: php://stderr
formatter: monolog.formatter.jsonFailing the test suite when the count grows
symfony/phpunit-bridge collects deprecations during a test run and prints them grouped by message with a count and the tests that caused them. Setting max[self]=0 with a baseline is the practical setting: existing debt is tolerated, new debt is a red build. Install it with composer require --dev symfony/phpunit-bridge.
<!-- phpunit.dist.xml -->
<phpunit>
<php>
<!-- max[self] = deprecations triggered by your own code.
A baseline file records the ones you already have, so only
new ones fail. Generate it once with generateBaseline=true. -->
<env name="SYMFONY_DEPRECATIONS_HELPER"
value="max[self]=0&baselineFile=./tests/allowed.json"/>
</php>
</phpunit>Moving the version constraint in one place
Symfony Flex reads extra.symfony.require and applies it to every symfony/* package, so a major upgrade edits one string instead of thirty. Then run composer update "symfony/*" (add --with-all-dependencies if third-party packages block it) and clear var/cache before booting.
{
"require": {
"php": ">=8.5",
"symfony/console": "*",
"symfony/framework-bundle": "*",
"symfony/runtime": "*"
},
"extra": {
"symfony": {
"require": "8.1.*"
}
}
}
Discussion