Tutorials Logic, IN info@tutorialslogic.com

PHP Iterators and Generators: Lazy Sequences with Traversable and yield

PHP Lazy Iteration

PHP iterables may be arrays, iterator objects, aggregate-created traversals, or generators. Generators suspend at yield, start work on iteration, can delegate while preserving keys, and often represent one forward pass.

Safe lazy pipelines declare rewind and key behavior, keep resources and transactions inside clear lifetimes, define partial progress, materialize deliberately, honor cancellation, and test early exit, exceptions, duplicate keys, and cleanup.

Iterable Contract

The iterable type accepts arrays and Traversable objects. It communicates that callers may loop over the value without promising random access, a count, or repeated iteration.

Sum Any Iterable

Sum Any Iterable
<?php
function sumValues(iterable $values): int
{
    $total = 0;
    foreach ($values as $value) {
        $total += $value;
    }
    return $total;
}

echo sumValues([4, 8, 15]);
Output
27

Generator Flow

Calling a generator function returns a Generator object without running the full body. Execution advances as foreach requests values and pauses at each yield.

Generate a Page Range

Generate a Page Range
<?php
function pages(int $last): Generator
{
    for ($page = 1; $page <= $last; $page++) {
        yield $page;
    }
}

foreach (pages(3) as $page) {
    echo "Page {$page}" . PHP_EOL;
}
Output
Page 1
Page 2
Page 3

Only the current loop state is needed; no three-item array is constructed.

Keys and Delegation

yield may provide a key, and yield from delegates to another iterable. Duplicate keys remain possible, so consumers that convert the result to an array must decide whether key replacement is acceptable.

Yield Record Keys

Yield Record Keys
<?php
function indexedTitles(array $records): Generator
{
    foreach ($records as $record) {
        yield $record['id'] => $record['title'];
    }
}

$records = [['id' => 10, 'title' => 'PHP'], ['id' => 20, 'title' => 'SQL']];
foreach (indexedTitles($records) as $id => $title) {
    echo "{$id}:{$title}" . PHP_EOL;
}
Output
10:PHP
20:SQL

Reusable Collection

Implement IteratorAggregate when an object owns collection behavior and can create a fresh iterator for each traversal. Return Traversable from getIterator().

Course Collection

Course Collection
<?php
final class CourseCollection implements IteratorAggregate
{
    public function __construct(private array $courses) {}

    public function getIterator(): Traversable
    {
        yield from $this->courses;
    }
}

foreach (new CourseCollection(['PHP', 'SQL']) as $course) {
    echo $course . PHP_EOL;
}
Output
PHP
SQL

Lazy Boundaries

  • A generator is normally forward-only and cannot be rewound after it has advanced.
  • iterator_to_array() consumes and materializes the remaining sequence.
  • Keep database and file resources open only as long as iteration needs them; use finally for cleanup.
  • Use an array when the collection is small and needs sorting, counting, random access, or repeated traversal.

Iterable Contracts

`iterable` accepts arrays and Traversable objects, but it does not promise countability, random access, rewindability, or repeated traversal. Document those properties wherever a consumer depends on them.

Use an array for a small materialized collection, an iterator for stateful traversal behavior, IteratorAggregate when an object can create a traversable sequence, and a generator for concise forward lazy production.

A lazy sequence postpones work until iteration requests values. Calling a generator function returns a Generator object; code before the first yield does not run merely because the function was called.

Laziness can reduce peak memory but extends the lifetime of database cursors, files, transactions, and remote pages. The consumer must finish or explicitly abandon work under a cleanup policy.

Keep the element type and failure model explicit. An iterator that sometimes yields error arrays mixes control and data; prefer exceptions or a typed result policy.

  • Document one-pass and rewind behavior.
  • Choose materialized or lazy ownership deliberately.
  • Remember generator execution starts on iteration.
  • Account for extended resource lifetime.
  • Keep yielded element types coherent.

Iterator Protocol

The Iterator protocol exposes `rewind`, `valid`, `current`, `key`, and `next`. PHP foreach coordinates those calls, but a custom iterator must keep key, value, and validity state consistent through empty, first, final, and exhausted positions.

Do not perform expensive remote work repeatedly in `valid` or `current` because consumers may call them more than once. Advance work in one controlled step and cache the current element.

Rewind may be impossible for a network stream or cursor. Reject a second traversal clearly or expose an IteratorAggregate that creates a fresh independent source when that is actually supported.

IteratorAggregate keeps collection ownership separate from traversal state through `getIterator`. Returning a fresh generator often produces a simpler reusable collection than implementing all Iterator methods on the domain object.

Concurrent mutation of the underlying collection needs a policy: snapshot, fail fast, observe changes, or forbid mutation. Never leave it to incidental array or database behavior.

  • Maintain protocol state consistently.
  • Avoid repeated work in current and valid.
  • State rewind support explicitly.
  • Prefer IteratorAggregate for fresh traversals.
  • Define concurrent mutation behavior.

