PHP in a nutshell: Functions and closures
What this part is about
Defining functions in PHP is largely very similar to defining them in other programming languages. There are, however, a few things that people coming from languages like JavaScript or Python may find surprising: namely the fact that a PHP function cannot see variables from its surroundings, and that parameter names are part of its public API (thanks to named arguments, which we’ll get to below).
In this part we’ll go through those surprises, and then through types in the signature, named and variadic
arguments, variable scope, closures and callable.
Declaring functions
function greet(string $name, string $greeting = 'Hello'): string{ return "$greeting, $name!";}Parameter types and the return type are syntactically optional, but it’s good practice to always declare
them. Without them, declare(strict_types=1) from part one has
nothing to enforce.
Compound types
function find(?int $id): ?User { } // ?T is shorthand for T|null — the same as: find(int|null $id): User|nullA question mark in front of a type means “or null”. The form without it looks the same, just longer — to PHP,
?int and int|null are exactly the same type.
function id(int|string $id): int|string { } // union (PHP 8.0) — int OR stringfunction render(Stringable&Countable $x): void { } // intersection (PHP 8.1) — Stringable AND Countable at onceA union means “one of”, an intersection “all at once”. Intersections only work with class types — you can’t
write int&string, because nothing could ever satisfy it.
Since an intersection requires class types, the example calls for a class:
class Basket implements Stringable, Countable{ public function __toString(): string { return 'Basket'; } // required by Stringable public function count(): int { return 3; } // required by Countable}
render(new Basket()); // OK — Basket satisfies both interfaces at oncerender('basket'); // TypeError — a string is neither one nor the otherClasses and interfaces are the topic of part seven, so for now you don’t need to understand what job
implements does here (although if you have experience with another language that supports classes, you
probably know perfectly well what it’s for). For now, just remember that in the code above
Stringable&Countable will accept only an object that satisfies both conditions at the same time.
Special return types
function log(string $m): void { } // void — the function returns nothing, it just runsfunction fail(): never { throw new … ; } // never (PHP 8.1) — the function NEVER finishesThe difference between void and never matters: a void function runs to the end and returns to the call
site, just without a value. A never function never finishes — it throws an exception or calls exit.
That’s what lets static analysis know the code after such a call is unreachable:
function abort(string $msg): never{ throw new RuntimeException($msg);}
$user = $repo->find($id) ?? abort('No such user');// we only reach this line if abort() did NOT run,// which means find() returned something — so $user definitely isn't nullSince abort() declares never, the analyser knows the right-hand side of ?? never produces a value — the
only way out of that line and further down is through a non-empty result from find(). Hence the certainty
that $user isn’t null: if it were, execution would never have got there, it would have thrown instead.
There’s a third type of this kind — static (PHP 8.0) — but it only makes sense inside a class:
class Model{ public static function make(): static // "an instance of THIS class, the one the method was called on" { return new static(); }}
class User extends Model { }
$u = User::make(); // the return type is User, not ModelIf make() declared : Model, the analyser would only know you get some Model. We cover inheritance in
part seven — for now it’s enough to remember that static as a return type means “the same type the method
was called on”.
Parameters
Default values
function paginate(int $page, int $perPage = 20) { }A default value must be a constant expression — a literal, a constant, an array of literals, or an expression built from such elements. It cannot be the result of a function call.
const VAT = 0.23;
function price(float $net, float $rate = 1 + VAT) { } // OK — an expression built from a constant and a literal, computed at compile timefunction bad(int $x = random_int(1, 10)) { } // error — a function callSince PHP 8.1 there is one exception: new in a default value — more on that in part nine of the series.
Parameters with default values should come last in the list. Putting them earlier is allowed, but it forces the caller to pass everything anyway — unless they use named arguments.
Named arguments
htmlspecialchars('<b>Hello</b>', double_encode: false); // the parameter name comes straight from the function's signatureThe names don’t come from nowhere — they are exactly the names the author of the function wrote in its signature. It shows best on an example of your own:
function paginate(int $page, int $perPage = 20, string $sort = 'id', bool $desc = false): string{ // …}
paginate(2, 20, 'id', true); // what does that 20 mean, and that true?paginate(page: 2, desc: true); // the same arguments, only readable — and without repeating the defaultsNamed arguments (PHP 8.0) solve two problems at once: they let you skip middle parameters that have defaults,
and they make the call document itself. setVisible(true, false, true) says very little about the values
passed to that function, whereas setVisible(header: true, footer: false, sidebar: true) lets you understand
what you’re dealing with without opening the function’s body.
A parameter name becomes part of your public API. Renaming a parameter in the next version of a library is a backward-compatibility break — exactly like changing its type. The same trap awaits when overriding methods: if you name a parameter differently in a child class than in the base class, the very same call with a named argument will work for one class and blow up for the other — even though both have “the same” method.
Variadic parameters
function sum(int ...$nums): int{ return array_sum($nums); // $nums is an ordinary array}
sum(1, 2, 3); // 6Three dots in front of a parameter collect all the remaining arguments into an array. The type before ...
applies to each of them individually.
In practice this means that inside the function $nums is an array of ints only. PHP can’t express that in
the signature — array is just array, with no information about its contents. You write it in a docblock
instead: @param int[] $nums, or list<int> if you use PHPStan or Psalm.
At the call site those same three dots work the other way round — they unpack an array into individual arguments:
$args = [1, 2, 3];sum(...$args); // the same as sum(1, 2, 3)
paginate(...['page' => 2, 'perPage' => 50]); // the same as paginate(page: 2, perPage: 50)String keys in the unpacked array act as named arguments (PHP 8.1).
Passing by reference
function addOne(array &$arr): void // & means: work on the original, not on a copy{ $arr[] = 1;}
$list = [];addOne($list); // $list === [1]By default arguments are passed by value — the function gets a copy and cannot change the caller’s
variable. An ampersand (&) in the declaration changes that.
You’ll often hear that objects in PHP are passed “by reference”. That’s a shortcut, and it isn’t true. An object goes by value too — it’s just that the value is an object identifier, not the object itself. The identifier is what gets copied, so both variables point at the same object:
function rename(User $u): void { $u->name = 'new'; } // works — it's still the same objectfunction replace(User $u): void { $u = new User('new'); } // does NOT work — it only swaps the local variable
$user = new User('old');rename($user); // $user->name === 'new'replace($user); // $user->name is still 'new' — the object did not changeAnd that’s exactly why it isn’t passing by reference: if it were, replace() would have swapped the caller’s
variable. To achieve that, you have to write &$u explicitly — the very same difference we discussed with
references in part one.
The practical consequence: you don’t need
&to change an object’s state inside a function.&$objis only needed when you want to replace the caller’s variable itself with a different object.
& does call for care, though, because nothing at the call site gives it away. process($order) looks
identical whether the function merely reads the object or swaps your variable out from under you — to find
out, you have to go and look at the signature. Which is why it’s rarely used in practice.
Variable scope
This is the difference I mentioned at the start, and for many people the biggest surprise in the whole series.
$g = 10;
function f(): void{ echo $g; // Warning: Undefined variable $g}A function in PHP has no access to the scope it was defined in. There is no implicit closure like in JavaScript, no reading of outer variables like in Python.
To be clear about what is what: variables created outside any function land in the global scope — PHP has
no such thing as file or module scope, so $g = 10 at the top of a script is a global variable. Everything
that comes into being inside a function is local: parameters are ordinary local variables and disappear
with the end of the call, exactly like variables assigned in the function’s body. These two worlds don’t see
each other — a function won’t reach a global variable until it fetches it explicitly.
function f(): void{ global $g; // works, but ties the function to global state echo $GLOBALS['g']; // the same thing written differently}Both forms work and both are a bad idea: the function stops being testable and its dependencies vanish from the signature. The right answer is to pass the value in as an argument.
PHP 8.1 forbade overwriting the whole
$GLOBALSarray ($GLOBALS = […]). Writing individual keys still works.
Static variables
function counter(): int{ static $count = 0; // initialised ONCE, survives between calls return ++$count;}
counter(); // 1counter(); // 2static inside a function is a local variable that doesn’t disappear when the call ends. It’s useful for
memoisation and lazy initialisation, but it has a few consequences worth knowing about:
staticbehaves identically in a plain function and in a method — the variable belongs to the function, not to a single call and not to an object.- That’s why a static variable in a method is shared by all instances of the class: creating ten objects doesn’t give you ten counters, just one shared between them. In a plain function that’s obvious; in a method it surprises people, because intuition suggests “one per object”.
- Since PHP 8.1, an inherited method that a child class doesn’t override shares the same static variable with the base class.
That’s hidden global state again, only tucked away inside a function. In tests it can be painful.
Closures
Let’s start with the name, because two concepts get mixed up here. An anonymous function is a function without a name, written as an ordinary value — you can assign it to a variable, pass it along as an argument and call it later, just as you would with a number or a string. A closure, on the other hand, is the name of a mechanism: such a function can close over the variables from the place where it was created and remember them at the moment it’s called.
In PHP the two blur into one, because every anonymous function is an object of class Closure — whether or
not it captures anything. That’s why in practice, and in the PHP documentation itself, both names are used
interchangeably.
What it’s for: plenty of PHP functions take a “piece of logic” as an argument — array_map(),
array_filter(), usort(), event handling, routing. Defining a named function at the top of the file just to
pass it ten lines further down would be disproportionately heavy.
And what the closing over is for: a plain function cannot see its surroundings — that was the whole section above — so without this mechanism there would be no way to give a callback anything beyond its own arguments.
$min = 100;
$large = array_filter($orders, function (Order $o) use ($min): bool { return $o->total >= $min; // $min comes from outside — a plain function wouldn't see it});PHP has two syntaxes for this. They differ not only in notation, but also in the moment they take the variable’s value — and that’s the most common trap in this whole text.
function () use ()
$mul = 3;
$f = function (int $x) use ($mul): int { return $x * $mul;};
$g = function (int $x) use (&$mul): int { // capture by reference return $x * $mul;};The use clause lets you decide explicitly what the closure takes with it from its surroundings. Without an
ampersand it makes a copy at the moment of definition, not of the call — and that’s the detail that bites
most often:
$mul = 3;$multiply = function (int $x) use ($mul): int { return $x * $mul; };
$mul = 10;$multiply(2); // 6, not 20 — the closure remembered the three at the moment it came into beingArrow functions
$mul = 3;
$f = fn(int $x): int => $x * $mul; // $mul captured automatically, no use neededAn arrow function (PHP 7.4) needs no special syntax for capturing — it reaches for the variables from its
surroundings that you use in its body all by itself. You pay for that with one limitation: the body may
consist of exactly one expression, which is always returned implicitly. There’s no room here for several
statements or for your own return.
Capturing in
fnis always by value and there’s no way to change that — the syntaxfn() use (&$x)simply doesn’t exist, PHP will reject it with a parse error. If you need the value as it is at the moment of the call, you have three ways out: the fullfunction () use (&$x), passing the value as an argument, or capturing an object — what gets copied then is the object identifier, so you see its state as it currently is:
$box = new Box(); // $box->v === 1$scalar = 1;
$fromObject = fn() => $box->v;$fromScalar = fn() => $scalar;
$box->v = 99;$scalar = 99;
$fromObject(); // 99 — what was copied is the identifier, the object is the same one$fromScalar(); // 1 — what was copied is the valueIn code it looks like this:
$x = 1;
$byValue = function () use ($x) { return $x; };$byRef = function () use (&$x) { return $x; };$arrow = fn() => $x;
$x = 99;
$byValue(); // 1 — a copy from the moment of definition$byRef(); // 99 — a reference to the live variable$arrow(); // 1 — an arrow function copies as wellfunction () use () |
fn() => |
|
|---|---|---|
| Capturing variables | explicit, via use |
automatic, by value |
| Body | many statements | one expression |
return |
explicit | implicit |
| Capture by reference | use (&$x) |
not possible |
In practice: arrow functions for short callbacks (array_map, usort, filters), full closures where you need
several statements or a reference.
Callable
Let’s start with the problem callable solves. In PHP a function’s name is not a value — unlike in
JavaScript, you can’t simply write:
array_map(strlen, $words); // Error: Undefined constant "strlen"PHP sees the name of a constant there, not of a function. To pass a function where another function is
expected — to array_map() or usort(), say — you first have to wrap it somehow. callable is the
collective name for all the forms PHP can call in such a place, and six of them have piled up:
'strlen' // a function name as a string[$obj, 'method'] // an instance method[Foo::class, 'staticMethod'] // a static method'Foo::staticMethod' // the same thing as a string$obj // an object with an __invoke() method$closure // a closureAll six work. array_map('strlen', $words) is perfectly valid code and returns exactly what you’d expect
— you don’t need to resort to an arrow function or to use. That variety is simply a legacy of old PHP, and
it’s worth knowing so you recognise these forms in other people’s code.
Since PHP 8.1 there is, however, a form better than all of them — not because the others don’t work, but
because they are ordinary strings and arrays. To an IDE, 'strlen' is just text in quotes: it won’t suggest
the name as you type, it won’t let you jump to the definition, and when the function gets renamed it won’t
touch that string — you’ll see the typo only once the code runs.
The f(...) syntax
$fn = strlen(...);$fn = $obj->method(...);$fn = Foo::create(...);
array_map(strtoupper(...), $words); // the same as array_map('strtoupper', $words), but the IDE sees a function here, not textThe form takes an existing, named function and wraps it in an object of class Closure. The important part is
that it unifies how you pass a function as a value: it makes no difference whether it’s a global function, an
object’s method or a static method — the notation is always the same, instead of six different ones. On top of
that it’s checked by the compiler, understood by IDEs and carried along by refactoring. In new code, use only
this form.
A word of warning so the concepts don’t get muddled:
strlen(...)gives you an object of classClosure, but there is no closure mechanism at work here. A named function has no surroundings to capture, so nothing is closed over andstrlendoesn’t suddenly start seeing your variables.Closureis, in this spot, simply the name of the class PHP wraps “something callable” in. What actually captures isfunction () use (…)andfn().
Before PHP 8.1 the equivalent was Closure::fromCallable('strlen').
Variable functions
A variable function is a call in which you put a variable where the function’s name goes — you write
$name('hi') where strtoupper('hi') would normally be. PHP then takes the contents of that variable and
treats it as the name of a function to call. The name is resolved only at the moment of the call — nothing
checks beforehand whether such a function exists at all. It’s an older mechanism than f(...), hence the
limitations described below.
$fn = 'strtoupper';$fn('hi'); // 'HI' — the name is resolved at the moment of the call
is_callable($fn); // true
'strtoupper'('hi'); // this works too — PHP 7+ lets you call a string directly, but why, when you have strtoupper('hi')It won’t work for language constructs, because they aren’t functions: echo, print, isset, empty,
unset, include, require, list, array. If you need one of them as a callback, wrap it:
$isSet = fn($x) => isset($x);callable or Closure in a signature
Since both can be written as a type, it’s worth knowing how they differ. callable accepts every one of the
six forms above. Closure accepts only objects of that class, meaning anonymous functions, arrow functions
and the f(...) form:
function run(callable $c): string { return $c('hi'); }
run('strtoupper'); // OK — a string with a function name gets throughrun(strtoupper(...)); // OK — a Closure is a callable too
function runStrict(Closure $c): string { return $c('hi'); }
runStrict(strtoupper(...)); // OKrunStrict('strtoupper'); // TypeError: must be of type Closure, string givenClosure is narrower and usually the better choice: you get a guarantee that what you were handed is callable
here and now, rather than a string with a name that may point at a function which doesn’t exist — callable
will only find that out at the moment of the call.
There’s also one hard limitation: callable cannot be a property type.
class Button{ public Closure $onClick; // OK public callable $onHover; // Fatal error: Property Button::$onHover cannot have type callable}And while we’re here, the trap everybody walks into the first time they keep a closure in a property:
$b = new Button();$b->onClick = fn() => 'click';
$b->onClick(); // Error: Call to undefined method Button::onClick()($b->onClick)(); // 'click' — the parentheses tell PHP you mean the property, not a methodBinding $this
A closure created in the body of a method sees $this automatically — you don’t have to list it anywhere
in use:
class Cart{ private float $rate = 1.23;
public function grossPrices(array $items): array { return array_map(fn(float $net): float => $net * $this->rate, $items); // ↑ $this available with no extra work }}A closure created outside a class is a separate matter. It has no $this at all — but you can attach one
to it with Closure::bind():
class Counter{ private int $n = 5;}
$counter = new Counter();
$read = function () { return $this->n; };$bound = Closure::bind($read, $counter, Counter::class);
echo $bound(); // 5 — it has access to the private propertyThe second argument is the object that is to become $this inside the closure. The third is the class
scope — the answer to the question “from inside which class is this code notionally running”. It is that
argument, not the second one, that decides access to private and protected members. Your options are:
- a class name (
Counter::class) — the closure behaves as though it stood in that class’s body, so it sees itsprivateandprotected; - an object (
new Counter()) — PHP takes the class name from it, the effect is the same; null— no scope, the closure sees public members only;- omitting the argument — the default value is
'static', meaning “leave whatever scope the closure already has”. A closure created outside a class has none, so the effect is the same as withnull: trying to read$this->nin the example above ends withError: Cannot access private property.
$read is an object of class Closure, so it has methods of its own as well — hence the -> notation:
$read->call($counter); // binding and calling in one; the scope becomes the class of the object passed incall() is an ordinary instance method, just like bindTo(); Closure::bind() is its static counterpart.
The difference is in permanence: bind() and bindTo() return a new closure for further use, whereas
call() runs it right away, once.
There is one exception — a static closure, one declared with the word static in front of function or
fn. Such a closure has no $this by definition and you can’t attach one to it:
$fn = static fn(): int => 1;
Closure::bind($fn, $counter, Counter::class);// Warning: Cannot bind an instance to a static closure — returns null (in PHP 9 this will be an error)Binding $this is a tool for tests, adapters and hydrators, not for everyday code — it bypasses
encapsulation, so if you reach for it in ordinary business logic, the class probably has the wrong interface.
static in front of a closure is useful beyond binding, though. A closure created in a method holds on to
$this even when it doesn’t use it at all — and as long as it holds on, the object cannot be freed:
class Report{ public function __destruct() { echo "freed\n"; }
public function formatter(): Closure { return fn(float $v): string => number_format($v, 2); // holds $this, though it never uses it }
public function staticFormatter(): Closure { return static fn(float $v): string => number_format($v, 2); // holds nothing }}
$fn = (new Report())->formatter();// silence — the Report object lives on, because the closure holds $thisunset($fn);// "freed" — only now, together with the closure
$fn = (new Report())->staticFormatter();// "freed" — right away, the closure had nothing to hold the object bySo for callbacks handed from a method somewhere further on — to array_map(), to a job queue, to an event
container — it’s worth adding static when $this isn’t needed inside. Otherwise the object’s lifetime
starts to depend on how long somebody keeps your callback around.
What’s next
You can now wrap logic in a function and pass it along as a value. The next part is shorter and very concrete: dates and time.
We’ll see why DateTime and DateTimeImmutable are two different classes and which to pick by default, how
DateInterval and DatePeriod work, what strtotime() really does, and why time zones are a separate topic
in PHP that has to be handled deliberately.