deftcode
Polski
← Back to blog

PHP in a nutshell: Functions and closures

Read this in Polish
phpphp coursefunctions

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|null

A 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 string
function render(Stringable&Countable $x): void { } // intersection (PHP 8.1) — Stringable AND Countable at once

A 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 once
render('basket'); // TypeError — a string is neither one nor the other

Classes 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 runs
function fail(): never { throw new … ; } // never (PHP 8.1) — the function NEVER finishes

The 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 null

Since 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 Model

If 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 time
function bad(int $x = random_int(1, 10)) { } // error — a function call

Since 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 signature

The 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 defaults

Named 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); // 6

Three 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 object
function 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 change

And 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. &$obj is 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 $GLOBALS array ($GLOBALS = […]). Writing individual keys still works.

Static variables

function counter(): int
{
static $count = 0; // initialised ONCE, survives between calls
return ++$count;
}
counter(); // 1
counter(); // 2

static 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:

  • static behaves 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 being

Arrow functions

$mul = 3;
$f = fn(int $x): int => $x * $mul; // $mul captured automatically, no use needed

An 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 fn is always by value and there’s no way to change that — the syntax fn() 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 full function () 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 value
Diagram: a closure with use by reference sees the variable's current value, while use by value and an arrow function hold a copy from the moment of definition
The copy is made when the closure is defined. Only use (&$x) reaches for the current value.

In 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 well
function () 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 closure

All 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 text

The 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 class Closure, but there is no closure mechanism at work here. A named function has no surroundings to capture, so nothing is closed over and strlen doesn’t suddenly start seeing your variables. Closure is, in this spot, simply the name of the class PHP wraps “something callable” in. What actually captures is function () use (…) and fn().

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 through
run(strtoupper(...)); // OK — a Closure is a callable too
function runStrict(Closure $c): string { return $c('hi'); }
runStrict(strtoupper(...)); // OK
runStrict('strtoupper'); // TypeError: must be of type Closure, string given

Closure 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 method

Binding $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 property

The 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 its private and protected;
  • 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 with null: trying to read $this->n in the example above ends with Error: 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 in

call() 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 $this
unset($fn);
// "freed" — only now, together with the closure
$fn = (new Report())->staticFormatter();
// "freed" — right away, the closure had nothing to hold the object by

So 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.

Comments