Course outline

PHP / Advanced: reliable models and runtime evidence / Lesson 14

Generators, streams, and resource lifetime

Consume bounded data incrementally and pair resource acquisition with release. Distinguish a lazy sequence from a reusable collection and validate stream behavior using owned memory only.

About 4 min readContent reviewed
On this page

Lesson goals

  • Trace generator suspension and consumption.
  • Handle stream failures and close owned resources.
  • Explain the limits of lazy-memory claims.

A generator produces values when requested

A function containing yield returns a Generator rather than an eagerly built array. Execution advances as the consumer requests values, preserving local state between yields. This can avoid allocating an entire output list, but it does not guarantee low total memory if the source is already fully loaded or the consumer immediately materializes everything. A generator is stateful and typically single-pass; repeated traversal needs a new generator invocation or a deliberately repeatable abstraction. State the input bound and who owns consumption before advertising streaming as an optimization. Laziness changes timing and ownership as well as allocation.

Consume a bounded sequence deliberately

The example yields three page numbers and sums them without creating an intermediate array of page values. The final output is deterministic and independent of a database or file. iterator_to_array would deliberately materialize the sequence instead, which can be appropriate for small data but changes the memory contract. Do not reuse an exhausted Generator and expect it to restart like a new array. A fresh call to pages creates a new iteration state. Test empty input, one item, and an agreed maximum, and define whether an invalid limit rejects or represents an empty sequence.

php Incremental aggregation and intentional materialization
<?php
declare(strict_types=1);
function pages(int $limit): Generator {
    for ($page = 1; $page <= $limit; $page++) { yield $page; }
}
$total = 0;
foreach (pages(3) as $page) { $total += $page; }
echo $total, PHP_EOL;
echo implode(",", iterator_to_array(pages(3))), PHP_EOL;
Expected output
6
1,2,3

The first consumer keeps only a total; the second explicitly builds an array from a fresh generator.

A stream operation can fail before useful data exists

fopen and related APIs have documented failure outcomes; check them before reading or writing. A successful open does not prove every later write is complete or every read produced a full application record. Protocol framing, line endings, maximum record size, and encoding remain separate responsibilities. Never turn user input into an unrestricted stream wrapper or filesystem path. The local example uses php://memory, so it cannot open a configured service or application file. Real file and network work needs a narrow path or endpoint policy and error handling appropriate to that resource.

Close what this scope opened

A try/finally scope makes cleanup visible. The example writes two tiny synthetic lines, rewinds, reads them, and closes the memory stream even if an exception is introduced in the protected section. The output checks the parsed line values rather than an unpredictable resource identifier. For a generator that owns a stream, cleanup timing depends on the generatorโ€™s lifecycle and references; a consumer breaking early does not justify an assumption that every referenced generator has already been destroyed. Prefer clear ownership and explicit lifetime boundaries when prompt release is necessary. Do not rely on request shutdown for long-lived worker processes.

php An owned memory stream with explicit cleanup
<?php
declare(strict_types=1);
$stream = fopen("php://memory", "w+");
if ($stream === false) { throw new RuntimeException("memory stream unavailable"); }
try {
    fwrite($stream, "PHP
PDO
");
    rewind($stream);
    $lines = [];
    while (($line = fgets($stream)) !== false) { $lines[] = rtrim($line, "
"); }
    echo implode(",", $lines), PHP_EOL;
} finally { fclose($stream); }
Expected output
PHP,PDO

No real path or service is accessed. rtrim removes only the declared line-ending bytes, preserving other spaces.

Streaming needs backpressure and bounded records

Incremental production is useful only if consumers do not queue unbounded work or retain every item. Limit batches, record size, and downstream concurrency according to the actual task. A database cursor may still buffer results depending on driver settings, so a foreach loop alone is not proof of streaming from the server. Measure representative memory and throughput in a controlled environment and keep correctness checks active. If an export fails partway, define whether the output is discarded, marked incomplete, or resumed from a stable checkpoint. A partially emitted response cannot always be replaced by a clean error document.

Common errors and fixes

A generator is advertised as automatically constant-memory

Why it happens
Source buffering and consumer retention are ignored.
How to fix it
Inspect both ends and measure bounded representative cases.
Prevent it next time
Document materialization and batch limits.

A failed open result is used as a resource

Why it happens
The API failure contract is unchecked.
How to fix it
Check the returned value before reading or writing.
Prevent it next time
Test acquisition failure separately from processing failure.

A long-lived worker relies on request shutdown for cleanup

Why it happens
Resource lifetime is assumed from a short-request model.
How to fix it
Release resources at explicit owned boundaries.
Prevent it next time
Test repeated operation and early termination paths.

Check your understanding

1. Does iterator_to_array preserve the memory advantage of a huge generator?

Show worked solution

It materializes the yielded values, so retained memory grows with the output. Use incremental aggregation or bounded batches when the task permits. A generator still helps express production, but the consumer determines whether all values remain in memory.

2. Why remove only CR and LF rather than trim every line?

Show worked solution

Leading or trailing spaces may be legitimate record data. rtrim with the declared line-ending character set removes framing without silently rewriting those spaces. The parser should define its exact record and whitespace policy rather than borrowing a broad trim by habit.

Key takeaways

  • Laziness does not eliminate consumer, source, or resource costs.
  • Acquisition, failure, and release belong to one explicit lifecycle.

Official references