---
title: "foreach Already Knows: PHP and the Zero-One-Infinity Rule"
url: https://www.exakat.io/foreach-already-knows-php-and-the-zero-one-infinity-rule/
date: 2026-10-13
modified: 2026-10-13
lang: en
author: "dams"
description: "foreach Already Knows: PHP and the Zero-One-Infinity Rule [php] <?php foreach ($recipients as $recipient) { $mailer->send($recipient, $message); } ?> [/php] This loop doesn't ask when or if $recipients a list...."
categories:
  - "Technology"
tags:
  - "concept"
  - "php"
  - "syntax"
image: https://www.exakat.io/wp-content/uploads/2026/10/crates.640.jpg
word_count: 3025
---

# foreach Already Knows: PHP and the Zero-One-Infinity Rule

# foreach Already Knows: PHP and the Zero-One-Infinity Rule

This loop doesn't ask when or if `$recipients` a list. It doesn't ask anything, although, well, it does ask for an array or an iterable. Now, when `$recipients` holds three users, it runs three times. If it holds one, it runs once. If it's empty, the body simply never executes: no dispatcher spun up, no mailer touched, no special case written by anyone. PHP's own engine already treats zero, one, and many as the same problem: it is solved once, at the language level. And you don't have to pay attention to it in any way.

The rest of the code that we write hasn't caught up to that idea. Most PHP is written for exactly one thing: one user, one row, one file, one thing. The moment a function has to handle zero of them, or two, or an arbitrary number N, that code usually gets rewritten rather than extended: a new `if`, a extra loop, a external helper, you name it. The gap between how PHP's runtime treats cardinality and how PHP programmers treat it is the subject of this piece.

## Naming the rule

There's a decades-old name for the discipline `foreach` is abiding by: the **Zero-One-Infinity rule**, that I credit to computing pioneer `Willem van der Poel`, with an independent claim by language designer `Bruce MacLennan`. The rule says that a well-designed system should never bake in an arbitrary limit, such as "up to 5", or "at most 3", on how many of something can exist. Either a thing is forbidden, that is 0, allowed exactly once, that is 1, or allowed without limit, that is many. Nothing in between is a design decision; it's a bug waiting to be reported as a feature request.

Applied to everyday PHP, the rule reads a little differently than it did for language designers, back then in the previous millenium:

- **Zero** is usually an optimization: don't do the work, not even prepration, if there's nothing to do. No preloading before you know it's needed, no dispatch before you know there's a recipient, no clean up.
- **One** is the default case, the one every tutorial teaches, and it needs no introduction.
- **More**, or 2, or 3, or an undefined N, depending on the situation, is where the real cost shows up: duplicated code, then the itch to centralize it into a function or method, or a loop to drive it, and sometimes both. Sometimes, you don't even know where to put the lot: inside or outside the method.

PHP has plenty of internal gears for the two extremes already. What it's never quite settled is the syntax for the middle: the moment a script goes from `this variable holds one user` to `this variable holds however many users there are`.

Let's review them, and see how we can transition from one to the other.

## Zero: be lazy

Zero rarely gets its own code path in well-written PHP: it gets avoided, short-circuited, omitted, ignored. May be, even dispised. A repository method finds a user or it doesn't; the code afterward should cost nothing in the doesn't branch.

The nullsafe operator collapses that guard into the call itself, which is in fact the same optimization written as one expression instead of two statements:

`??=` is the same idea applied to work instead of data: don't build the expensive thing until something actually asks for it:

Notice PHP already has two different spellings for nothing, and it chooses between them based on the cardinality the code expected before it turned out to be empty:

`null` is what "zero" looks like when a slot was always meant to hold at most one thing. `[]` is what "zero" looks like when the slot was always meant to hold any number of things. These are two different absences, which are chosen by the shape of the context. There is no best practice so place the `if` anywhere.

## One: same old, same old

This is the most common case. It is deliberately a short description, because it's the case PHP needs the least help with. `$user->notify($message);` is all you need, and it gets used all the time, obviously. Just do it. If it is typed, it's a safe thing. Every PHP tutorial's first working example is the one case, and that's not an accident: it's the case the language was designed around first, and everything else was retrofitted on top.

