Intermediate · 50 MINUTES · VERSION 1
PHP iterable data, generators and failure timing
Aggregate validated rows in one pass while distinguishing lazy work, source memory and one-shot traversal.
Reviewed 23 Oct 2026Your setup may differ. Windows, Ubuntu, and optional tool bundles →
What you will learn
- Consume arrays and Traversable objects through iterable.
- Explain lazy execution and one-shot generator state.
- Validate rows and return success only after complete consumption.
Theory: produce values when requested
An iterable can be traversed with foreach. PHP accepts an array or Traversable object for this type. A generator uses yield to produce values incrementally. Calling its factory creates an object; its body starts working as iteration advances.
This can avoid building a second complete output collection. It cannot erase a large input array already built by the caller. A file/database source also has its own buffering, resource lifetime and failure rules. This fixture is not evidence that a database cursor streams without buffering.
A generator has traversal state. A consumed object is not a fresh dataset; create a new producer when another pass is required. A fresh external read can also see changed data, so define snapshot needs deliberately.
Worked example: validated aggregation
Required: PHP 8.4+ CLI. Save as study_stream.php. No framework, server, database or paid service. Laragon/XAMPP are optional.
<?php
declare(strict_types=1);
function sampleStudyRows(): Generator
{
yield ['minutes' => 15];
yield ['minutes' => 30];
yield ['minutes' => 0];
}
function sumStudyRows(iterable $rows): int
{
$total = 0;
foreach ($rows as $row) {
$minutes = is_array($row) ? ($row['minutes'] ?? null) : null;
if (! is_int($minutes) || $minutes < 0 || $minutes > 480) {
throw new InvalidArgumentException('Each row needs whole minutes from 0 to 480.');
}
if ($minutes > PHP_INT_MAX - $total) {
throw new OverflowException('Total is too large.');
}
$total += $minutes;
}
return $total;
}
if (realpath($_SERVER['SCRIPT_FILENAME'] ?? '') === __FILE__) {
echo 'Array total: '.sumStudyRows([['minutes' => 15], ['minutes' => 30]]).PHP_EOL;
echo 'Generator total: '.sumStudyRows(sampleStudyRows()).PHP_EOL;
echo 'Empty total: '.sumStudyRows([]).PHP_EOL;
}
Run and explain the result
Windows PowerShell:
php ./study_stream.php
Ubuntu Bash:
php ./study_stream.php
Array total: 45
Generator total: 45
Empty total: 0
PHP must be on PATH. Both shells share source. Windows PHP 8.4.26 was executed; Ubuntu and memory benchmarks were not.
The consumer requires each row to contain whole integer minutes from 0 to 480, ignores extra fields and checks integer capacity before adding. It returns only after every row succeeds. Empty input returns 0. A later invalid row or producer exception prevents a completed return. Arrays remain unchanged, but iterators are consumed. The guard keeps require-based tests quiet.
Guided lab
- Predict both totals and test empty input; compare the array before/after.
- Try 0, 480, missing/text/null minutes, -1, 481 and a non-array row.
- Make a generator log an event before each yield. Factory creation should do no body work; advancement should.
- Yield one valid row and then throw. The consumer must report failure, not a completed partial total.
- Compare two fresh generators, then try to traverse the same exhausted object again. Do not assume rewind always works.
Cost and operational boundaries
For n valid rows this consumer uses O(n) work and O(1) additional aggregate space. Source memory is separate; materializing it with iterator_to_array adds a full collection. Accepting iterable does not remove either cost.
Lazy execution changes error timing. A successful factory call does not mean every row is valid. Do not display final saved/success until required work completes. Real persistence may need transactions, idempotent chunks and cleanup; this pure sum provides none of those guarantees.
Independent exercise
Write maxStudyMinutes(iterable $rows): ?int using the same row rules, one pass and no materialization. Return null for no rows but integer 0 for a single zero-minute row.
Test empty/zero/two values, invalid late rows and a producer failure. Hint: track whether a valid row was seen; a falsey zero check loses a valid result. Use a new producer for a second traversal.
Common mistakes and interview
Avoid assuming factory creation validates everything, ignoring source memory, coercing numeric strings, catching every exception and returning partial success, or treating an exhausted generator as a repeatable list.
Interview: “When does a generator help with large data?” State producer/consumer contracts, failure timing, memory boundaries, one-pass behavior and the integration checks needed for the actual source.
English practice and glossary
Iterable: accepted traversable input. Generator: an iterator from a yielding function. Yield: produce a value and suspend. Lazy execution: work when requested. Consumer: code requesting values. Materialization: building a collection from a stream.
Say: “The producer starts when __. If row __ fails, the consumer __. Memory also depends on __.” Record actual observations in your journal.
Recap and next step
Separate producing values from consuming them. Describe source memory and failure timing honestly. Next connect PHP service boundaries to framework and transactional database work, without executing learner code on this website.
Reveal exercise hints and self-check answers
Generator creation does not execute the full producer. A consumer completes only after the traversal succeeds. A later exception means failure, not a valid partial total. Source memory is independent of the consumer’s aggregate variables.
Exercise outline: start with null maximum, validate each row and update the maximum with a real integer comparison. Propagate source failures. Empty yields null; valid zero stays 0. Use a fresh producer for another pass.