Description
Description
Problem Description
For enums and several internal object types, casting to boolean yields true
, but comparing against true
(with ==
) does not.
This leads to some very confusing inconsistencies, as user "Crapulam" pointed out on Reddit:
enum TestEnum
{
case A;
}
$truthy = true;
var_dump(TestEnum::A == true); // bool(true)
var_dump(TestEnum::A == $truthy); // bool(false)
Demo: https://3v4l.org/OOTcA
What's happening is that the compiler has a special case to treat $foo == true
as (bool)$foo
, so ends up running the object's cast_object
handler instead of the compare
handler. The enum
implementation has inconsistent handler definitions, leading to the wonky result.
Note that this is not unique to enums, but happens for various other objects, e.g. XMLParser
: https://3v4l.org/ZqWRK
Analysis
The base class entry for enums sets the compare
handler to zend_objects_not_comparable
, causing all comparisons to be false
; but it leaves other handlers, including cast_object
at default:
Lines 167 to 169 in be27cbd
That leaves the cast handler as zend_std_cast_object_tostring
, which handles checking for __toString
, but also treats all objects as true
when cast or coerced to boolean:
php-src/Zend/zend_object_handlers.c
Lines 1745 to 1747 in cb9785a
The zend_objects_not_comparable
handler doesn't have any logic at all:
php-src/Zend/zend_object_handlers.c
Lines 1611 to 1614 in cb9785a
That means it skips the normal logic in zend_std_compare_objects
which would call the cast handler when comparing to a non-object:
php-src/Zend/zend_object_handlers.c
Lines 1493 to 1495 in cb9785a
As a final piece of the puzzle, this special case was first added by Nikita in 8.0.3 to fix a regression caused by resource-to-object conversion on sockets: cb9785a It's now used for a bunch of different "opaque" objects.
The specific requirement was that two different sockets should not compare equal with ==
Suggested Fix
Theoretically, we could keep the current comparison, and fix the cast behaviour to match. This would be straight-forward for enums, because they explicitly forbid __toString
, so could just error for all casts. However, this would be a rather visible BC break, because if ( $foo )
is frequently used as a lazy emptiness test.
Probably the safer way around is to fix the comparison, by making the compare
handler for some or all "non-comparable" objects still use the standard casting logic when comparing to non-objects.
My gut feel is that we could just expand the zend_objects_not_comparable
callback to share the "object and non-object" block from the top of zend_std_compare_objects
.
That's still a small compatibility break if someone is relying on the current behaviour for any of the affected objects. Probably safe for 8.5, but not 8.4.x
PHP Version
PHP 8.0.3 onwards
Operating System
No response