data types and standard library
Iterables
An iterable is any value PHP can loop over with foreach, including arrays, generators, and objects that implement iterator interfaces.
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 of rows. Accepting iterable lets a function process those values without caring whether they came from an array or a lazy stream.
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 just needs a sequence it can loop over.
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.
Reading a file line by line
Generators are useful when the source could be large.
<?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(), but the idea is the same: process one line, yield it, then move to the next. This keeps memory usage predictable.
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.
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.
When to use array instead
Use array when the function depends on array-specific behaviour: random access by index, sorting in place, counting before processing, or using array functions. Use iterable when the function only needs to loop.
<?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.
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.
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.