## Arrays, and the container that stopped caring

Before going further into `more than one`, it's worth stopping on a traditional PHP feature, that the platform got right decades before this pattern had a name: the data side of this problem was already solved. PHP is untyped by default: it dissolves the zero/one/many distinction at the array level, long before it ever reaches a function signature. Actually, any effort to force types on arrays, is going against what we are presenting here.

Imagine a moment wrapping every value in an array. A list, if you want, with a steady type, no index.

The values are of the same type. Here, we see the humble `[...]` or `array()` for the old-timers (hi there!), regardless of how many elements are inside it. Nothing converts, nothing changes shape, nothing declares a new type as the count moves from 0 to 1 to N. `count($x)` doesn't care which case it's looking at: it is typeless. Neither does `foreach`. And it is important to understand that they are working on different levels.

Compare that to a statically-typed language, where the question `maybe zero, maybe one, maybe many` usually forces three genuinely different types that don't compose without an explicit conversion step:

`Optional` and `List` share no supertype worth using; going from one to the other is a refactor, not a reassignment. PHP's array already is the composition those two types are trying to approximate separately. The literal syntax, `[]`, is simultaneously the empty set, the singleton (the unique value here, not the pattern), and an arbitrary-length collection, with no wrapper to strip off later.

So the noun problem, what container holds `however many of these there are`, was never PHP's weak point. What's left, and what the rest of this piece is actually about, is the verb problem: the syntax you write to act on that container still assumes exactly one, and has to be entirely rewritten the moment it isn't.

## Two in a box: it is still fine to write by hand

Treating two items in th same way is where the `more` side of the rule start its transition, and it's still comfortable territory: PHP lets you name both halves and move on:

`list()` destructuring, and its less blog-friendly `[$a, $b]` shorthand, is PHP's admission that `exactly two, with distinct roles` is common enough to deserve its own syntax: a middle ground between `one variable`and `a loop`. Nobody reaches for `array_map()` to handle a winner and a runner-up. Yet, most people should reach for `in_array()` instead of doing an `or`, even for at the two values scale: let that sink in. The point is that there are easy work around, that don't feel like bringing out the big guns.

## Three: where duplication starts to hurt

Three is usually still hand-writable, but it's the point where the transition starts:

Three identical lines, differing only in which variable holds `$this`. Nothing here is wrong, but the pattern is now visible, and the next person to read it, aka the next executive to be added, is going to feel the pull toward a loop before they feel any pull toward more copy-paste.

## N, the infinite and beyond: where centralization becomes mandatory

Once the count isn't known until runtime, hand-written duplication stops being an option and PHP forces a choice: a function or method that takes the collection, and a loop inside it. We all recognize `array_map()` and `foreach()` and its cousins.

Actually, that notion applies to other PHP features, such as parameters. Variadics are PHP's syntax for the specific flavor of `many`where even the caller doesn't know the count at compile time: it's baked into the call site itself rather than passed as a collection:

Both are still hiding `foreach` under their vest. Variadics only change how the many things arrive at the function boundary, not how they're processed once inside it, which is a hint that `foreach` really is doing all the load-bearing work for more in PHP, everywhere it shows up.

## PHP's built-in answers to "more"

Once `more` needs handling, PHP hands several tools, each answering a slightly different question about it.

`array_map`, `array_filter`, and `array_reduce` are `foreach` with the loop body extracted and named: the same zero-or-more traversal, but expressed as `apply this operation` instead of `repeat this block`:

`Countable` and `IteratorAggregate` let a custom type declare that it behaves like `zero or more` without exposing its internal array at all: typing the cardinality instead of just the storage:

