Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cannot set exception code for custom exception #14673

Open
Chi-teck opened this issue Jun 26, 2024 · 12 comments
Open

Cannot set exception code for custom exception #14673

Chi-teck opened this issue Jun 26, 2024 · 12 comments

Comments

@Chi-teck
Copy link

Chi-teck commented Jun 26, 2024

Description

The following code:

<?php

class Example extends \Exception {
  protected $code = 5;
}

echo '  → ', (new Example(''))->getCode(), \PHP_EOL;
echo '0 → ', (new Example('', 0))->getCode(), \PHP_EOL;
echo '1 → ', (new Example('', 1))->getCode(), \PHP_EOL;

Resulted in this output:

  → 5
0 → 5
1 → 1

But I expected this output instead:

  → 5
0 → 0
1 → 1

There is a note about default parameters in exception constructor on php.net. However, case 2 in the above example explicitly passes the code parameter to the constructor.

Calling the constructor of class Exception from a subclass ignores the default arguments, if the properties $code and $message are already set. about default parameters in exception constructor on php.net.

PHP Version

PHP 8.2

Operating System

Debian 11

@iluuu1994
Copy link
Member

if (code) {
ZVAL_LONG(&tmp, code);
zend_update_property_ex(base_ce, Z_OBJ_P(object), ZSTR_KNOWN(ZEND_STR_CODE), &tmp);
}

The code property is only updated if the value is not 0. Changing this now would have some implications. E.g. this code would break:

class MyException extends Exception {
    public function __construct() {
        $this->code = 5;
        parent::__construct();
    }
}

$myException = new MyException();
var_dump($myException->getCode());

Instead of redeclaring the code property, why don't you override __construct()? This behaves the way you expected it to:

class Example extends \Exception {
    public function __construct(string $message = "", int $code = 5, ?Throwable $previous = null) {
        parent::__construct($message, $code, $previous);
    }
}

echo '  -> ', (new Example(''))->getCode(), \PHP_EOL;
echo '0 -> ', (new Example('', 0))->getCode(), \PHP_EOL;
echo '1 -> ', (new Example('', 1))->getCode(), \PHP_EOL;

@Chi-teck
Copy link
Author

The code property is only updated if the value is not 0.

And that does not align with how all other classes in PHP work . Property values passed through constructor must always override default property values.

@iluuu1994
Copy link
Member

That's not really how it works. PHP is implementation-defined. This behavior is intentional and has been the same for 21 years. So, I consider this a wontfix, but let's see what others think.

@Chi-teck
Copy link
Author

Chi-teck commented Jun 28, 2024

I believe this behavior is not intentional. The documentation says that in this case the default arguments are ignored, but in practice it also ignores explicitly set argument when it's value is equal to zero, which is very confusing.

// This should return zero for any exception classes.
(new Example(code: 0))->getCode(); 

@iluuu1994
Copy link
Member

iluuu1994 commented Jun 28, 2024

For context:

https://www.php.net/manual/en/exception.construct.php

Note: Calling the constructor of class Exception from a subclass ignores the default arguments, if the properties $code and $message are already set.

Named arguments are relatively new. The default arguments refers to omitting the arguments, or providing the same value as the default explicitly. But again, PHP is implementation defined. When behavior and documentation diverge, behavior wins, unless it is obviously wrong. Even when it is obviously wrong, it's sometimes better to be cautious and not risk breaking old code, especially in patches. This could be discussed for master, but IMO is out of the question for stable versions.

@Chi-teck
Copy link
Author

Named arguments are relatively new.

It has nothing to do with named arguments.
This example may fail same way.

(new Example('', 0))->getCode();

The default arguments refers to omitting the arguments, or providing the same value as the default explicitly.

Well, that's not typical interpretation of "default arguments". In my opinion default arguments are those defined on the function signature, like int $code = 0.

unless it is obviously wrong.

That's what I am thinking about it. It obviously wrong.

@damianwadley
Copy link
Member

To be very clear here, the behavior is totally intentional: if the $code parameter is non-zero then the constructor will set the property to that value. The internal code does something that translates to:

function __construct(string $message = "", int $code = 0, ?Throwable $previous = null) {
    if (func_num_args() >= 1) {
        $this->message = $message;
    }
    if ($code) {
        $this->code = $code;
    }
    if ($previous) {
        $this->previous = $previous;
    }
}

There's a little magic for the $message, unfortunately, but the behavior for $code is simple and obviously deliberate.

The implementation is the source of truth, so if the documentation doesn't accurately describe this behavior then it's the documentation that should be fixed.
Whether or not the implementation is "good" is a separate problem, however the behavior has been around for a long time - almost 21 years - so the question is whether there's sufficient reason to change it.

@Chi-teck
Copy link
Author

There's a little magic for the $message, unfortunately, but the behavior for $code is simple and obviously deliberate.

The current behavior for $message is what I expected for $code. It's intuitively clear and aligns with how all other PHP objects work.

Why $message and $code have different behavior?

<?php

class Example extends \Exception {
  public $code = 5;
  public $message = 'default';
}

$exception = new Example('', 0);
var_dump($exception->getMessage());
var_dump($exception->getCode());
string(0) ""
int(5)

almost 21 years

I don't think that age of a bug should turn it into a feature. )

@damianwadley
Copy link
Member

Why $message and $code have different behavior?

The code (which I suggest you look at, if you haven't yet) is based on whether $message is null - something only the internals can get away with since the signature is supposed to be string $message = "" and so null isn't supported as an actual value. The reason for null and the difference in behavior seems to be simply the result of various changes being made over a long time; I suspect the idea was to have it work like ?string $message = null but it didn't happen that way for whatever reason.

But that's not really the point. The point is that the constructor follows an unusual pattern, and that pattern has been in place for a long time, so it's highly likely (especially since it's somewhat documented) that some developers have been making use of it. Changes to that shouldn't be made on a whim.

I don't think that age of a bug should turn it into a feature.

Just because you don't like the behavior doesn't mean it's a bug.

Don't mistake me, I don't care for what it's doing either, and I also believe the constructor should work normally and anyone wishing to customize the message or code should do so by overriding the constructor. But me not liking the behavior either still doesn't mean it's a bug.

@damianwadley
Copy link
Member

Perhaps this should be added to the 8.4 deprecations/9.0 removal list?

@Chi-teck
Copy link
Author

it's somewhat documented

If you referer to that note on php.net that basically means that one of these parameters does not comply with this documentation. Which one depends on how you interpret the term "default arguments".

Just because you don't like the behavior doesn't mean it's a bug.

It's true. However, it's very confusing and it took me quite a lot time to figure out that this behavior comes from PHP internals not from my custom code.

@morrisonlevi
Copy link
Contributor

I understand your desire to "fix" this, but do you have any issues with the work-around? Formatted for short lines:

class Example extends \Exception
{
    public function __construct(
        string $message = "",
        int $code = 5,
        ?Throwable $previous = null
    ) {
        parent::__construct($message, $code, $previous)
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants