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.
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
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;
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
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); }
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.