Erik van Velzen
Erik van Velzen

Reputation: 7012

In __destruct(), how can you see if an exception is currently in flight?

How can I see if an exception is currently in flight, i.e. the stack is unwinding?

In the example below how would you implement isExceptionInFlight()?

<?php

class Destroyer
{
    function __destruct()   {
        if (isExceptionInFlight()) {
            echo 'failure';
        } else {
            echo 'success';
        }
    }
}

function isExceptionInFlight() {
    // ?????
}

function createAndThrow()
{
    $var = new Destroyer;
    throw new \Exception;
}

createAndThrow();

The purpose of this would be to implement D's scope statement, which is available as a library in multiple other languages. This allows you to get rid of nested try-catch blocks, which in turn makes it easier to do transactions with rollbacks correctly.

Addendum1:

I've looked around in the Zend PHP Engine and executor_globals.exception seems to be what I'm looking for (https://github.com/php/php-src/blob/master/Zend/zend_globals.h). However this value is always nullptr when I inspect it during __destruct(). Any idea where I should look next?

Addendum2:

Inspecting executor_globals.opline_before_exception has led to some progress. However it is not reset to nullptr when the exception has been caught.

Addendum3:

I've found the following code (line 135)

/* Make sure that destructors are protected from previously thrown exceptions.
 * For example, if an exception was thrown in a function and when the function's
 * local variable destruction results in a destructor being called.
 */
old_exception = NULL;
if (EG(exception)) {
    if (EG(exception) == object) {
        zend_error_noreturn(E_CORE_ERROR, "Attempt to destruct pending exception");
    } else {
        old_exception = EG(exception);
        EG(exception) = NULL;
    }
}
zend_call_method_with_0_params(&obj, object->ce, &destructor, ZEND_DESTRUCTOR_FUNC_NAME, NULL);
if (old_exception) {
    if (EG(exception)) {
        zend_exception_set_previous(EG(exception), old_exception);
    } else {
        EG(exception) = old_exception;
    }
}

This seems to actively PREVENT me from doing what I want, and explains why executor_globals.exception is always nullptr.

Upvotes: 5

Views: 371

Answers (1)

Marco Aur&#233;lio Deleu
Marco Aur&#233;lio Deleu

Reputation: 4367

Although I don't recommend, I have implemented it in the past. My approach was (simply put) like this:

Implement custom Exception class

class MyException extends Exception {
    public static $exceptionThrown = false;

    public function __construct($your parameters) {
         self::$exceptionThrown = true;
    }

}

Now, every exception should be your own exception implementation instead of default Exception.

class Destroyer {
    public function __destruct() {
        if(MyException::exceptionThrown() {
            Database::rollback();
        } else {
            Database::commit();
        }
    }
}

Upvotes: 1

Related Questions