Phase 3 · Object-Oriented & Modern PHPModule 20~46 min read

Closures, Iterators & Generators

Pass behavior as data and stream large sequences lazily with callables, iterators, and generators.

What you'll learn

Treat behavior as data with callables and process large sequences without loading them all into memory. Closures customize operations; generators produce one item at a time with a remarkably small API.

By the end of this lesson, you'll be able to:

  • Capture values safely in closures
  • Accept first-class callables
  • Stream large inputs with generators

Core mental model

Professional PHP is less about memorizing APIs and more about choosing a clear boundary for each responsibility. Use this table as a decision guide while reading the examples.

ConceptWhat it protectsDecision rule
ClosureConfigurable behaviorPass a small operation into a general algorithm.
IterableUniform sequence contractAccept it when arrays and lazy sources should both work.
GeneratorBounded memoryYield items when the full collection is unnecessary.

Professional workflow

Build the feature in small, verifiable steps. Each step leaves the system in a state you can test.

  1. Describe the lazy data pipeline boundary: its inputs, outputs, invariants, and expected failures.
  2. Implement the smallest happy path behind an explicit contract.
  3. Add validation and translate low-level failures into language the caller understands.
  4. Exercise the boundary with realistic data, then inspect output, logs, and resource cleanup.
  5. Refactor only after behavior is protected by a repeatable check.

Make the boundary visible

Name inputs, outputs, side effects, and failure cases before adding framework or infrastructure code. That habit keeps advanced PHP understandable as the application grows.

Guided code lab

Build a configurable pipeline

Arrow functions capture by value and keep short transformations readable.

prices.php
<?php
$taxRate = 0.11;
$prices = [1200, 800, 2500, 400];

$billable = array_filter($prices, fn (int $cents): bool => $cents >= 800);
$withTax = array_map(
    fn (int $cents): int => (int) round($cents * (1 + $taxRate)),
    $billable,
);

print_r($withTax);

Stream a CSV file

The file remains open only while iteration is active; each row is yielded as needed.

rows.php
<?php
/** @return Generator<int, array<int, string>> */
function rows(string $path): Generator
{
    $file = new SplFileObject($path);
    $file->setFlags(SplFileObject::READ_CSV | SplFileObject::SKIP_EMPTY);
    foreach ($file as $row) {
        if (is_array($row)) yield $row;
    }
}

foreach (rows('students.csv') as $row) {
    echo $row[0] . PHP_EOL;
}

Production practice

Contract

Document callable signatures and iterable key/value types so tools can verify the pipeline.

Verification

Test empty, one-item, and early-termination cases; assert generators do not eagerly consume the source.

Operations

Stream network or file sources with explicit timeouts and ensure resources close when iteration stops.

Common failure mode

Converting a generator to an array erases its memory advantage. Keep the pipeline lazy until a concrete collection is truly needed.

Independent workshop

Create a lazy log processor that filters a date range and yields aggregated error counts.

Your finished workshop must include:

  • Generator reading one line at a time
  • Injected parsing and filtering callables
  • A memory comparison against file()

Definition of done

Run the happy path and at least two failure paths, explain one design tradeoff in a short README, and leave the code formatted and ready for review.

Recap & quick check

Key takeaways

  • Closures pass behavior into algorithms.
  • Arrow functions capture outer values by value.
  • iterable accepts arrays and Traversable objects.
  • Generators make streaming code read like a loop.

Quick check

1. Which keyword produces a generator value?

2. Why accept iterable?

3. What can destroy lazy memory savings?

Keep the workshop: later phases deliberately build on these boundaries, so today's small example can become part of your portfolio architecture.