11. Error Handling
PHPUnit’s test runner registers an error handler and processes
E_DEPRECATED, E_USER_DEPRECATED, E_NOTICE, E_USER_NOTICE, E_STRICT, E_WARNING, and E_USER_WARNING
errors. We will use the term “issues” to refer to E_DEPRECATED, E_USER_DEPRECATED, E_NOTICE, E_USER_NOTICE,
E_STRICT, E_WARNING, and E_USER_WARNING errors for the remainder of this chapter.
E_STRICT was deprecated in PHP 8.4. On PHP versions where E_STRICT errors still exist, PHPUnit processes
them as if they were E_NOTICE errors.
The error handler also processes E_USER_ERROR errors. These are not issues in the sense used in this chapter,
they are handled differently (see Errors (E_USER_ERROR)).
The error handler is active while a test is running and only processes issues triggered by test code or code that is called from test code. It ignores issues triggered by PHPUnit’s own code as well as code from PHPUnit’s dependencies. Issues that are triggered while the test suite is loaded, before the first test is run, are processed separately (see Issues triggered outside of tests).
Other error handlers
When PHPUnit’s test runner becomes aware (after it called set_error_handler() to register its error handler)
that another error handler was registered then it keeps its error handler active and calls the previously
registered error handler before it processes an issue. Consequently, the features described in this chapter
remain available when you use your own error handler. The return value of the previously registered error
handler does not prevent PHPUnit’s test runner from processing an issue. While the previously registered
error handler runs, error_reporting() returns the value it would return without PHPUnit’s test runner,
so error handlers that check error_reporting() dynamically (also to respect the @ operator) behave
as they do in production.
When the previously registered error handler throws an exception, for instance to turn errors into exceptions,
then this exception replaces the error: PHPUnit’s test runner does not process an issue and the exception
propagates into the test, where it can be expected using expectException(), for example. Be aware that this
also applies to deprecations: when the previously registered error handler throws an exception for
E_DEPRECATED or E_USER_DEPRECATED errors then a test that triggers such an error will be reported
as errored instead of as having triggered a deprecation.
The #[WithoutErrorHandler] attribute can be used to disable PHPUnit’s error handler for a test method
so that only the previously registered error handler is active while the test method is run.
Your own error handler should follow best practices
Your own error handler should ignore errors emitted by code it is not responsible for, for instance PHPUnit’s code.
The error handler emits events that are, for instance, subscribed to and used by the default progress and result printers as well as loggers.
Here is the code that we will use for the examples in the remainder of this chapter:
.
├── phpunit.xml
├── src
│ └── FirstPartyClass.php
├── tests
│ └── FirstPartyClassTest.php
└── vendor
├── autoload.php
└── ThirdPartyClass.php
4 directories, 5 files
<?php declare(strict_types=1);
namespace example;
use PHPUnit\Framework\TestCase;
use vendor\ThirdPartyClass;
final class FirstPartyClassTest extends TestCase
{
public function testOne(): void
{
$this->assertTrue((new FirstPartyClass)->method());
}
public function testTwo(): void
{
$this->assertTrue((new ThirdPartyClass)->anotherMethod());
}
}
<?php declare(strict_types=1);
namespace example;
use function trigger_error;
use vendor\ThirdPartyClass;
final class FirstPartyClass
{
public function method(): true
{
(new ThirdPartyClass)->method();
trigger_error('deprecation in first-party code', E_USER_DEPRECATED);
return true;
}
}
<?php declare(strict_types=1);
namespace vendor;
use example\FirstPartyClass;
final class ThirdPartyClass
{
public function method(): void
{
trigger_error('deprecation in third-party code', E_USER_DEPRECATED);
}
public function anotherMethod(): true
{
return (new FirstPartyClass)->method();
}
}
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.1/phpunit.xsd"
bootstrap="vendor/autoload.php"
cacheDirectory=".phpunit.cache"
>
<testsuites>
<testsuite name="default">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit>
PHPUnit’s test runner prints D, N, and W, respectively, for tests that execute code which triggers an issue
(D for deprecations, N for notices, and W for warnings).
Shown below is the default output PHPUnit’s test runner prints for the example shown above:
$ ./tools/phpunit PHPUnit 13.4.0 by Sebastian Bergmann and contributors. Runtime: PHP 8.5.10 Configuration: /path/to/example/phpunit.xml DD 1 / 1 (100%) Time: 00:00.002, Memory: 8.00 MB OK, but there were issues! Tests: 2, Assertions: 2, Deprecations: 2.
Detailed information, for instance which issue was triggered where, is only printed when --display-deprecations, --display-phpunit-deprecations, --display-phpunit-notices, --display-errors, --display-notices, --display-warnings, or --display-all-issues is used:
$ ./tools/phpunit --display-deprecations PHPUnit 13.4.0 by Sebastian Bergmann and contributors. Runtime: PHP 8.5.10 Configuration: /path/to/example/phpunit.xml DD 1 / 1 (100%) Time: 00:00.002, Memory: 8.00 MB 2 tests triggered 2 deprecations: 1) /path/to/vendor/ThirdPartyClass.php:10 deprecation in third-party code Triggered by: * exampleFirstPartyClassTest::testOne /path/to/tests/FirstPartyClassTest.php:17 * exampleFirstPartyClassTest::testTwo /path/to/tests/FirstPartyClassTest.php:22 2) /path/to/src/FirstPartyClass.php:13 deprecation in first-party code Triggered by: * exampleFirstPartyClassTest::testOne /path/to/tests/FirstPartyClassTest.php:17 * exampleFirstPartyClassTest::testTwo /path/to/tests/FirstPartyClassTest.php:22 OK, but there were issues! Tests: 2, Assertions: 2, Deprecations: 2.
Each of these CLI options has a corresponding attribute on the <phpunit> element of the XML configuration file,
for instance displayDetailsOnTestsThatTriggerDeprecations
(see The displayDetailsOnTestsThatTriggerDeprecations Attribute).
By default, issues do not affect the shell exit code: it indicates success even when the tests triggered issues.
The failOnDeprecation,
failOnNotice, and
failOnWarning attributes as well as the
failOnAllIssues attribute (and the corresponding
--fail-on-* CLI options) can be used to let the test runner exit with a shell exit code that indicates failure
when issues were triggered. Enabling one of these settings implicitly enables the display of the corresponding
issue details.
Limiting issues to “your code”
A common problem is that dependencies in vendor trigger deprecation warnings, notices, or warnings that clutter your test output.
The reporting of issues can be limited to “your code” so that you only see issues that originate from code you are responsible for.
First, you need to configure what you consider “your code” using the <source> element in your XML configuration file (see The <source> Element).
Then you can use the following attributes on the <source> element to filter issues:
ignoreIndirectDeprecations="true"ignoresE_DEPRECATEDandE_USER_DEPRECATEDtriggered by third-party code (e.g. code invendor)restrictNotices="true"ignoresE_NOTICE,E_USER_NOTICE, andE_STRICTtriggered by third-party coderestrictWarnings="true"ignoresE_WARNINGandE_USER_WARNINGtriggered by third-party code
Here is a configuration that only reports issues from your own code:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.1/phpunit.xsd"
bootstrap="vendor/autoload.php"
cacheDirectory=".phpunit.cache"
>
<testsuites>
<testsuite name="default">
<directory>tests</directory>
</testsuite>
</testsuites>
<source ignoreIndirectDeprecations="true"
restrictNotices="true"
restrictWarnings="true">
<include>
<directory>src</directory>
</include>
</source>
</phpunit>
Here is what the output of PHPUnit’s test runner will look like after we configured (see above) it to restrict the reporting of issues to our own code:
$ ./tools/phpunit --display-deprecations PHPUnit 13.4.0 by Sebastian Bergmann and contributors. Runtime: PHP 8.5.10 Configuration: /path/to/example/phpunit.xml DD 1 / 1 (100%) Time: 00:00.002, Memory: 8.00 MB 2 tests triggered 2 deprecations: 1) /path/to/vendor/ThirdPartyClass.php:10 deprecation in third-party code Triggered by: * exampleFirstPartyClassTest::testOne /path/to/tests/FirstPartyClassTest.php:17 * exampleFirstPartyClassTest::testTwo /path/to/tests/FirstPartyClassTest.php:22 2) /path/to/src/FirstPartyClass.php:13 deprecation in first-party code Triggered by: * exampleFirstPartyClassTest::testOne /path/to/tests/FirstPartyClassTest.php:17 OK, but there were issues! Tests: 2, Assertions: 2, Deprecations: 2.
As you can see in the output shown above, deprecations triggered by third-party code located in the
vendor directory are not reported anymore.
The following attributes can be used on the <source> element to configure how PHPUnit
uses the information what your code is:
The <ignoreSelfDeprecations> Attribute setting can be used to ignore deprecations triggered by first-party code in first-party code
The <ignoreDirectDeprecations> Attribute setting can be used to ignore deprecations triggered by first-party code in third-party code
The <ignoreIndirectDeprecations> Attribute setting can be used to ignore deprecations triggered by third-party code
The <restrictNotices> Attribute setting can be used to ignore notices in third-party code
The <restrictWarnings> Attribute setting can be used to ignore warnings in third-party code
PHPUnit only identifies how an issue was triggered when the <source> element is configured. This identification
can also be disabled explicitly by setting identifyIssueTrigger="false" on the <source> element
(see The <identifyIssueTrigger> Attribute). When trigger identification is disabled,
all issues are treated as if it was unknown whether they were triggered by first-party code or third-party code:
the ignoreSelfDeprecations, ignoreDirectDeprecations, and ignoreIndirectDeprecations settings as well as
the trigger-specific failOn*Deprecation settings discussed below have no effect. The restrictNotices and
restrictWarnings settings are not affected by this: they only consider the location where the issue was triggered.
Failing on deprecations based on how they are triggered
The failOnDeprecation setting (see The failOnDeprecation Attribute)
and the --fail-on-deprecation CLI option are all-or-nothing: when they are used, every deprecation that
is reported causes PHPUnit’s test runner to exit with a shell exit code that indicates failure.
This is not always what you want. Deprecations triggered by first-party code in first-party code (self)
or by first-party code in third-party code (direct) are deprecations that you can act on by changing your
own code. Deprecations triggered by third-party code in third-party code (indirect) are usually outside
of your control: you may want to see them without letting them fail the test run.
When PHPUnit can identify how a deprecation was triggered, which requires the <source> element
(see The <source> Element) to be configured, the shell exit code can be
controlled separately for each trigger type:
the failOnSelfDeprecation attribute and the
--fail-on-self-deprecation/--do-not-fail-on-self-deprecationCLI options control failing on deprecations triggered by first-party code in first-party codethe failOnDirectDeprecation attribute and the
--fail-on-direct-deprecation/--do-not-fail-on-direct-deprecationCLI options control failing on deprecations triggered by first-party code in third-party codethe failOnIndirectDeprecation attribute and the
--fail-on-indirect-deprecation/--do-not-fail-on-indirect-deprecationCLI options control failing on deprecations triggered by third-party code
These settings are overrides that are layered on top of failOnDeprecation:
By default, each trigger type follows the
failOnDeprecationsetting.Configuring
failOnSelfDeprecation="true",failOnDirectDeprecation="true", orfailOnIndirectDeprecation="true"(or using the corresponding--fail-on-*CLI option) enables failing on deprecations of that trigger type even whenfailOnDeprecationis not enabled.Using
--do-not-fail-on-self-deprecation,--do-not-fail-on-direct-deprecation, or--do-not-fail-on-indirect-deprecationexempts deprecations of that trigger type from failing the test run even whenfailOnDeprecationorfailOnAllIssuesis enabled.Deprecations that are not classified as
self,direct, orindirect(for instance deprecations triggered by test code, or deprecations for which no trigger could be identified) always follow thefailOnDeprecationsetting.
Configuring failOnSelfDeprecation="false", failOnDirectDeprecation="false", or failOnIndirectDeprecation="false"
in the XML configuration file cannot exempt a trigger type when failOnDeprecation is enabled: only the
--do-not-fail-on-* CLI options can do that. So that this conflict does not go unnoticed, PHPUnit emits a test
runner warning for such a setting that has no effect, for example failOnSelfDeprecation="false" has no effect
because failOnDeprecation is enabled. Use the --do-not-fail-on-self-deprecation CLI option instead.
Here is a configuration for a common workflow: deprecations that you can act on (self and direct)
fail the test run while indirect deprecations are only reported:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/13.3/phpunit.xsd"
bootstrap="vendor/autoload.php"
cacheDirectory=".phpunit.cache"
failOnSelfDeprecation="true"
failOnDirectDeprecation="true"
>
<testsuites>
<testsuite name="default">
<directory>tests</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>src</directory>
</include>
</source>
</phpunit>
The same effect can be achieved on the command line by combining --fail-on-deprecation with
--do-not-fail-on-indirect-deprecation.
A deprecation that is exempted from failing the test run is still counted and still displayed: the test
runner prints “OK, but there were issues!”, but the shell exit code indicates success. Like
failOnDeprecation, the trigger-specific settings implicitly enable the display of deprecation details.
Together with the ignoreSelfDeprecations, ignoreDirectDeprecations, and ignoreIndirectDeprecations
attributes on the <source> element that were discussed earlier in this chapter, each trigger type
supports three states: a deprecation can be reported and fail the test run, it can be reported without
failing the test run, or it can be ignored entirely.
Ignoring issue suppression
By default, the error handler registered by PHPUnit’s test runner respects the suppression operator (@).
This means that issues triggered using @trigger_error(), for example, will not be reported by the
default progress and result printers.
The suppression of issues using the suppression operator (@) can be ignored by configuration settings
in PHPUnit’s XML configuration file:
The <ignoreSuppressionOfDeprecations> Attribute setting can be used to ignore the suppression of
E_USER_DEPRECATEDissuesThe <ignoreSuppressionOfPhpDeprecations> Attribute setting can be used to ignore the suppression of
E_DEPRECATEDissuesThe <ignoreSuppressionOfNotices> Attribute setting can be used to ignore the suppression of
E_USER_NOTICESissuesThe <ignoreSuppressionOfPhpNotices> Attribute setting can be used to ignore the suppression of
E_NOTICEandE_STRICTissuesThe <ignoreSuppressionOfWarnings> Attribute setting can be used to ignore the suppression of
E_USER_WARNINGissuesThe <ignoreSuppressionOfPhpWarnings> Attribute setting can be used to ignore the suppression of
E_WARNINGissuesThe <ignoreSuppressionOfErrors> Attribute setting can be used to ignore the suppression of
E_USER_ERRORerrors (see Errors (E_USER_ERROR))
Ignoring previously reported issues
PHPUnit’s test runner supports declaring the currently reported list of issues. Issues that are on this so-called baseline are no longer reported. This allows you to focus on new issues that are triggered by new or changed code.
When you run your test suite using the --generate-baseline CLI option then PHPUnit’s test runner
will write a list of all issues that are triggered to an XML file:
$ phpunit --generate-baseline baseline.xml PHPUnit 13.4.0 by Sebastian Bergmann and contributors. Runtime: PHP 8.2.10 Configuration: /path/to/example/phpunit.xml D 1 / 1 (100%) Time: 00:00.008, Memory: 4.00 MB OK, but there were issues! Tests: 1, Assertions: 1, Deprecations: 1. Baseline written to /path/to/example/baseline.xml.
When you run your test suite using the --use-baseline CLI option (or if you have configured a baseline
in your XML configuration file for PHPUnit using the The <baseline> Attribute setting)
then PHPUnit’s test runner will use this list of already known issues to ignore them for the current run:
$ phpunit --use-baseline baseline.xml PHPUnit 13.4.0 by Sebastian Bergmann and contributors. Runtime: PHP 8.2.10 Configuration: /path/to/example/phpunit.xml . 1 / 1 (100%) Time: 00:00.007, Memory: 4.00 MB OK (1 test, 1 assertion) 2 issues were ignored by baseline.
The --ignore-baseline CLI option can be used to ignore a baseline that is configured in the XML configuration
file for a single test run so that all issues are reported again.
Deprecations, notices, and warnings that are triggered by tests are put on the baseline when it is generated.
PHPUnit’s own deprecations, notices, and warnings as well as issues triggered outside of tests
(see Issues triggered outside of tests) are not put on the baseline. Issues that are suppressed using
the suppression operator (@) are only put on the baseline when the corresponding ignoreSuppressionOf*
setting (see above) is configured.
Expecting Deprecations (E_USER_DEPRECATED)
The expectUserDeprecationMessage() method can be used to expect that an E_USER_DEPRECATED
issue with a specified message is triggered.
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class DeprecationExpectationTest extends TestCase
{
public function testFailure(): void
{
$this->expectUserDeprecationMessage('the-deprecation-message');
}
}
Running the test shown above yields the output shown below:
./tools/phpunit tests/DeprecationExpectationTest.php
PHPUnit 13.4.0 by Sebastian Bergmann and contributors.
Runtime: PHP 8.5.11
F 1 / 1 (100%)
Time: 00:00, Memory: 32.33 MB
There was 1 failure:
1) DeprecationExpectationTest::testFailure
Expected deprecation with message "the-deprecation-message" was not triggered
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.
Alternatively, the $this->expectUserDeprecationMessageMatches() can be used to expect that
an E_USER_DEPRECATED issue is triggered where the deprecation message matches a specified
regular expression.
This can be used together with the #[IgnoreDeprecations] attribute to not let the test fail.
The #[IgnoreDeprecations] attribute (see IgnoreDeprecations) can be used both on
the test class and on the test method and is repeatable. It accepts an optional regular expression as its
messagePattern argument. When a message pattern is specified, only deprecations with a message that matches
the regular expression are ignored. This allows you to ignore the deprecation that a test expects while all other
deprecations triggered by the test are still reported.
Testing deprecated functionality
When you deprecate functionality in your code, you want to keep tests for the deprecated code until it is actually removed.
Use the #[IgnoreDeprecations] attribute together with expectUserDeprecationMessage() on tests that directly exercise deprecated functionality.
This ensures the deprecated code still works as expected, the expected deprecation message is verified, and the test is not reported as having triggered a deprecation.
Wrappers for trigger_error()
Some libraries do not call PHP’s trigger_error() function directly to trigger E_USER_DEPRECATED issues,
but use a wrapper such as the trigger_deprecation() function from symfony/deprecation-contracts or the
Doctrine\Deprecations\Deprecation::trigger() method from doctrine/deprecations. Such a wrapper adds a
stack frame: from PHP’s point of view, the deprecation is triggered inside the wrapper, not in the deprecated
code that calls the wrapper.
The <deprecationTrigger> element (see The <deprecationTrigger> Element)
can be used to configure functions and methods as deprecation triggers:
<source>
<include>
<directory>src</directory>
</include>
<deprecationTrigger>
<function>trigger_deprecation</function>
<method>Doctrine\Deprecations\Deprecation::trigger</method>
</deprecationTrigger>
</source>
Configuring deprecation triggers has two effects on the processing of E_USER_DEPRECATED issues:
The location that is reported for a deprecation is the location where the configured wrapper was called, not the location inside the wrapper where
trigger_error()was called.The stack frames of configured wrappers are ignored when PHPUnit identifies how a deprecation was triggered, so that the deprecation is classified based on the code that uses the wrapper.
Functions and methods that are configured as deprecation triggers but are not defined when the configuration is
loaded cause a test runner warning. The ignoreUndefinedTriggers attribute on the <deprecationTrigger>
element can be used to silently ignore such entries, for instance when a deprecation trigger is provided by a
library that is not installed in all environments.
Custom Issue Trigger Resolvers
While the <deprecationTrigger> element (see The <deprecationTrigger> Element) allows you to
configure functions and methods that act as wrappers around trigger_error(), some frameworks require more
sophisticated logic to determine the correct caller and callee for issue classification. For these cases, PHPUnit
supports custom issue trigger resolvers.
A custom issue trigger resolver is a class that implements the PHPUnit\Runner\IssueTriggerResolver\Resolver interface:
<?php declare(strict_types=1);
namespace PHPUnit\Runner\IssueTriggerResolver;
interface Resolver
{
/**
* Return null to defer to the next resolver in the chain.
*
* @param list<array{file?: string, line?: int, class?: class-string, function?: string, type?: string, args?: list<mixed>}> $trace
*/
public function resolve(array $trace, string $message): ?Resolution;
}
The resolve() method receives the filtered stack trace and the error message. It must return either:
A
PHPUnit\Runner\IssueTriggerResolver\Resolutionobject that specifies the callee and caller file pathsnullto defer to the next resolver in the chain
The Resolution class is constructed with two nullable string arguments: the callee file path and the caller file path:
new Resolution(
$trace[1]['file'] ?? null, // callee (where the issue originated)
$trace[2]['file'] ?? null, // caller (what called the code that triggered the issue)
);
PHPUnit uses these file paths to classify the issue as self, direct, indirect, or test
by checking whether each file belongs to first-party code, third-party code, test code, or PHPUnit itself.
Implementing a custom resolver
Consider a framework that wraps trigger_error() in its own method:
namespace Vendor;
final class Framework
{
public function trigger(): void
{
@trigger_error('framework deprecation', E_USER_DEPRECATED);
}
}
Without a custom resolver, PHPUnit’s default resolver uses $trace[0] as the callee (the file containing
trigger_error()) and $trace[1] as the caller. When the framework method is in the call stack, this
means the framework file is the callee and the first-party code calling it is the caller, which may not
accurately reflect the intended classification.
A custom resolver can inspect the stack trace and adjust the caller/callee accordingly:
<?php declare(strict_types=1);
namespace Vendor;
use PHPUnit\Runner\IssueTriggerResolver\Resolution;
use PHPUnit\Runner\IssueTriggerResolver\Resolver;
final class FrameworkResolver implements Resolver
{
public function resolve(array $trace, string $message): ?Resolution
{
if (isset($trace[0]['file']) && str_contains($trace[0]['file'], 'Framework.php')) {
return new Resolution(
$trace[1]['file'] ?? null,
$trace[2]['file'] ?? null,
);
}
return null;
}
}
This resolver checks whether the first frame in the stack trace is from the framework. If so, it shifts
the caller/callee by one frame to skip the framework’s wrapper. If the condition does not match, it returns
null to let the next resolver (or the default resolver) handle the issue.
Registering a custom resolver
Custom resolvers are registered in PHPUnit’s XML configuration file using the
<issueTriggerResolvers> element inside <source> (see The <issueTriggerResolvers> Element):
<source>
<include>
<directory>src</directory>
</include>
<issueTriggerResolvers>
<issueTriggerResolver className="Vendor\FrameworkResolver"/>
</issueTriggerResolvers>
</source>
Multiple resolvers can be registered and are called in the order they are listed. The first resolver that
returns a Resolution object wins. If all custom resolvers return null, PHPUnit falls back to its
default resolver.
Custom Deprecation Filters
The configuration settings described above let you ignore deprecations based on where they are triggered
(self, direct, or indirect). Sometimes you need finer-grained control, for instance to suppress
a particular deprecation message that you cannot fix yet, while still reporting all other deprecations. For
these cases, PHPUnit supports custom deprecation filters.
A custom deprecation filter is a class that implements the PHPUnit\Runner\DeprecationFilter interface:
<?php declare(strict_types=1);
namespace PHPUnit\Runner;
use PHPUnit\Event\Code\IssueTrigger\IssueTrigger;
interface DeprecationFilter
{
/**
* Return true to ignore the deprecation, false to let PHPUnit process it.
*/
public function ignores(string $message, string $file, int $line, IssueTrigger $trigger): bool;
}
The ignores() method is called for every E_DEPRECATED and E_USER_DEPRECATED issue. It receives:
$message: the deprecation message$file: the file in which the deprecation was triggered$line: the line on which the deprecation was triggered$trigger: anIssueTriggerobject describing how the deprecation was triggered
The method must return true to ignore the deprecation or false to let PHPUnit process it.
A deprecation that is ignored by a filter is treated exactly like a deprecation that is ignored using the
#[IgnoreDeprecations] attribute: it is not shown in the test runner’s progress output or in the
deprecation summary, and it does not count towards failOnDeprecation (see
The failOnDeprecation Attribute).
Implementing a custom deprecation filter
The following filter ignores a single deprecation message while letting PHPUnit process all other deprecations:
<?php declare(strict_types=1);
namespace App\Tests;
use function str_contains;
use PHPUnit\Event\Code\IssueTrigger\IssueTrigger;
use PHPUnit\Runner\DeprecationFilter;
final class MyDeprecationFilter implements DeprecationFilter
{
public function ignores(string $message, string $file, int $line, IssueTrigger $trigger): bool
{
return str_contains($message, 'please ignore this deprecation');
}
}
The $trigger argument can be used to make the decision depend on how the deprecation was triggered.
For instance, $trigger->isIndirect() returns true when the deprecation was triggered by third-party code.
Registering a custom deprecation filter
Custom deprecation filters are registered in PHPUnit’s XML configuration file using the
<deprecationFilters> element inside <source> (see The <deprecationFilters> Element):
<source>
<include>
<directory>src</directory>
</include>
<deprecationFilters>
<deprecationFilter className="App\Tests\MyDeprecationFilter"/>
</deprecationFilters>
</source>
Multiple filters can be registered. A deprecation is ignored as soon as one of the registered filters
returns true for it.
Errors (E_USER_ERROR)
When test code, or code that is called from test code, triggers an E_USER_ERROR error then PHPUnit’s error
handler turns this error into an exception: the execution of the test is aborted at the point where the error
was triggered and the test is reported as errored.
The suppression operator (@) does not change this behavior: the test is aborted and reported as errored even
when the E_USER_ERROR error was suppressed.
Triggering an E_USER_ERROR error using trigger_error() has been deprecated in PHP 8.4. Code that uses
trigger_error($message, E_USER_ERROR) should be migrated to throw an exception (or call exit()).
Issues triggered outside of tests
Not all code runs while a test is running: test class files are loaded and data providers are executed while the test suite is being loaded, before the first test is run. PHPUnit’s error handler also processes issues that are triggered during this phase.
Issues that are triggered while a data provider runs are attributed to the tests that use the data provided by the data provider: they are reported as if they had been triggered by these tests.
Other issues that are triggered while the test suite is loaded, for instance a deprecation triggered when a test
class file is loaded, cannot be attributed to a test. They are counted and reported separately as issues that
were “triggered outside of tests”. The details for these issues are displayed using the same --display-* CLI
options and displayDetailsOn* configuration settings that were discussed earlier in this chapter, and they
are considered by the failOn* settings.
The bootstrap script (see The bootstrap Attribute) runs before PHPUnit’s error handler is registered: issues triggered by the bootstrap script are not processed by PHPUnit.
Error handlers registered by tests
PHPUnit’s test runner takes a snapshot of the registered error handlers before each test and compares it to the registered error handlers after the test:
When a test registers an error handler and does not remove it, the test is considered risky and reported with the message “Test code or tested code did not remove its own error handlers”. This check is not performed for tests that are run in a separate process.
When a test removes error handlers that it did not register, for instance PHPUnit’s own error handler, the test is considered risky and reported with the message “Test code or tested code removed error handlers other than its own”.
After each test, PHPUnit’s test runner restores the error handlers that were registered before the test so that subsequent tests are not affected.
Disabling PHPUnit’s error handler
When you want to test your own error handler or want to test that unit of code under test triggers an expected issue, for instance, the error handler registered by PHPUnit’s test runner will interfere with what you want to achieve.
The #[WithoutErrorHandler] attribute can be used in such a case to disable PHPUnit’s error handler for
a test method.
Disabling PHPUnit’s error handler is also required when the code under test relies on error_get_last() or
error_reporting(): while PHPUnit’s error handler is active, errors that it processes are not reported by
error_get_last() and error_reporting() returns a masked value so that PHP itself does not report errors
that PHPUnit’s error handler already processes.
Be aware that features of PHPUnit that rely on its error handler, meaning everything discussed in this chapter,
do not work for a test method that uses the #[WithoutErrorHandler] attribute.