The `iterable` type hint, union of `array` and `Traversable`, is PHP's way of saying `zero or more, and I genuinely don't need to know how you boxed them together`:

Generators push this the furthest, because they can yield zero, one, or an unbounded number of values, lazily, one at a time, without ever materializing the "how many" question as a number:

If nothing is active, the generator yields nothing and costs nothing beyond the check. If everything is active, it yields everything, one at a time, without ever holding the full set in memory. Zero, one, and infinity, genuinely unified in a single construct: not a bad way to close out the rule's namesake.

## The 8.x-era features, seen through this lens

A few newer additions to PHP line up with this pattern more directly than they're usually given credit for.

First-class callable syntax is PHP explicitly turning a single-item operation into a value that can be handed to something that applies it across many:

`strtoupper(...)` doesn't call `strtoupper`: it becomes a `Closure` version of it, built specifically to be passed somewhere that will call it repeatedly. That's the `one operation, applied N times` idea, made explicit at the syntax level for the first time.

`match()` occasionally gets used to encode this exact rule directly, which is a nice bit of self-reference:

Enums are an interesting middle case: a fixed, compile-time `many`: a closed set of possibilities known in advance, where every individual case is simultaneously a singleton. Think of it as a boolean that will allow a third leg to sprout when you need it, or a fourth, a fifth (and not six, we don't want bugs obviously):

An enum is `many` at the type level and `one` at every use site: a preview of the tension the next section runs into on purpose.

## The idea PHP could finish: applying object syntax onto arrays

Now, let's write a piece of science fiction. It's worth noting that PHP wouldn't be the first language to get there. Groovy's spread-dot operator already does this at the language level:

```
def totals = orders*.total()
?>
[/php]

```

`orders*.total()` calls `.total()` on every element of `orders` and collects the results: no loop, no `map`, no proxy object. It reads exactly like calling `.total()` on a single order, with one extra character marking and do this to all of them.

Interestingly, there are already some experimentations in the PHP syntax with the current level of features. Laravel's `HigherOrderCollectionProxy` gets close to the same effect in userland, without any language changes, by leaning on `__call()` and `__get()`:

`->each` returns a proxy object; the `->notify($message)` that follows it gets forwarded to every item in the collection. It's a library trick, not a syntax change, and it's proof the idiom is desirable enough that people built magic-method plumbing to fake it.

PHP itself has neither. Today, going from one to many always means switching syntax entirely:

The intent hasn't changed at all between those two lines: only the cardinality has. Yet the code had to be rewritten from a method call into a function call wrapping a closure. A hypothetical broadcast syntax would let the array itself carry that intent, the same way `[]` already carries "however many" at the data level:

In this syntax, there is nothing to see. `$users` contains objects, and each, if any, can process a call to notify(), with the same method and arguments. This is no less readable than making it a foreach loop. No one needs three lines to understand what is happening here.

## Upgrading some more syntax could be quite fun

Broadcasting only makes sense where we need to act on each element. There are some situations where we still want to work on the group, not the elements. I see two of them, and you can let your imagination run wild.

The first one is when reaching for class constants, static methods or properties. They would be always the same, as they do not depend on the object, but on the class. Of course, we could optimize this by short circuiting it to a call to `array_fill()`, unless there are side effects that need to be counted.

Generally speaking, a static method or property has no per-element `$this` to bind to. `Logger::record()` would stuff the same static values in the same file, again and again. This is quite a trap:

The second one is when using the current operators. Does a scalar, which are now virtually banned, auto-transform to an array, with `array_fill()`, to generate the same operation on each element? Nested arrays may also be worth a look.

Then,

# A look at the bigger picture

PHP's story on cardinality turns out to be further along than it looks at first glance. The container problem, that super-variable that holds zero, one, or many of something, was solved by being untyped: `[]` absorbs all three without ever needing a wrapper type to convert between them, which is more than most statically-typed languages can say. In fact, a large part of the PHP community tend to ditch this feature away in favor of typing strongly every single piece of data. And that might be a piece of advice I also provide, so, mea culpa.

The traversal problem is solved too, and solved very well: `foreach` skips empty input for free, Generators can lazily yield nothing, one thing, or an unbounded stream from the same construct, and `iterable` lets a function stop caring which container it was handed. Let them come to me...

What's still unfinished is the call-site problem: the fact that `do this` and `do this to each of these` have never shared a syntax in PHP, even though the data underneath them always could. Groovy closed that gap at the language level with the star operator `*.`; Laravel closed a narrower version of it in userland with proxy objects. PHP has the container and the traversal machinery to make the same move: what's missing is the verb. And a bit of syntax modernization or science fiction.