Skip to main content
← All articles
php

PHP 8.6's partial application: I ran it on RC3 so you don't trip on the footguns

PHP 8.6's partial application: I ran it on RC3 so you don't trip on the footguns

PHP 8.6 adds partial function application, and it's the feature people will demo first: write load('node', ?) and instead of calling, you get a closure with the first argument baked in. It's genuinely pleasant. It also has three sharp edges I only really understood after running them on the RC — and if you write Drupal contrib, you can't use any of it for years. Let me show you both halves.

What it is

The RFC (Larry Garfield and Arnaud Le Blanc, passed 33–0) adds two placeholders in a call: ? for exactly one argument, ... for zero or more. Use either and the call doesn't execute — it returns an ordinary Closure. trim(?) is a one-arg closure; foo(1, ...) fixes the first arg and forwards the rest; bar(...) with no remaining parameters is a zero-arg "thunk" that just delays the call. Because the result is a plain Closure, reflection needs no new API. GA is 19 November 2026.

The three footguns (verified on RC3)

I ran these on the 8.6 RC rather than trust the prose, because each one surprised me:

  • Arguments evaluate when the closure is created, not when it's called. This is the opposite of an arrow function. I wrote $f = load('x', tick(), ?) and tick() ran immediately, printing its side effect before $f was ever invoked. If you're used to fn() capturing lazily, this will bite.
  • Without ..., extra arguments are silently ignored. Pass more args to the closure than you left holes for and PHP just drops them — no warning. Convenient until it hides a bug.
  • The ... key-leak. This is the nasty one. ... forwards every extra argument, including ones you didn't mean to expose. The RFC's own example: array_find($arr, intval(?, ...)). array_find passes both the value and the key to your callback, so the key lands in intval's second parameter, $base. I confirmed it: array_find([16 => 'ff'], intval(?, ...)) returns 'ff' because key 16 became the base and parsed the hex — while intval(?) (no ...) returns NULL. A silent correctness bug hiding inside a tidy one-liner.

A couple more boundaries I hit: you can't partially apply new — new P(1, ?) is a compile-time fatal ("Cannot create Closure for new expression") and it isn't catchable, so use a static factory instead. And sort(...) inside a pipe fails with a by-reference error — that's a pipe limitation, not PFA's fault, but you'll meet it.

Where the real value is

Here's my honest take: as a capability, PFA adds nothing. fn($x) => load('node', $x) already does the job. The actual wins are smaller and quieter. First, readability — a thunk and a one-hole partial read better than a nest of arrow functions in a pipe. Second, and more concretely, the UPGRADING notes say array_map() with a first-class-callable or PFA callback now compiles to the equivalent foreach loop, skipping the intermediate closure (similarly, printf() with only %s/%d becomes string interpolation). That's a genuine optimization — though note it's scoped to those specific rewrites; I didn't benchmark anything broader and I wouldn't claim 8.6 is "faster" on the strength of it.

It's a convenience with a compiler optimization hiding behind it, wrapped around three behaviors that don't match your arrow-function instincts. Learn the footguns before the syntax.

The Drupal catch: learn it now, ship it in 2028

If you write Drupal contrib, file this under "someday." Drupal 12 requires PHP 8.5, so PFA — an 8.6 feature — can't appear in Drupal 12 core, nor in any contrib code that still supports 8.5 (which is most of it, for years). Contrib lives at the floor of the PHP versions it claims to support, and that floor moves slowly. (For the record, I think Drupal 11's floor is 8.3, but that's from memory — don't quote me, check your own composer.json.) So this is a feature to understand now and actually reach for much later. Play with it on an RC, get the key-leak into your muscle memory, and then wait.

Links

BM
Blue Moose
The moose behind Blue Moose. Full-stack PHP developer — Drupal by day, Symfony by night, tests always.