Skip to content
16

Iterators & Generators

TypePHP provides lazy runtime validation for Traversable objects, Iterator instances, and PHP Generator functions, validating yielded keys, values, and generator inputs ($gen->send()) on-the-fly during iteration.


How Lazy Iteration Works (IterableWrapper & IteratorProxy)

When an iterable or generator is passed into a function accepting Traversable<K, V> or returned from a function:

  1. Zero Memory Spikes: TypePHP does not convert the iterator to an array or load items eagerly into RAM.
  2. On-the-Fly Validation: Keys (K) and values (V) are validated lazily during iteration at the exact moment each item is accessed inside current() or yield.
  3. Rewindability Preserved: IteratorProxy unwraps and preserves iterator rewindability, allowing multiple foreach loops over the same wrapped iterator without crashing.
  4. Method & Countable Forwarding: Forwards Countable::count() and custom iterator methods directly to the inner iterator using __call().

Generic Streams vs. Concrete Collection Classes (Zero Proxy Overhead)

A critical architectural design in TypePHP is distinguishing between abstract generic streams and concrete collection classes:

1. Abstract Generic Streams (iterable<T>, Traversable<K, V>, Generator<K, V>)

When a method or parameter specifies an abstract stream keyword with inner type constraints:

php
/**
 * Abstract stream contract -> Wrapped in IteratorProxy
 *
 * @param Traversable<string, positive-int> $scores
 */
public function processScores(Traversable $scores): void

Because Traversable is a general interface with no methods of its own, TypePHP wraps it in a lazy IteratorProxy to validate yielded items and keys during iteration.

2. Concrete Collection Classes (FileCollection, ArrayCollection, OrderList)

When a method parameter, property, or return value is a concrete class (even if that class implements \IteratorAggregate or \Traversable):

php
class StorefrontConfig
{
    // Native PHP class type hint
    protected FileCollection $styleFiles;

    public function setStyleFiles(FileCollection $styleFiles): void
    {
        $this->styleFiles = $styleFiles; // Preserved as raw FileCollection!
    }
}

TypePHP leaves concrete collections 100% unwrapped as raw PHP objects:

  • Preserves Native PHP Nominal Type Hints: Prevents fatal PHP engine errors (e.g. Cannot assign IteratorProxy to property ...::$styleFiles of type FileCollection).
  • Preserves Custom Domain Methods & State: Custom business methods (like $files->getPublicUrls() or $files->filterByExtension()) and private properties remain directly accessible.
  • Direct \WeakMap Enforcement: For generic concrete classes (like ArrayCollection<int, Animal>), TypePHP tracks generic template bindings directly in \WeakMap memory, running validation rules inside $collection->add() and $collection->set() without needing any wrapper proxy!
  • Zero Allocation Overhead: Eliminates proxy object allocations and garbage collection pressure, allowing concrete collections to run at native C-level speed.

Summary: When TypePHP Wraps vs. Leaves Unwrapped

Type AnnotationObject PassedAction TakenWhy
Traversable<string, positive-int>Any Iterator / GeneratorWrapped in IteratorProxyAbstract stream; needs lazy item validation during iteration.
iterable<User>Any Array / TraversableWrapped in IteratorProxyAbstract stream; validates items on-the-fly.
FileCollectionnew FileCollection()Unwrapped (Raw Object)Concrete class; preserves native PHP type hints and domain methods.
ArrayCollection<int, Animal>new ArrayCollection()Unwrapped (Raw Object)Generic state is tracked directly via \WeakMap inside add()/set().
Un-annotated (mixed $items)Any IteratorUnwrapped (Raw Object)No type contracts to enforce; zero overhead.

Traversable & Iterator Contracts (Traversable<K, V>)

Validate keys and values on any Traversable or ArrayIterator instance:

php
<?php

declare(strict_types=1);

/**
 * @param Traversable<non-empty-string, positive-int> $items
 */
function processTraversable(Traversable $items): array
{
    $results = [];
    foreach ($items as $key => $value) {
        $results[$key] = $value;
    }

    return $results;
}

// 1. Valid Call
$iterator = new ArrayIterator(['item1' => 10, 'item2' => 20]);
processTraversable($iterator);