Yield Semantics

`yield` emits a value and suspends the function with its local state preserved. A yielded key uses `yield $key => $value`; omitted keys advance integer keys according to generator behavior.

A generator may return a final value retrievable with `getReturn` only after completion. Do not hide essential processing results there when normal foreach consumers are unlikely to inspect them.

`yield from` delegates to an array or Traversable and preserves its keys. When materialized with `iterator_to_array`, duplicate keys overwrite earlier values unless key preservation is disabled intentionally.

Generators can receive values or exceptions through advanced APIs, but ordinary one-way iteration is easier to understand. Use a dedicated state machine or async abstraction when bidirectional control becomes the main design.

Yielding by reference creates difficult aliasing and should be reserved for a proven mutation contract. Ordinary value yields are safer for pipelines.

  • Define yielded keys as part of the contract.
  • Keep essential results out of obscure generator returns.
  • Handle delegated duplicate keys intentionally.
  • Prefer one-way iteration.
  • Avoid reference yields without clear ownership.

Resource Cleanup

A generator that opens a file, cursor, or lock should wrap production in try/finally so cleanup occurs when iteration completes, throws, or the generator is closed. Keep resource acquisition as close as possible to that ownership block.

Breaking from foreach leaves no guarantee that unrelated references to the generator disappear immediately. Do not keep abandoned generators in long-lived properties or caches; provide an explicit closeable abstraction when prompt release is essential.

Avoid holding a database transaction across yields to arbitrary caller code. The caller may pause, perform slow work, or never finish. Fetch bounded pages or keep transaction ownership inside one eager operation.

Errors can occur after several values were already consumed. Consumers must define whether partial progress is acceptable, transactional, resumable, or deduplicated before retrying the sequence.

Cancellation and worker shutdown should stop further production and release resources. Check signals or deadlines at safe boundaries in long streams.

  • Use finally around owned resources.
  • Release abandoned generators promptly.
  • Keep transactions out of caller-controlled pauses.
  • Define partial-progress recovery.
  • Honor cancellation in long sequences.

Lazy Pipelines

Compose mapping, filtering, batching, and validation as lazy stages when each stage can process one value independently. Keep stage names and types clear so the pipeline remains debuggable.

A pipeline saves memory only if no stage calls `iterator_to_array`, sorts globally, groups everything, or retains every prior item. Materialize deliberately at the point an operation genuinely requires the full set.

Repeated membership searches can still make a lazy pipeline quadratic. Build a bounded lookup map or move joins and filtering into the database when that source can perform them efficiently.

Backpressure means the producer advances only as the consumer requests values, but external prefetching libraries may buffer. Set page size, queue limits, and response bytes independently of PHP generator syntax.

Do not expose a lazy source past the lifetime of its owning request, unit of work, or connection. Convert to stable values before crossing that boundary.

  • Compose stages with clear element contracts.
  • Materialize only for full-set operations.
  • Avoid hidden quadratic searches.
  • Bound external buffering and page sizes.
  • Keep lazy sources inside owner lifetimes.

Iteration Tests

Test empty, one-value, multiple-value, duplicate-key, exception-before-first-yield, exception-midstream, early break, complete consumption, second traversal, and cancellation behavior. Assert resource closure in every path.

Use a source double that counts fetches to prove laziness and page boundaries. Confirm that creating the generator performs no work when that is the contract and that requesting one value does not consume the complete source.

Test materialization with and without preserved keys. A count assertion alone misses overwritten duplicate-key values and sparse-key JSON shape changes.

Measure peak memory and latency with realistic records. Generators reduce collection allocation but cannot fix large retained payloads, slow per-item calls, or unbounded consumer output.

Static analysis annotations can state key and element types for Traversable and Generator. Combine them with runtime validation at external data sources.

Place exception translation at the consumer boundary that knows whether partial output was committed. A producer should add source context and preserve the previous Throwable, while the consumer decides whether to retry, discard, checkpoint, or return a partial result.

  • Cover early exit and midstream failure.
  • Assert lazy fetch counts.
  • Verify materialized key behavior.
  • Measure complete pipeline memory.
  • Describe key and element types statically.
Before you move on

Mastery Check

5 checks
  • Document element, key, rewind, and failure contracts.
  • Keep lazy work inside resource ownership boundaries.
  • Handle yield-from and materialized key collisions.
  • Bound pages, buffers, output, and partial progress.
  • Assert laziness and cleanup on every exit path.

Trace the Sequence

0 of 2 checked

Q1. When does a generator body begin executing?

Q2. What does iterator_to_array() change?

Iteration Boundary

  • Assuming a generator rewinds

    A consumed generator cannot be reused like an array. Create a new generator for another pass or materialize values when repeat traversal is required.

Try this next

Stream a Data Source

0 of 2 completed

  1. Yield one parsed row at a time and close the handle in finally when iteration ends.
  2. Describe which values exist simultaneously for a 100,000-row array and a generator pipeline.
Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.