[php-src] Issue #20217: Class with property hooks causes ReflectionClass->isIterable() to return true on PHP8.4

From: Date: Sat, 18 Oct 2025 18:05:53 +0000
Subject: [php-src] Issue #20217: Class with property hooks causes ReflectionClass->isIterable() to return true on PHP8.4
Groups: php.bugs 
Request: Send a blank email to php-bugs+get-251084@lists.php.net to get a copy of this message
Issue: https://github.com/php/php-src/issues/20217 Author: lisachenko ### Description The following code: ```php <?php class ClassWithPhp84PropertyHooks { private string $backing = 'default'; public string $name { get => $this->backing; set => $this->backing = strtoupper($value); } } $refClass = new ReflectionClass(ClassWithPhp84PropertyHooks::class); var_dump($refClass->isIterable()); ``` Resulted in this output: ``` true ``` But I expected this output instead: ``` false ``` Copilot analysis: ReflectionClass::isIterable() returns true because the engine considers the class iterable when its internal class entry has a non‑NULL get_iterator handler — and classes that use the new property hooks end up with such a handler set. isIterable() checks ce->get_iterator || instanceof Traversable, so the presence of the get_iterator pointer makes it return true even if the class does not implement Traversable. What the code does (evidence) The ReflectionClass::isIterable implementation (ext/reflection/php_reflection.c) returns false for interfaces/traits/abstracts, otherwise it returns: RETURN_BOOL(ce->get_iterator || instanceof_function(ce, zend_ce_traversable)); i.e. it is true when either the class has a get_iterator handler or it implements Traversable. Why property hooks flip that bit The property-hook implementation in the engine installs/customizes object handlers so that property access and enumeration can be handled by the new hook code. As part of that support the class entry ends up with a get_iterator handler (so the engine can enumerate properties through the new hooks). Because isIterable() only looks at the get_iterator pointer (or Traversable), Reflection reports the class as iterable. Practical consequence ReflectionClass::isIterable() returning true in this case is expected engine behaviour. It does not necessarily mean the class implements Traversable/IteratorAggregate; it means the Zend class entry provides an iterator function for foreach/iteration. You can confirm with reflection checks, e.g.: $r = new ReflectionClass(ClassWithPhp84PropertyHooks::class); var_dump($r->isIterable(), $r->implementsInterface(Traversable::class)); // likely: true, false References ReflectionClass::isIterable implementation: https://github.com/php/php-src/blob/bd82e8d149aa6d9d42cb35dcaca216346f8465a6/ext/reflection/php_reflection.c#L5528-L5640 ### PHP Version ```plain PHP 8.4.13 (cli) (built: Sep 23 2025 14:14:32) (NTS) Copyright (c) The PHP Group Built by Homebrew Zend Engine v4.4.13, Copyright (c) Zend Technologies with Xdebug v3.4.1, Copyright (c) 2002-2025, by Derick Rethans with Zend OPcache v8.4.13, Copyright (c), by Zend Technologies ``` ### Operating System MacOS

« previous php.bugs (#251084) next »