sebastianbergmann/phpunit · error · PHPUnit\Framework\Exception
IteratorAggregate::getIterator() returned an object that was
Error message
IteratorAggregate::getIterator() returned an object that was already seen
What it means
PHPUnit's Count constraint (used by assertCount()) counts Traversables by unwrapping IteratorAggregate::getIterator() chains until it reaches a real Iterator. Every aggregate visited is recorded in a RecursionContext; if a getIterator() call returns an aggregate that was already seen, the chain is cyclic and would loop forever, so PHPUnit aborts with this exception. It is a defect in the object under test's iterator wiring, not in the assertion itself.
Source
Thrown at src/Framework/Constraint/Cardinality/Count.php:91
/**
* @throws Exception
*/
protected function getCountOf(mixed $other): ?int
{
if (is_countable($other)) {
return count($other);
}
if ($other instanceof EmptyIterator) {
return 0;
}
if ($other instanceof Traversable) {
$context = new Context;
while ($other instanceof IteratorAggregate) {
if ($context->contains($other) !== false) {
throw new Exception('IteratorAggregate::getIterator() returned an object that was already seen');
}
$context->add($other);
try {
$other = $other->getIterator();
} catch (\Exception $e) {
throw new Exception(
$e->getMessage(),
$e->getCode(),
$e,
);
}
}
$iterator = $other;
if ($iterator instanceof Generator) {View on GitHub (pinned to f123cdb2a2)
Solutions
- Make getIterator() return a fresh Iterator instance (e.g. new ArrayIterator($this->items)) or the inner iterator itself, never $this or an ancestor aggregate in the chain
- If the class is itself an Iterator, remove the IteratorAggregate implementation so PHPUnit counts it directly without unwrapping
- For a decorator, return the decorated object's iterator: `return $this->inner->getIterator();` or `return $this->inner instanceof Iterator ? $this->inner : $this->inner->getIterator();`
- As a workaround in the test, count a materialized copy: `assertCount($n, iterator_to_array($object));` (only safe when the chain terminates for iterator_to_array)
Example fix
// before
class Playlist implements IteratorAggregate, Iterator
{
public function getIterator(): Iterator { return $this; } // cycle: itself
// ...
}
assertCount(3, new Playlist);
// after
class Playlist implements IteratorAggregate
{
public function getIterator(): ArrayIterator { return new ArrayIterator($this->tracks); }
}
assertCount(3, new Playlist); Defensive patterns
Strategy: validation
Validate before calling
// Before assertCount() on a suspect aggregate, verify the chain terminates:
function terminates(IteratorAggregate $root): bool
{
$seen = [];
$node = $root;
while ($node instanceof IteratorAggregate) {
if (in_array($node, $seen, true)) {
return false; // cycle: PHPUnit would abort
}
$seen[] = $node;
$node = $node->getIterator();
}
return true;
}
if (!terminates($playlist)) {
$this->addWarning('Playlist::getIterator() is cyclic; fix the production code.');
}
assertCount(3, $playlist); Type guard
function isNonCyclicIteratorAggregate(IteratorAggregate $root): bool
{
$seen = [];
$node = $root;
while ($node instanceof IteratorAggregate) {
if (in_array($node, $seen, true)) {
return false;
}
$seen[] = $node;
$node = $node->getIterator();
}
return true;
} Try / catch
try {
assertCount(3, $traversable);
} catch (\PHPUnit\Framework\Exception $e) {
if (str_contains($e->getMessage(), 'already seen')) {
$this->markTestIncomplete('Cyclic IteratorAggregate in SUT: ' . $e->getMessage());
}
throw $e;
} Prevention
- Never implement getIterator() as `return $this;` — if the class is already an Iterator, drop IteratorAggregate
- In decorators, return $this->inner->getIterator(), not $this->inner
- Static analysis rule: flag methods returning $this from getIterator()
When it happens
Trigger: Calling assertCount($n, $traversable) (or any assertion that evaluates a Count constraint, including assertThat with Count) on an object whose getIterator() returns $this or returns another IteratorAggregate that eventually leads back to an already-visited aggregate. Typical: a class implements both IteratorAggregate and Iterator and does `return $this;`, or a decorator's getIterator() returns the decorated aggregate instead of its iterator.
Common situations: Lazy-collection or repository classes that implement both Iterator and IteratorAggregate; wrappers/decorators added around a collection during refactoring; entities converted to iterables where getIterator() was stubbed with `return $this;` to satisfy an interface; rarely, ORM/Collection libraries (Doctrine Collections, Laravel collections) wrapped in custom adapters.
Related errors
- Subscriber "%s" does not implement any known interface - did
- Unknown event type "%s"
- Subscriber "%s" does not implement a known interface
- Subscriber "%s" does not exist or is not an interface
- Event class "%s" does not exist
AI-assisted analysis of sebastianbergmann/phpunit@f123cdb2a2 (2026-08-23).
Data as JSON: /api/errors/b048eaed6adae2bb.
Report an issue: GitHub.