Summary
ReflectionValue on the 8.4 branch still declares the pre-8.1 fake-type ids:
// src/Reflection/ReflectionValue.php:135-136
public const _IS_BOOL = 17;
public const _IS_NUMBER = 18;
PHP 8.1 inserted IS_NEVER = 17 into the type table (Zend/zend_types.h), shifting the cast-only ids to:
#define IS_NEVER 17
#define _IS_BOOL 18
#define _IS_NUMBER 19
So on PHP 8.4 the engine passes 18 to a cast_object handler for a (bool) cast, which a userland __cast implementation written against ReflectionValue::_IS_BOOL (17) never matches — and worse, ReflectionValue::_IS_NUMBER (18) does match it, so boolean casts get routed into the numeric branch. IS_NEVER is also missing from the constant table entirely.
Reproduction (PHP 8.4.19 NTS, 8.4 branch, ffi.enable=1, opcache.jit=off)
final class Probe implements ObjectCreateInterface, ObjectCastInterface {
use ObjectCreateTrait;
public static function __cast(CastObjectHook $hook): mixed {
error_log('cast type = ' . $hook->getCastType());
return true;
}
}
(new ReflectionClass(Probe::class))->installExtensionHandlers();
$b = (bool) new Probe(); // logs "cast type = 18", not ReflectionValue::_IS_BOOL (17)
ReflectionValue::name(18) consequently reports _IS_NUMBER for what is actually a boolean cast.
Expected
_IS_BOOL = 18, _IS_NUMBER = 19, and IS_NEVER = 17 added, matching the PHP 8.1+ zend_types.h layout that this branch's FFI definitions otherwise target. The tests/Stub/NativeNumber.php stub and ReflectionClassTest::testInstallCastObjectHandler() dispatch on these constants too, so they currently exercise the wrong branches ((int)/(float) casts appear to pass because IS_LONG/IS_DOUBLE were unaffected by the shift, but the _IS_BOOL case in the test handler is unreachable).
Context
Found while implementing casts in lisachenko/native-php-matrix (lisachenko/native-php-matrix#17); that library currently works around it with a local ENGINE_IS_BOOL = 18 constant.
Summary
ReflectionValueon the8.4branch still declares the pre-8.1 fake-type ids:PHP 8.1 inserted
IS_NEVER = 17into the type table (Zend/zend_types.h), shifting the cast-only ids to:So on PHP 8.4 the engine passes 18 to a
cast_objecthandler for a(bool)cast, which a userland__castimplementation written againstReflectionValue::_IS_BOOL(17) never matches — and worse,ReflectionValue::_IS_NUMBER(18) does match it, so boolean casts get routed into the numeric branch.IS_NEVERis also missing from the constant table entirely.Reproduction (PHP 8.4.19 NTS,
8.4branch,ffi.enable=1,opcache.jit=off)ReflectionValue::name(18)consequently reports_IS_NUMBERfor what is actually a boolean cast.Expected
_IS_BOOL = 18,_IS_NUMBER = 19, andIS_NEVER = 17added, matching the PHP 8.1+zend_types.hlayout that this branch's FFI definitions otherwise target. Thetests/Stub/NativeNumber.phpstub andReflectionClassTest::testInstallCastObjectHandler()dispatch on these constants too, so they currently exercise the wrong branches ((int)/(float)casts appear to pass becauseIS_LONG/IS_DOUBLEwere unaffected by the shift, but the_IS_BOOLcase in the test handler is unreachable).Context
Found while implementing casts in
lisachenko/native-php-matrix(lisachenko/native-php-matrix#17); that library currently works around it with a localENGINE_IS_BOOL = 18constant.