Summary
With opcache enabled, ImmutableHandler::install() reports success but no write is ever blocked. Writing to a property of an immutable object outside a constructor or static method just succeeds — no LogicException, no fatal error, no warning:
$object = new TestObject(['publicProperty' => 100]);
$object->publicProperty = 42; // expected: "Immutable object could be modified only in
// constructor or static methods". Actual: 42, silently.
Reproduced on both supported minors — PHP 8.4.19 + z-engine 8.4.2 and PHP 8.5.9 + z-engine 8.5.0 — with opcache.enable_cli=1. Turning opcache off restores correct behaviour on both. This is not a regression from the recent stable-z-engine upgrade (#14): it reproduces identically against the older 8.4.x-dev/8.5.x-dev lines.
Why the test suite does not catch it
tests/Functional/*.phpt carry no --INI-- section, and CI runs shivammathur/setup-php, whose CLI builds have opcache.enable_cli=Off. So every test runs in exactly the configuration where the handlers work. On a distro build with opcache.enable_cli=On (which is how this was found), 5 of the 8 functional tests fail — every one that asserts a write is rejected — and they fail by producing no output at all, because the exception that should have been raised never happens.
Root cause is upstream
install() hooks ImmutableInterface with setInterfaceGetsImplementedHandler(), and that callback installs create_object, write_property, get_property_ptr_ptr and unset_property on each implementor. Under opcache the callback does run and the create_object handler does take effect — only the property handlers are lost. This is reproducible with plain z-engine calls and no code from this package, so it is tracked upstream as lisachenko/z-engine#238.
Installing the same handlers directly on a named class works fine under opcache; it is specific to installing them from inside the interface hook.
What this package should do about it regardless
Even once the engine side is fixed, a silent no-op is the wrong failure mode for a library whose entire purpose is enforcing a constraint. Worth having:
- A loud failure or warning at
install() time when the environment is one where the handlers will not take effect, so an application cannot come up believing its objects are protected when they are not.
- A regression test that runs with opcache enabled, so the configuration that breaks this is exercised rather than avoided — an
--INI-- block on the functional tests, or a dedicated CI leg with opcache.enable_cli=1.
- A note in the README requirements stating the constraint until the engine side is resolved.
Environment
- PHP 8.4.19 (NTS) and PHP 8.5.9 (NTS), linux-x64
lisachenko/z-engine 8.4.2 / 8.5.0 (also reproduced on 8.4.x-dev / 8.5.x-dev)
ffi.enable=1, opcache.jit=off, opcache.enable_cli=1
Summary
With opcache enabled,
ImmutableHandler::install()reports success but no write is ever blocked. Writing to a property of an immutable object outside a constructor or static method just succeeds — noLogicException, no fatal error, no warning:Reproduced on both supported minors — PHP 8.4.19 + z-engine 8.4.2 and PHP 8.5.9 + z-engine 8.5.0 — with
opcache.enable_cli=1. Turning opcache off restores correct behaviour on both. This is not a regression from the recent stable-z-engine upgrade (#14): it reproduces identically against the older8.4.x-dev/8.5.x-devlines.Why the test suite does not catch it
tests/Functional/*.phptcarry no--INI--section, and CI runsshivammathur/setup-php, whose CLI builds haveopcache.enable_cli=Off. So every test runs in exactly the configuration where the handlers work. On a distro build withopcache.enable_cli=On(which is how this was found), 5 of the 8 functional tests fail — every one that asserts a write is rejected — and they fail by producing no output at all, because the exception that should have been raised never happens.Root cause is upstream
install()hooksImmutableInterfacewithsetInterfaceGetsImplementedHandler(), and that callback installscreate_object,write_property,get_property_ptr_ptrandunset_propertyon each implementor. Under opcache the callback does run and thecreate_objecthandler does take effect — only the property handlers are lost. This is reproducible with plain z-engine calls and no code from this package, so it is tracked upstream as lisachenko/z-engine#238.Installing the same handlers directly on a named class works fine under opcache; it is specific to installing them from inside the interface hook.
What this package should do about it regardless
Even once the engine side is fixed, a silent no-op is the wrong failure mode for a library whose entire purpose is enforcing a constraint. Worth having:
install()time when the environment is one where the handlers will not take effect, so an application cannot come up believing its objects are protected when they are not.--INI--block on the functional tests, or a dedicated CI leg withopcache.enable_cli=1.Environment
lisachenko/z-engine8.4.2 / 8.5.0 (also reproduced on8.4.x-dev/8.5.x-dev)ffi.enable=1,opcache.jit=off,opcache.enable_cli=1