calien / phpunit-deprecation-causer
PHPUnit extension reporting deprecations that first-party code causes through third-party code, while ignoreIndirectDeprecations keeps suppressing everything else.
Package info
github.com/calien666/phpunit-deprecation-causer
pkg:composer/calien/phpunit-deprecation-causer
Requires
- php: ^8.4
- phpunit/phpunit: ^13.1
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.64
- phpstan/phpstan: ^2.1
- phpstan/phpstan-phpunit: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-06 14:33:30 UTC
README
PHPUnit extension calien/phpunit-deprecation-causer
Description
A PHPUnit extension that keeps failOnDeprecation="true" meaningful when ignoreIndirectDeprecations="true" is set:
deprecations your own code causes through a factory or a dependency injection container are reported again, while
deprecations that third-party code triggers among itself stay suppressed.
The problem
With <source ignoreIndirectDeprecations="true">, PHPUnit decides by two stack frames: the file that triggered the
deprecation and the file that called into it. When both are third-party code, the deprecation is suppressed.
That also hides deprecations your code caused, whenever a factory or container sits in between:
// Reported: your code calls the deprecated constructor. new DeprecatedService(); // Suppressed: the container calls the deprecated constructor on your behalf. $container->get(DeprecatedService::class);
The same applies to a service of yours that gets a deprecated service injected. The extension walks past the
configured pass-through files and lets PHPUnit classify the first frame behind them instead. If that frame is
first-party code (inside <source>) or the test itself, PHPUnit reports the deprecation as one your code caused.
Compatibility
Every major of the package supports exactly one PHPUnit major. Configuration and integration point are the same in all of them.
| Branch | State | Composer Package Name | Version | PHPUnit | PHP |
|---|---|---|---|---|---|
| main | development | calien/phpunit-deprecation-causer | 13.0.x-dev | 13.1 and later | 8.4, 8.5 |
| 12 | development | calien/phpunit-deprecation-causer | 12.0.x-dev | 12.5.13 and later | 8.3, 8.4, 8.5 |
| 11 | development | calien/phpunit-deprecation-causer | 11.0.x-dev | 11.5.54 and later | 8.2, 8.3, 8.4, 8.5 |
Installation
Install the version matching the PHPUnit major of the project as development dependency:
# PHPUnit 13 composer require --dev 'calien/phpunit-deprecation-causer':'13.0.*@dev' # PHPUnit 12 composer require --dev 'calien/phpunit-deprecation-causer':'12.0.*@dev' # PHPUnit 11 composer require --dev 'calien/phpunit-deprecation-causer':'11.0.*@dev'
Important
No version is released yet. Once 13.0.0, 12.0.0 and 11.0.0 are tagged, require ^13, ^12 or ^11.
Configuration
Register the extension and name the pass-through paths, comma-separated. A file is pass-through code when its path contains one of the fragments:
<phpunit failOnDeprecation="true"> <source ignoreIndirectDeprecations="true"> <include> <directory>src/</directory> </include> </source> <extensions> <bootstrap class="Calien\PhpUnitDeprecationCauser\Extension"> <parameter name="passThroughPaths" value="/vendor/acme/container/,/var/cache/container/"/> </bootstrap> </extensions> </phpunit>
List only code that instantiates or calls on behalf of its caller. Everything else stays subject to PHPUnit's own classification.
When a file also contains code of its own, name the single method instead, as Fqcn::method. Only frames that run
inside that method count as pass-through code then:
<parameter name="passThroughPaths" value="/vendor/acme/container/,Acme\Testing\TestCase::get"/>
The extension does nothing when ignoreIndirectDeprecations is off: PHPUnit reports every deprecation then.
A deprecation reported this way is a regular deprecation: failOnDeprecation,
displayDetailsOnTestsThatTriggerDeprecations and #[IgnoreDeprecations] apply to it as to any other.
<deprecationTrigger> entries are respected.
Framework integrations
The package knows no framework. An integration ships its pass-through paths in its own PHPUnit extension and hands
them to DeprecationCauserRegistrar, merged with the ones configured by the project:
use Calien\PhpUnitDeprecationCauser\DeprecationCauserRegistrar; use Calien\PhpUnitDeprecationCauser\PassThroughPaths; use PHPUnit\Runner\Extension\Extension; use PHPUnit\Runner\Extension\Facade; use PHPUnit\Runner\Extension\ParameterCollection; use PHPUnit\TextUI\Configuration\Configuration; final class AcmeFrameworkExtension implements Extension { public function bootstrap(Configuration $configuration, Facade $facade, ParameterCollection $parameters): void { $passThroughPaths = (new PassThroughPaths(['/vendor/acme/framework/src/Container/'])) ->merge(PassThroughPaths::fromParameters($parameters)); (new DeprecationCauserRegistrar())->register($configuration, $facade, $passThroughPaths); } }
When a framework executes project code from files it generated, such as a cache file concatenating configuration
files of several packages, the integration passes a GeneratedFileMapper as fourth argument. It maps such a file and
line back to the file the code came from, and PHPUnit then classifies that file.
Some deprecations name their cause only in the message, for instance when a framework migrates the configuration of
a project at runtime and no frame of the project is on the stack. A MessageCauseResolver, passed as fifth argument,
names the causing file from the message; FirstPartyCode::fromConfiguration() gives it the <source> directories
and tells whether a file belongs to the project, so third-party configuration stays suppressed.
Limitations
- Resolution started by third-party code is not attributed: when a framework instantiates your service and that service needs a deprecated dependency, no frame of your code is on the stack.
- Deprecations about configuration, such as a framework migrating your configuration at runtime, carry no frame
of your code either. A framework integration can attribute them with a
MessageCauseResolver. - Native PHP deprecations (
E_DEPRECATED) are left to PHPUnit; onlyE_USER_DEPRECATEDis handled. - Tests in separate processes are not covered: the extension is not bootstrapped in the child process.
- The extension implements PHPUnit's issue trigger resolver interface, but registers itself through PHPUnit's internal error handler, so that one call can change in a minor PHPUnit release. With a resolver registered, PHPUnit also passes call arguments into the stack traces it collects for deprecations.
Development
See DEVELOPERS.md.