PHP Iterables
What Is an Iterable?
In simple terms: An iterable is anything you can loop over with foreach: an array, a generator, or an object designed to be traversed. If a function only needs to loop over some items, it does not care whether they come from an array in memory, a generator reading a huge file, or a collection object — it just needs something iterable.
Technically: iterable is a pseudo-type, available since PHP 7.1, that accepts both arrays and objects implementing the Traversable interface (generators, Iterator, and IteratorAggregate implementations). Since PHP 8.2, iterable is formally an alias for the union type Traversable|array.
iterable
├── array
└── Traversable (interface)
├── Iterator → Generator, ArrayIterator, your own iterators
└── IteratorAggregate → your own collection classes
Why Use iterable?
| Parameter Type | Accepts | Limitation |
|---|---|---|
array | Arrays only | Generators must be converted, loading everything into memory |
Traversable | Objects only | Plain arrays are rejected |
iterable | Both | Only foreach (and conversion functions) can be used on it |
Typing a parameter as iterable makes a function work with any source of items, including memory-efficient generators.
A Function That Accepts Any Iterable
<?php
function totalPrice(iterable $products): float
{
$total = 0.0;
foreach ($products as $product) {
$total += $product['price'];
}
return $total;
}
function productsFromDatabase(): Generator
{
// Simulates rows fetched one at a time
yield ['name' => 'Desk Lamp', 'price' => 1299];
yield ['name' => 'Backpack', 'price' => 499];
}
final class ProductCollection implements IteratorAggregate
{
public function __construct(private array $products) {}
public function getIterator(): Generator
{
yield from $this->products;
}
}
$sources = [
'array' => [['name' => 'Gel Pen', 'price' => 20], ['name' => 'Notebook', 'price' => 60]],
'generator' => productsFromDatabase(),
'ArrayIterator' => new ArrayIterator([['name' => 'Water Bottle', 'price' => 349]]),
'collection' => new ProductCollection([['name' => 'Mouse', 'price' => 599], ['name' => 'Pad', 'price' => 150]]),
];
foreach ($sources as $label => $products) {
printf("%-14s ₹%8.2f\n", $label, totalPrice($products));
}
Output
array ₹ 80.00
generator ₹ 1798.00
ArrayIterator ₹ 349.00
collection ₹ 749.00
How It Works
totalPrice()only usesforeach, so it can declare its parameter asiterable.- It receives four very different sources: a plain array, a generator, a built-in iterator object, and a custom collection.
ProductCollectionimplementsIteratorAggregate: itsgetIterator()method returns a generator, which makes the object usable inforeach(see Iterators).- Passing a non-iterable value (such as a string) would throw a
TypeError.
Checking and Converting Iterables
<?php
var_dump(is_iterable([1, 2]));
var_dump(is_iterable(new ArrayIterator([])));
var_dump(is_iterable('text'));
function letters(): Generator
{
yield 'a';
yield 'b';
yield 'c';
}
echo iterator_count(letters()) . "\n";
print_r(iterator_to_array(letters()));
print_r(iterator_to_array(['x' => 1, 'y' => 2])); // arrays accepted since PHP 8.2
Output
bool(true)
bool(true)
bool(false)
3
Array
(
[0] => a
[1] => b
[2] => c
)
Array
(
[x] => 1
[y] => 2
)
| Function | Purpose |
|---|---|
is_iterable($value) | Checks whether a value can be used with foreach |
iterator_to_array($iterable, $preserveKeys = true) | Converts any iterable to an array (arrays accepted since PHP 8.2) |
iterator_count($iterable) | Counts elements by iterating (arrays accepted since PHP 8.2) |
Converting a generator to an array loads all values into memory and consumes the generator. Convert only when you need array features or must iterate several times.
Array Functions Need Arrays
PHP's array_* functions accept only real arrays:
<?php
function numbers(): Generator
{
yield 1;
yield 2;
}
try {
array_map(fn(int $n): int => $n * 2, numbers());
} catch (TypeError $e) {
echo $e->getMessage() . "\n";
}
Output
array_map(): Argument #2 ($array) must be of type array, Generator given
There are two solutions: convert with iterator_to_array() (simple, but loads everything), or write lazy helpers that work on any iterable.
Lazy Helpers for Iterables
<?php
function mapIterable(iterable $items, callable $fn): Generator
{
foreach ($items as $key => $item) {
yield $key => $fn($item);
}
}
function filterIterable(iterable $items, callable $keep): Generator
{
foreach ($items as $key => $item) {
if ($keep($item)) {
yield $key => $item;
}
}
}
function numbers(): Generator
{
foreach (range(1, 10) as $n) {
yield $n;
}
}
$evenSquares = mapIterable(filterIterable(numbers(), fn(int $n): bool => $n % 2 === 0), fn(int $n): int => $n * $n);
echo implode(', ', iterator_to_array($evenSquares, false)) . "\n";
echo implode(', ', iterator_to_array(mapIterable(['a' => 1, 'b' => 2], fn(int $n): int => $n * 10))) . "\n";
Output
4, 16, 36, 64, 100
10, 20
The helpers accept arrays and generators, process one item at a time, and preserve keys. Libraries such as nikic/iter provide complete sets of such functions.
Returning Iterables
A function can also declare iterable as its return type. This gives the implementation freedom: it may return an array today and a generator later, without changing callers that only loop over the result:
<?php
interface ProductSource
{
/** @return iterable<array{name: string, price: int}> */
public function products(): iterable;
}
final class InMemoryProducts implements ProductSource
{
public function products(): iterable
{
return [['name' => 'Gel Pen', 'price' => 20]];
}
}
final class CsvProducts implements ProductSource
{
public function __construct(private string $path) {}
public function products(): iterable
{
$handle = fopen($this->path, 'r');
fgetcsv($handle, escape: ''); // skip header
while (($row = fgetcsv($handle, escape: '')) !== false) {
yield ['name' => $row[0], 'price' => (int) $row[1]];
}
fclose($handle);
}
}
The @return iterable<...> docblock describes the element type for IDEs and static analysis tools such as PHPStan and Psalm, because PHP itself has no generic types.
Caution: A caller that receives an
iterablemust not assume it can loop twice, usecount(), or access elements by index — a generator supports none of these. If you need those features, require anarray(orCountable) explicitly.
Common Mistakes
Using Array Functions on Iterables
Effect: TypeError when a generator or iterator object is passed.
Fix: Use foreach, lazy helpers, or iterator_to_array().
Iterating a Generator Twice
Effect: "Cannot traverse an already closed generator".
Fix: Convert to an array first, or create a new generator.
Typing Everything as array
Effect: Callers must convert generators, losing memory efficiency.
Fix: Accept iterable when the function only loops.
Losing Values With Duplicate Keys
Effect: iterator_to_array() overwrites values when keys repeat (common with yield from).
Fix: Pass false as the second argument.
Best Practices
- Accept
iterablein functions that only loop over their input. - Return
iterable(orGenerator) where lazy production of items is useful. - Document element types with
@paramand@returngenerics for static analysis. - Use lazy helpers instead of converting large iterables to arrays.
- Require
arrayorCountableexplicitly when counting, indexing, or multiple passes are needed.
Official reference: https://www.php.net/manual/en/language.types.iterable.php