Doctrine Deprecations makes deprecation warnings countable and suppressible
Thin library around different deprecation strategies
At a glance
- What is it?
- doctrine/deprecations is a small PHP layer that puts deprecation reporting behind one class, with three modes chosen by a DOCTRINE_DEPRECATIONS environment variable or a static call, deduplication by identifier, and PHPUnit integration. The hard rule it imposes on producers is that a library must never switch the mode on, because that decision belongs to the application.
- Who is it for?
- Adopt doctrine/deprecations in a library that wants to deprecate an API without hijacking the host application's error handler, and in a consumer that wants one place to decide how deprecations surface. Do not let your library call the enableWith methods, since the README states that decision belongs to applications and frameworks.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 131 days ago.
- What is it written in?
- Mainly PHP, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Star history
- doctrine/deprecations1,857 ★ · +2
| Repository | 2026-10-16 | 2026-10-23 |
|---|---|---|
| doctrine/deprecations | 1.9k | 1.9k |
2 Stars in the last 7 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
One class decides how deprecations surface
The whole library is a thin layer over two mechanisms that PHP projects already have: trigger_error with E_USER_DEPRECATED, or PSR-3 logging. The point is not to add a third mechanism but to make the choice explicit and, crucially, to be inert until someone makes it.
The default is side-effect free. Nothing is triggered, nothing is logged, and the library can sit inside another package without knowing what error handler it will run under. That constraint is the design: a library cannot assume the application around it has a handler installed, and a handler installed by surprise is exactly the kind of surprise a library should avoid.
There is also a PSR-3 route for consumers who would rather not depend on the global error handler's state, which is the option the README recommends collecting deprecations with.
Three modes, one environment variable
The consumer side has three switches. To route deprecations to a PSR-3 logger:
\Doctrine\Deprecations\Deprecation::enableWithPsrLogger($logger);To emit them through the native error function, set DOCTRINE_DEPRECATIONS to trigger, or call enableWithTriggerError(). To count them without logging or emitting anything, set it to track, or call enableTrackingDeprecations().
The `track` mode is the one worth understanding first because the other two build on it. In every mode, tracking collects what fired and how often:
$deprecations = \Doctrine\Deprecations\Deprecation::getTriggeredDeprecations();
foreach ($deprecations as $identifier => $count) {
echo $identifier . " was triggered " . $count . " times\n";
}So you can start in track mode, see whether any deprecation fires at all, and only then decide whether to surface them. Deprecation::disable() turns tracking back off.
Identifiers are links, and dedup keys off them
A deprecation identifier is defined in the README as a link to any resource, most often a GitHub issue or pull request explaining the deprecation and its alternative. That definition does a lot of work. Because the identifier is a URL rather than a message string, deduplication is stable across versions where the message wording changes, and an application can suppress a deprecation by pasting the link from a log line.
Suppression comes in two granularities. A single deprecation is silenced with:
\Doctrine\Deprecations\Deprecation::ignoreDeprecations("https://link/to/deprecations-description-identifier");And a whole package is silenced with ignorePackage("doctrine/orm"), which is the blunt instrument for when a dependency's deprecations are not your problem yet.
Deduplication is on by default, and it is per request rather than global. The README explains that PHPUnit and similar tools can collect multiple instances of the same deprecation, which is why withoutDeduplication() exists. Note the consequence: if you disable it in production for a moment, a hot code path that deprecates on every call turns into a hot logging path.
Two trigger methods, and only one is unconditional
From the producer side there are two entry points, and the difference between them is the most interesting decision in the library.
trigger() fires unconditionally, even when the deprecated code path was reached from inside the library itself. It takes a package name, an identifier link and a message, with optional trailing arguments applied through sprintf:
\Doctrine\Deprecations\Deprecation::trigger(
"doctrine/orm",
"https://github.com/doctrine/orm/issue/1234",
"message %s %d",
"foo",
1234
);triggerIfCalledFromOutside() fires only when the caller is outside the current package. For a library deprecating its own behaviour, that is usually what you want, since internal calls during migration are noise rather than a signal to the consumer.
Both attach a limited stack trace to the message to locate the offending call. Based on the issue link, triggerIfCalledFromOutside triggers only once per request.
The rule that governs all of this is stated plainly: a producer or library should never call the enableWith methods and leave the decision about how deprecations are handled to applications and frameworks.
PHPUnit gets a trait and a configuration snippet
For testing, a VerifyDeprecations trait lets a test assert on what fired:
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
class MyTest extends TestCase
{
use VerifyDeprecations;
public function testSomethingDeprecation()
{
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/issue/1234');
triggerTheCodeWithDeprecation();
}
}Paired with expectNoDeprecationWithIdentifier(), that gives a test the ability to fail when a deprecation stops happening, which is how you find out a removal is now safe to schedule.
At suite level, the README shows a phpunit.xml configuration that displays details on tests that trigger deprecations and fails on deprecation, sets DOCTRINE_DEPRECATIONS to trigger in a server variable so native PHP deprecations are used, and adds a source element with ignoreSuppressionOfDeprecations="true" so the @ operator in a suppressed @trigger_error does not hide the notice.
That last attribute is a subtle and important one: without it, a suppressed deprecation silently disappears from the suite and the failOnDeprecation flag appears to be working when it is not. Code that must still trigger can be marked with #[IgnoreDeprecations] for specific tests.
No environment variable for dedup, so use a bootstrap file
One gap is called out directly: deduplication cannot be turned off with an environment variable. The documented workaround is a bootstrap file that requires the autoloader and calls withoutDeduplication(), then a pointer to that file from the PHPUnit configuration:
// tests/bootstrap.php
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
use Doctrine\Deprecations\Deprecation;
Deprecation::withoutDeduplication();Once that is in place you can set bootstrap="tests/bootstrap.php" in the phpunit element of the configuration.
The repository layout reflects a library with real static analysis behind it: composer.json, phpcs.xml, phpstan.neon, phpunit.xml.dist, a tests/ directory and a test_fixtures/ directory. There is also a SECURITY.md for reporting issues.
Licence, release line and where this is the wrong choice
The library is MIT licensed, so there is no obstacle to adopting it in commercial code.
Note the default branch: it is 1.1.x, not main. That is a maintenance branch and tells you the 1.1 line is the one receiving fixes. Releases 1.1.4 on 2024-12-08, 1.1.5 on 2025-04-07 and 1.1.6 on 2026-02-07 show a steady but infrequent cadence, and the last push was on 2026-06-14, four months before today. That is a healthy rate for a library this small, and also a sign to expect patch releases rather than features.
It is the wrong tool in two situations. If your application already has a deprecation policy through the error handler and nothing needs deduplication or suppression, this adds a dependency for no gain. And if you need deprecation data across requests rather than within one, the tracking store here is per request and does not describe where the data goes.
The producers side is also a commitment. Once you adopt the identifier convention, renaming an identifier breaks every consumer's suppression list, so treat those links as API.
Editorial conclusion
Adopt doctrine/deprecations in a library that wants to deprecate an API without hijacking the host application's error handler, and in a consumer that wants one place to decide how deprecations surface. Do not let your library call the enableWith methods, since the README states that decision belongs to applications and frameworks. Verify first which mode your application already selects through the DOCTRINE_DEPRECATIONS environment variable, because it defaults to nothing being enabled.
Frequently asked questions
How do I enable deprecations in doctrine/deprecations?
Call Deprecation::enableWithPsrLogger($logger) to route them to a PSR-3 logger, Deprecation::enableWithTriggerError() to emit them natively, or Deprecation::enableTrackingDeprecations() to only count them. The same three modes can be selected with the DOCTRINE_DEPRECATIONS environment variable set to trigger or track.
How do I suppress a specific deprecation?
Call Deprecation::ignoreDeprecations() with the deprecation identifier, which is a link to a GitHub issue or pull request describing the change. To silence a whole package at once, use Deprecation::ignorePackage("doctrine/orm").
What is a deprecation identifier in Doctrine Deprecations?
It is a link to any resource explaining the deprecation and its alternative, most often a GitHub issue or pull request. Deduplication keys off the identifier, and triggerIfCalledFromOutside triggers once per request per identifier.
Can a library force deprecations to be enabled?
No. The README states a producer or library should never call the enableWith methods and should leave the decision about handling deprecations to applications and frameworks. The library is side-effect free by default for exactly this reason.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/doctrine-deprecations)Star history chart for your README
[](https://hysenlabs.com/projects/doctrine-deprecations)Was this useful?