// 2. Invalid Call (Value -50 violates positive-int)
$badIterator = new ArrayIterator(['item1' => 10, 'item2' => -50]);
processTraversable($badIterator);
// Throws: TypeError: Iterator $items value['item2'] must be of type positive-int, negative int (-50) given

// 3. Invalid Call (Empty string violates non-empty-string key)
$badKeyIterator = new ArrayIterator(['' => 10]);
processTraversable($badKeyIterator);
// Throws: TypeError: Iterator $items key must be of type non-empty-string, empty string ('') given

Generic Iterables with Template Substitution (@template T)

When a function accepts generic iterables (iterable<T> or Traversable<string, T>), TypePHP dynamically substitutes T with the bound generic type and lazily validates items during iteration:

Deep Dive Guide: For comprehensive details on template bounds, covariance/contravariance, and runtime generic state inspection, see the Generics Basics & Bounds documentation.

php
use App\Models\Animal;
use App\Models\Dog;
use App\Models\Car;

/**
 * Generic stream processor where T is inferred from $sample
 *
 * @template T
 *
 * @param iterable<T> $stream
 * @param T $sample
 *
 * @return list<T>
 */
function collectStream(iterable $stream, mixed $sample): array
{
    $collected = [];
    foreach ($stream as $item) {
        $collected[] = $item;
    }

    return $collected;
}

// 1. Valid Call: Infers T = int, validates all items against int
$intIterator = new ArrayIterator([10, 20, 30]);
collectStream($intIterator, 1); // Returns [10, 20, 30]

// 2. Invalid Call: T is inferred as int (from 1), but iterator yields string ('invalid')
$badIterator = new ArrayIterator([10, 'invalid', 30]);
collectStream($badIterator, 1);
// Throws: TypeError: Iterator $stream value must be of type int, string 'invalid' given

Generic Traversables with Class Bounds (@template T of Animal)

php
/**
 * @template T of Animal
 *
 * @param Traversable<non-empty-string, T> $stream
 *
 * @return list<T>
 */
function collectAnimalStream(Traversable $stream): array
{
    $collected = [];
    foreach ($stream as $key => $animal) {
        $collected[] = $animal;
    }

    return $collected;
}

// Valid Call
collectAnimalStream(new ArrayIterator(['dog1' => new Dog()]));

// Invalid Call (Car is not an Animal)
collectAnimalStream(new ArrayIterator(['car1' => new Car()]));
// Throws: TypeError: Iterator $stream value must be of type App\Models\Animal, App\Models\Car given

Generator Function Contracts (Generator<TKey, TValue, TSend, TReturn>)

PHP Generator functions allow declaring up to 4 generic parameters:

  • TKey: Type of yielded keys (yield $key => $val).
  • TValue: Type of yielded values (yield $val).
  • TSend: Type of values sent into the generator via $gen->send($val).
  • TReturn: Type of value returned when the generator completes (return $val).

Yielded Key and Value Validation (TKey & TValue)

php
/**
 * @return Generator<non-empty-string, positive-int>
 */
function generateScores(): Generator
{
    yield 'alice' => 100; // Valid
    yield 'bob' => -50;   // Invalid: -50 violates positive-int!
}

$gen = generateScores();

foreach ($gen as $name => $score) {
    // Throws lazily on second yield:
    // TypeError: Return iterator value must be of type positive-int, negative int (-50) given
}

Generic Generators Yielding Template T (Generator<int, T>)

Generators seamlessly support generic template substitution for yielded values:

php
/**
 * @template T
 *
 * @param T $item
 * @param positive-int $count
 *
 * @return Generator<int, T>
 */
function streamItem(mixed $item, int $count): Generator
{
    for ($i = 0; $i < $count; $i++) {
        yield $i => $item;
    }
}

// Infers T = int, yields int values
$gen = streamItem(100, 3);
foreach ($gen as $k => $v) {
    // [0 => 100, 1 => 100, 2 => 100]
}

Generator Input Validation ($gen->send() / TSend)

TypePHP validates values sent into an interactive generator via $gen->send() against the declared TSend parameter:

php
/**
 * TKey = int, TValue = string, TSend = positive-int, TReturn = void
 *
 * @return Generator<int, string, positive-int, void>
 */
