← All posts

PHP Closure Recursive Functions - Variable Scope

Aug 31, 2023 · 4 min read · 4,720 views
PHP Closure Recursive Functions - Variable Scope

A PHP closure recursive function cannot call itself through a variable captured with use ($var), because use copies the variable's value at the moment the closure is defined — not when it runs. The fix is to capture by reference with use (&$var).

TL;DR: change use ($var1) to use (&$var1). Everything below explains why that single & matters, what error you get without it, and which alternatives avoid the reference entirely.

What is the closure of a function?

Diagram of a closure capturing a variable from its enclosing scope

A closure is a function value that references variables from outside its body. The function may access and assign to the referenced variables; in this sense the function is "bound" to the variables. For example, the adder function returns a closure. Each closure is bound to its own sum variable. Ref

PHP differs from JavaScript here in one important way. In JavaScript, a nested function looks up outer variables when it runs. In PHP, you must declare what you want to capture with use, and by default PHP copies the value at the moment the closure is created. That timing difference is the entire source of this bug.

Why a PHP closure recursive function fails

The below code in PHP will throw Error:

PHP closure recursive function throwing Value of type string is not callable
$var1 = "test";

$var1 = static function($num) use ($var1) {
      if ($num < 1) return 1;
      return $var1($num - 1);
};

$var1(5);

echo "ok";

Trace the order of operations and the failure becomes obvious:

  1. $var1 is assigned the string "test".
  2. The closure is created. At this instant, use ($var1) copies the current value — the string "test" — into the closure's scope.
  3. $var1 is then reassigned to point at the closure itself. The copy already taken is unaffected.
  4. Calling $var1(5) runs the closure, which tries to invoke its captured copy — still the string "test".

On PHP 8 this raises Error: Value of type string is not callable. On PHP 7 you would instead see Fatal error: Function name must be a string. Either way the closure never sees itself.

The Solution

To pass $var1 by reference, so that it will point to last definition of $var1 which is closure function instead of string.

$var1 = "test";

$var1 = static function($num) use (&$var1) {
      if ($num < 1) return 1;
      return $var1($num - 1);
};

$var1(5);

echo "ok";
PHP closure recursive function working after capturing the variable by reference

With &, the closure holds a reference to the variable slot rather than a snapshot of its contents. When step 3 reassigns $var1, the closure's reference follows along and resolves to the closure itself at call time.

Alternatives that avoid the reference

Capturing by reference creates a cycle: the variable points at the closure, and the closure points back at the variable. PHP's cycle collector will clean this up, but the memory is not freed the moment the variable leaves scope. If that matters, two alternatives sidestep the problem:

Pass the closure to itself as an argument. No capture, no cycle:

$fact = static function($self, $num) {
      if ($num < 1) return 1;
      return $num * $self($self, $num - 1);
};

echo $fact($fact, 5); // 120

Or just use a named function. A named function is resolved at call time by the engine, so recursion works with no special handling — and it is easier to test and profile:

function factorial(int $num): int {
      return $num < 1 ? 1 : $num * factorial($num - 1);
}

Note that arrow functions (fn() =>) do not help here. They capture outer variables automatically, but they capture by value, exactly like a plain use ($var).

Frequently asked questions

Why does use ($var) copy the value instead of reading it later?

PHP binds captured variables when the closure object is constructed, not when it is invoked. This is a deliberate design choice that makes a closure's behaviour independent of whatever happens to the outer scope afterwards.

Does static on the closure cause this error?

No. static only prevents the closure from binding $this. The recursion failure happens identically with or without it.

Is capturing by reference safe to use in production?

Yes. The only caveat is the reference cycle described above. For a short-lived closure it is a non-issue; inside a long-running worker that creates many such closures, prefer a named function.

For more PHP internals write-ups, see my PR merged into PHP core adding ReflectionConstant::inNamespace(). The official reference for capture semantics lives in the PHP manual on anonymous functions.

Comments