[php-src] Issue #20217: Class with property hooks causes ReflectionClass->isIterable() to return true on PHP8.4
| From: | lisachenko | 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