Data Types And Standard Library
Iterables
An iterable is any value PHP can loop over with foreach. Arrays are iterable. Generator objects created by functions that use yield are iterable. Objects can also be iterable when they implement the iterator interfaces PHP understands.
This matters in real projects because not every collection should be loaded into memory at once. A database export, CSV import, log reader, or API paginator may contain thousands or millions of rows. Accepting iterable lets a function process those values without caring whether they came from a ready-made array or a lazy source that produces one value at a time.
The important tradeoff is capability. iterable promises that code can loop. It does not promise random access, sorting, counting, array functions, or a second pass over the same data. Use it when looping is the real requirement. Ask for array when your function truly needs array behavior.
Accepting anything loopable
Use iterable when a function only needs to loop over values.
<?php
declare(strict_types=1);
function countActiveProducts(iterable $products): int
{
$count = 0;
foreach ($products as $product) {
if ($product['active'] === true) {
$count++;
}
}
return $count;
}
$products = [
['sku' => 'KB-101', 'active' => true],
['sku' => 'MS-202', 'active' => false],
['sku' => 'MN-303', 'active' => true],
];
echo countActiveProducts($products) . PHP_EOL;
// Prints:
// 2
The function does not require array functions such as array_filter(). It does not read index 0. It does not sort the collection. It just needs a sequence it can loop over. That makes iterable the honest type.
This is especially useful at boundaries between code that produces data and code that consumes data. A repository might return rows from a database cursor. A file reader might yield parsed lines. A test might pass a small array. The consumer can be the same in all three cases if it only uses foreach.
Returning values lazily with yield
A generator function uses yield to return values one at a time.
<?php
declare(strict_types=1);
function paidOrderIds(): iterable
{
yield 101;
yield 104;
yield 109;
}
foreach (paidOrderIds() as $orderId) {
echo 'Paid order: ' . $orderId . PHP_EOL;
}
// Prints:
// Paid order: 101
// Paid order: 104
// Paid order: 109
The function looks like it returns a collection, but PHP does not build a full array of values first. Each value is produced when the loop asks for it. This is lazy evaluation. It can reduce memory use and can let the caller start processing before every value has been discovered.
A generator is still ordinary PHP code. It can validate, transform, skip, and stop. The difference is that it yields values instead of collecting all of them into an array.
<?php
declare(strict_types=1);
function activeSkus(iterable $products): iterable
{
foreach ($products as $product) {
if ($product['active'] === true) {
yield $product['sku'];
}
}
}
$products = [
['sku' => 'KB-101', 'active' => true],
['sku' => 'MS-202', 'active' => false],
['sku' => 'MN-303', 'active' => true],
];
foreach (activeSkus($products) as $sku) {
echo $sku . PHP_EOL;
}
// Prints:
// KB-101
// MN-303
The input is iterable and the output is iterable. No intermediate SKU array is required unless a caller chooses to create one.
Reading lines lazily
Generators are useful when the source could be large. This example reads from a string so it is easy to run, but it models a line-by-line reader.
<?php
declare(strict_types=1);
function linesFromString(string $contents): iterable
{
foreach (explode("\n", $contents) as $line) {
yield rtrim($line, "\r");
}
}
$csv = "sku,status\nKB-101,active\nMS-202,inactive";
foreach (linesFromString($csv) as $line) {
echo $line . PHP_EOL;
}
// Prints:
// sku,status
// KB-101,active
// MS-202,inactive
A real file reader would use fopen() and fgets(), handle failure to open the file, and close the handle in a finally block. The idea is the same: process one line, yield it, then move to the next. This keeps memory usage predictable because the program does not need an array containing every line.
Do not make code lazy just because it looks sophisticated. For ten hard-coded values, an array is simpler. Laziness earns its keep when the source is large, expensive, remote, paginated, or naturally streamed.
Generators are usually one pass
Many iterables can only be consumed once. If you need to loop over the same values multiple times, convert them to an array deliberately.
<?php
declare(strict_types=1);
function numbers(): iterable
{
yield 1;
yield 2;
yield 3;
}
$values = iterator_to_array(numbers());
echo array_sum($values) . PHP_EOL;
echo count($values) . PHP_EOL;
// Prints:
// 6
// 3
Do not call iterator_to_array() automatically. It is a tradeoff: you gain repeatable array operations, but you also load every value into memory. That may be fine for a paginated API response containing 25 records. It may be a problem for a multi-gigabyte import.
One-pass behavior also affects debugging. If a function logs every item by looping over an iterable and then tries to process it in a second loop, the second loop may have nothing left. If the function needs two passes, make that requirement explicit by accepting an array or by materializing the iterable once with a clear name such as $rows.
Preserving keys
Generators can yield keys as well as values.
<?php
declare(strict_types=1);
function stockBySku(): iterable
{
yield 'KB-101' => 12;
yield 'MS-202' => 0;
}
foreach (stockBySku() as $sku => $stock) {
echo $sku . ': ' . $stock . PHP_EOL;
}
// Prints:
// KB-101: 12
// MS-202: 0
This is useful when callers need the identity of each item, not only the value. The same idea applies to errors by field name, totals by status, and configuration values by option name.
When converting an iterable to an array, decide whether keys should be preserved. iterator_to_array() preserves keys by default. If duplicate keys are yielded, later values can replace earlier ones. If duplicates should be impossible, validate before converting or while generating.
Accepting iterable and returning array
A function can accept iterable but return array when the output is intentionally a finished collection. The active SKU exercise uses that shape: input can be lazy, but the caller receives a complete list of strings.
<?php
declare(strict_types=1);
function collectPositiveNumbers(iterable $numbers): array
{
$positive = [];
foreach ($numbers as $number) {
if ($number > 0) {
$positive[] = $number;
}
}
return $positive;
}
echo implode(', ', collectPositiveNumbers([-1, 0, 2, 5])) . PHP_EOL;
// Prints:
// 2, 5
This is not wrong. It simply means the function has chosen to materialize the result. That may be exactly what the next part of the program needs. The key is that the choice is deliberate and visible in the return type.
When to use array instead
Use array when the function depends on array-specific behavior: random access by index, sorting in place, counting before processing, appending by reference, or using array functions.
<?php
declare(strict_types=1);
function firstProductName(array $products): string
{
if ($products === []) {
throw new InvalidArgumentException('At least one product is required.');
}
return $products[0]['name'];
}
echo firstProductName([
['name' => 'Keyboard'],
]) . PHP_EOL;
// Prints:
// Keyboard
This function should ask for an array because it directly reads index 0. Accepting iterable would be misleading. A generator does not promise that index exists, and an object iterator may not support array-style access.
Be equally honest in the other direction. If a function loops once and does not care where the values came from, requiring array may force callers to load data into memory unnecessarily.
Resource lifetime and exceptions
Lazy iteration often sits next to resources: open files, database cursors, HTTP response bodies, or temporary streams. The generator should own cleanup when it opens the resource. In real file code, that usually means opening the handle, yielding lines inside a try block, and closing the handle in finally. The caller should be able to stop early without leaking the resource.
Exception behavior should also be deliberate. If a generator validates rows as it yields them, the exception happens when the caller reaches the bad row, not when the generator function is first called. That timing can surprise tests and logs. A line such as $rows = readRows($path); may not read anything yet. The first foreach step does the work.
This delayed execution is useful for streaming, but it means errors should contain enough context: the line number, record key, page number, or source path. Without that context, a failure halfway through a long import is difficult to diagnose. A good lazy reader yields valid records and throws precise exceptions for invalid ones.
Generators are also a poor place for hidden global side effects. If each yielded value sends an email or writes to a database, the timing of those effects depends on how far the caller iterates. Keep generators focused on producing values. Put irreversible actions in explicit command code where partial progress, retries, and failure handling can be reviewed.
When a caller intentionally stops early, that should be safe. Searching for the first matching row, reading only a preview, or cancelling an import should not require the rest of the iterable to be consumed. That is one of the practical benefits of lazy code, but it only works when cleanup and errors are designed as part of the iterator contract. Document whether the iterable is single-use, whether keys are meaningful, and whether iteration may throw after some values have already been processed.
Testing iterable code
Test iterable consumers with both arrays and generators. The array case is convenient. The generator case proves the function really does use only loop behavior. If the function accidentally calls count(), reads index 0, or passes the iterable to an array function, the generator test will expose the mismatch.
Include empty iterables, one-item iterables, and values with keys when keys matter. For lazy readers, test that invalid records fail close to where they are produced and that cleanup happens when a resource is opened. For collectors, test that the returned array has the expected values and indexes.
When a function intentionally materializes an iterable, test the memory-sensitive boundary with realistic sizes outside the unit test suite, or at least review the maximum expected input. A helper that is fine for 100 records may be the wrong design for a nightly export of 10 million rows.
Review checklist
Before approving iterable code, identify whether the function only needs to loop or whether it also needs array capabilities. Check for accidental second passes over a generator. Look for hidden calls to iterator_to_array() on unbounded sources. Confirm whether keys should be preserved, ignored, or rejected when duplicated. Prefer generator return values when output can stay lazy, and prefer arrays when the caller genuinely needs a finished collection.
What to remember
iterable is a promise that code can loop over values. It is not a promise that values can be counted, sorted, indexed, or reused. That distinction is important when you write code for imports, exports, paginated APIs, and database results. Use iterable to keep consumers flexible, use generators to produce values lazily, and use array when array behavior is truly part of the contract for callers and tests.
Practice
Task: Process active SKUs from an iterable
Write a function that accepts an iterable of product records and returns the SKUs for active products.
Requirements
- Use
declare(strict_types=1);. - The function should accept
iterable $products. - Each product record should contain
skuandactive. - Return a plain array of active SKUs.
- Include one example that passes an array.
- Include one example that passes a generator.
- Print both results.
- Include the expected output as comments in the same PHP code block.
The point is to prove that the processing function does not care whether the records came from an array or from a lazy source.
Show solution
<?php
declare(strict_types=1);
function activeSkus(iterable $products): array
{
$skus = [];
foreach ($products as $product) {
if ($product['active'] === true) {
$skus[] = $product['sku'];
}
}
return $skus;
}
function productStream(): iterable
{
yield ['sku' => 'BK-100', 'active' => true];
yield ['sku' => 'CP-200', 'active' => false];
yield ['sku' => 'DS-300', 'active' => true];
}
$arrayProducts = [
['sku' => 'KB-101', 'active' => true],
['sku' => 'MS-202', 'active' => false],
['sku' => 'MN-303', 'active' => true],
];
echo implode(', ', activeSkus($arrayProducts)) . PHP_EOL;
echo implode(', ', activeSkus(productStream())) . PHP_EOL;
// Prints:
// KB-101, MN-303
// BK-100, DS-300
The processing function only uses foreach, so iterable is the right parameter type. It returns an array because the caller receives a finished list of SKUs that can be printed, counted, or passed to another function.
Task: Yield non-empty lines from text
Build a generator that reads lines from a string and yields only meaningful lines.
Requirements
- Use
declare(strict_types=1);. - Write a function that accepts one string and returns
iterable. - Split the input on newline characters.
- Trim each line.
- Skip empty lines after trimming.
- Yield the remaining lines one at a time.
- Loop over the generator and print each yielded line.
- Include expected output comments in the PHP code block.
Show solution
<?php
declare(strict_types=1);
function nonEmptyLines(string $contents): iterable
{
foreach (explode("\n", $contents) as $line) {
$line = trim($line);
if ($line === '') {
continue;
}
yield $line;
}
}
$text = " first line\n\n second line \n \nthird line";
foreach (nonEmptyLines($text) as $line) {
echo $line . PHP_EOL;
}
// Prints:
// first line
// second line
// third line
The function returns an iterable sequence instead of building a separate array of lines. Each cleaned line is produced only when the foreach loop asks for it.
Task: Yield stock counts keyed by SKU
Create a generator that turns product records into stock counts keyed by SKU.
Requirements
- Use
declare(strict_types=1);. - Accept an
iterableof product records. - Each product record should contain
skuandstock. - Yield each stock count with the SKU as the key.
- Demonstrate the function with an array input.
- Print the SKU and stock count in a
foreachloop. - Include expected output comments in the PHP code block.
Show solution
<?php
declare(strict_types=1);
function stockCountsBySku(iterable $products): iterable
{
foreach ($products as $product) {
yield $product['sku'] => $product['stock'];
}
}
$products = [
['sku' => 'KB-101', 'stock' => 12],
['sku' => 'MS-202', 'stock' => 0],
['sku' => 'MN-303', 'stock' => 8],
];
foreach (stockCountsBySku($products) as $sku => $stock) {
echo $sku . ': ' . $stock . PHP_EOL;
}
// Prints:
// KB-101: 12
// MS-202: 0
// MN-303: 8
The generator preserves the identity of each value by yielding the SKU as the key. Callers can loop over the keyed iterable without first building a separate map.