function processInteractiveGenerator(): Generator
{
    $receivedInput = yield 1 => 'first_value';
    yield 2 => "processed: {$receivedInput}";
}

$gen = processInteractiveGenerator();
$gen->current(); // Advances to first yield

// 1. Valid Send (100 satisfies TSend = positive-int)
$gen->send(100);

// 2. Invalid Send (-500 violates TSend = positive-int)
$gen = processInteractiveGenerator();
$gen->current();

$gen->send(-500);
// Throws: TypeError: processInteractiveGenerator(): Generator sent value (TSend) must be of type positive-int, negative int (-500) given

Generic Interactive Generators (Generator<int, T, T, void>)

When TSend uses a generic template T, $gen->send() is dynamically validated against the bound generic type:

php
/**
 * @template T
 *
 * @param T $initial
 *
 * @return Generator<int, T, T, void>
 */
function streamInteractive(mixed $initial): Generator
{
    $current = $initial;
    for ($i = 0; $i < 3; $i++) {
        $input = yield $i => $current;
        if ($input !== null) {
            $current = $input;
        }
    }
}

// Initial value 10 locks T = int
$gen = streamInteractive(10);
$gen->current();

$gen->send(20); // Valid (20 is int)

$gen->send('invalid'); // Invalid: string violates T = int!
// Throws: TypeError: streamInteractive(): Generator sent value (TSend) must be of type int, string 'invalid' given

Delegated Generators (yield from)

TypePHP seamlessly intercepts delegated yield from expressions, lazily validating keys and values yielded from nested iterators or arrays:

php
/**
 * @return Generator<string, positive-int>
 */
function parentGenerator(): Generator
{
    yield from ['a' => 10, 'b' => 20]; // Valid
    yield from ['c' => -99];           // Invalid: -99 violates positive-int
}

foreach (parentGenerator() as $key => $val) {
    // Throws lazily on 'c' => -99:
    // TypeError: Return iterator value must be of type positive-int
}

Complex Yield & Send Types (Array Shapes, Generics & Lists)

Because GeneratorChecker delegates key, value, and TSend validation directly to TypePHP's central validator engine, all complex types (array shapes, lists, generic objects, unions) are fully enforced inside generator signatures:

php
use App\Generics\Producer;
use App\Models\Dog;

/**
 * Generator yielding Array Shapes and accepting Array Shapes in $gen->send()
 *
 * @return Generator<int, array{id: positive-int, username: non-empty-string}, array{action: 'approve'|'reject'}, void>
 */
function processComplexGenerator(): Generator
{
    $input = yield 1 => ['id' => 10, 'username' => 'Alice'];
    
    // $input is validated against TSend shape array{action: 'approve'|'reject'} when sent!
}

$gen = processComplexGenerator();
$firstItem = $gen->current(); // Returns ['id' => 10, 'username' => 'Alice']

// 1. Valid Send
$gen->send(['action' => 'approve']);

// 2. Invalid Send ('action' => 'delete' violates 'approve'|'reject')
$gen = processComplexGenerator();
$gen->current();

$gen->send(['action' => 'delete']);
// Throws: TypeError: processComplexGenerator(): Generator sent value (TSend)['action'] must be of type ('approve' | 'reject')

Multi-Level IteratorAggregate Unwrapping & Method Forwarding

When passing custom classes implementing IteratorAggregate, IteratorProxy recursively unwraps the inner iterator while preserving method forwarding and Countable support:

php
class NestedCollection implements IteratorAggregate, Countable
{
    public function __construct(private array $items = ['a' => 10, 'b' => 20]) {}

    public function getIterator(): Traversable
    {
        return new ArrayIterator($this->items);
    }

    public function count(): int
    {
        return count($this->items);
    }

    public function getCustomMetadata(): string
    {
        return 'custom_metadata';
    }
}

/**
 * @param Traversable<string, positive-int> $collection
 */
function processCollection(Traversable $collection): void
{
    // 1. Iteration validates on-the-fly
    foreach ($collection as $k => $v) { ... }

    // 2. Countable::count() is forwarded
    echo count($collection); // 2

    // 3. Custom methods forwarded via __call
    echo $collection->getCustomMetadata(); // 'custom_metadata'
}

processCollection(new NestedCollection());