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(), ?)andtick()ran immediately, printing its side effect before$fwas ever invoked. If you're used tofn()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_findpasses both the value and the key to your callback, so the key lands inintval's second parameter,$base. I confirmed it:array_find([16 => 'ff'], intval(?, ...))returns'ff'because key16became the base and parsed the hex — whileintval(?)(no...) returnsNULL. 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.