Skip to content
Johann Pardanaud

Mocking an exception in PHP

When writing tests, you might sometimes need to mock an exception, this is an unusual case but it happened to me one year ago while overcoming a bug in Symfony's Security component. I needed to know if an exception was thrown by the Symfony\Component\Security\Http\Firewall\AccessListener class or not, I did it by writing a code similar to this:

use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Http\Firewall\AccessListener;

class ThrowerChecker
    public function isThrownByFirewall(AccessDeniedException $exception): bool
        // Walking through the stack frames and returning true if one of them is triggered
        // by the firewall (Symfony\Component\Security\Http\Firewall\AccessListener)
        foreach ($exception->getTrace() as $stackItem) {
            $class = $stackItem['class'] ?? null;
            if ($class === AccessListener::class) {
                return true;

        return false;

However, testing this short piece of code can be quiet tricky. You can't mock an exception and override the getTrace() method because it is marked as finallike many other ones.

Since PHP 7, you can use the Throwable interface, which can be easily mocked because interfaces can't declare final methods. However, sometimes you might need to mock a specific implementation, like for the isThrownByFirewall(AccessDeniedException $exception): bool method declared in the previous code.

The only solution left is to use the Reflection API to override the internal properties of the exception. You can easily know what are the internal properties of the class by dumping an instance of the Exception class:

// I'm using a function here to generate the exception
// because I want to show you a proper trace.
function generateException()
    return new Exception();



  object(Exception)#1 (7) {
    string(0) ""
    string(0) ""
    string(35) "/Users/johann/Desktop/exception.php"
    array(1) {
      array(4) {
        string(35) "/Users/johann/Desktop/exception.php"
        string(17) "generateException"
        array(0) {

Here you can see the trace property contains what would have been returned by the getTrace() method if we called it, this is what needs to changed to properly mock the exception trace. Let's write a simple test with a method handling the modification for us:

use PHPUnit\Framework\TestCase;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Http\Firewall\AccessListener;

class ThrowerCheckerTest extends TestCase
    public function testIfExceptionsThrownByFirewallAreDetected(): void
        $exception = new AccessDeniedException;
        self::overrideExceptionTrace(            $exception,            [['class' => 'foo'],            ['class' => AccessListener::class], ['class' => 'bar']]        );
        $throwerChecker = new ThrowerChecker();

     * Overrides an exception trace through reflection, since this
     * cannot be done through mocking (getTrace is final).
     * @param array<int, array<string, mixed>> $trace
    private static function overrideExceptionTrace(
        \Exception $exception,
        array $trace
    ): void {
        $exceptionReflection = new \ReflectionObject($exception);        // Find the parent root to be able to read the "trace" property        // because it's defined on the \Exception class        while ($exceptionReflection->getParentClass() !== false) {            $exceptionReflection = $exceptionReflection->getParentClass();        }        $traceReflection = $exceptionReflection->getProperty('trace');        $traceReflection->setAccessible(true);        $traceReflection->setValue($exception, $trace);        $traceReflection->setAccessible(false);    }

And that's it! We use a loop to find the reflection of the Exception class, then we make the property accessible, we rewrite it, and we make it private again.

Note that this kind of mock should be an… exception (I'm funny at parties)! This was done only to fix a bug in vendor code ; with proper software architecture this shouldn't happen.