PHP 8.0 feature focus: Named Arguments

Larry Garfield
Larry Garfield
Director of Developer Experience
19 Nov 2020

Last week’s installment talked about the most contentious new feature of PHP 8.0. In our 10th and final installment, we’ll cover the formerly most contentious feature, one that somehow managed not only to generate little pushback this time around, but to become one of PHP 8.0’s top new features.

In every programming language that has functions (that is, all of them), functions can be called by passing an ordered list of arguments to them as input parameters. That works quite well, but does have some edge cases where it’s less than ideal. Specifically, when there’s more than one parameter it’s easy to forget what the order of them is or what a given parameter means. (And even when there’s just one, it may not be self-evident at the call site what it means.)

There are various workarounds that are possible, such as passing an associative array instead of discrete parameters, but those all introduce their own problems. Passing an associative array, for instance, bypasses all type safety.

A few languages address that problem by allowing (usually optionally) callers to specify parameters by name, rather than by position. PHP 8.0 is now one of those languages.

Named parameters

In PHP, named parameters are entirely controlled by the caller. All functions and methods are, automatically, named-parameter supporting. When the function is called, the caller can choose to use a positional argument, a named argument, or even a combination of them.

As an example, PHP’s array_fill() function produces a new array of a specified size where all elements are set to the same starting value. Like so:

<?php

$new = array_fill(0, 100, 50);

Just from reading that … what do those numbers mean? Will $new be a 100 element array of 50 or a 50 element array of 100? It’s not obvious unless you know how array_fill() works.

With named parameters, you can call it like this instead:

<?php

array_fill(start_index: 0, count: 100, value: 50);

Now it’s clear what we’ll get back: 100 array elements, with keys starting at 0, with all values set to 50. But you can also order the parameters however you want:

<?php

array_fill(
  value: 50,
  count: 100, 
  start_index: 0, 
);

The argument names are matched to the parameter names in the function definition, regardless of order. In this example we’ve also shown the parameters in vertical form, which is fine, and demonstrated a trailing comma on the last parameter. (Which, as noted, is new in PHP 8.0 and helps support exactly this case.)

Parameter names must be literals; you cannot have a variable as a parameter name.

It’s also possible to specify only certain parameters by name and others positionally. More specifically, you can list parameters positionally until you switch to named arguments and then use named thereafter. Thus, this is perfectly fine:

<?php

array_fill(0,
  value: 50,
  count: 100, 
);

But this is not:

<?php

array_fill(
  value: 50,
  0,
  count: 100, 
);

Named variadics

One tricky question with named parameters is around variadics. “Variadics” are the fancy name for the ability to pass a variable number of parameters to a function. That is, you’ve long been able to do this in PHP:

<?php

include_these($product, 1, 2, 3, 4, 5);

function include_these(Product $product, int ...$args)
{
    // $args is now an array of ints.

    $vals = ['a', 'b'];
    do_other_thing(...$vals);
}

Here, the ... operator, affectionately known as “splat,” either collects arguments up into an array or spreads them out from an array, depending on the context. But how does that interact with named arguments?

The way they interact is, I would argue, what you’d logically expect. An indexed array that is spread out will spread out as positional arguments. An associative array that is spread out will spread out as named arguments. To continue the earlier example:

<?php

// This is a named array, so the values will map to named parameters.
$params = ['count' => 100, 'start_index' => 0, 'value' => 50];
array_fill(...$params);

When collecting variadic arguments, they’ll be collected as either numerically indexed array values if passed positionally or as string-keyed array values if passed by name. Be aware that, if you have a variadic parameter, that means you may have an array that is a mixture of numeric and named keys.

That also leads to interesting possibilities to build up a function call dynamically, by first dynamically building an associative array and then calling the function with it using splat.

<?php

$args['value'] = $request->get('val') ?? 'a';
$args['start_index'] = 0;
$args['count'] = $config->getSetting('array_size');

$array = array_fill(...$args);

Limitations

The main pushback against named arguments was, and always has been, that making it “too easy” to have functions with lots of arguments would encourage poor API design. For example, this method call is unquestionably hard to understand:

<?php

read_value($object, true, false, false, false, true, true, 7, false, true);

Such an API really should be redesigned. The fear is that named parameters would allow API designers to get away with saying “well, just use names on them, then you can even skip all the defaults you don’t care about that way.” That’s true, but also misses the point that the problem is the API is too complicated.

There’s no strong preventative here other than “don’t use this as a crutch to make bad APIs,” which could be said about nearly any language feature.

But … why?

Named parameters were first seriously proposed all the way back in 2013, but didn’t gain traction until now. What really brought them to the forefront again was several discussions around how to make object construction easier. Several proposals were floated in early 2020 for dedicated, one-off syntaxes for easier “struct” objects, that is, objects that are just a collection of probably-public properties.

Languages like Go and Rust make it very easy to create structs with named parameters, because they don’t technically have objects the way PHP does. They have a struct with properties, period, that you can define even as a struct literal, and then you can hang methods off of them. The net result is similar but not quite the same as the Java/C++/PHP style of object. Still, plenty of people have been justifiably jealous of how easy those languages make it to create new complex structures.

None of the one-off syntaxes that were proposed really addressed the problem in a way that “fit” with PHP. Several people, however, myself included, noted that a more robust approach would be to solve the underlying problems in a way that allowed a nicer object construction syntax to just “fall out” naturally.

I laid out the argument for that in detail in a blog post back in March. In short, constructor property promotion plus named arguments would, together, effectively give us a struct-initialization syntax. The whole would be greater than the sum of its parts. Fortunately, the ever-busy Nikita Popov agreed with me and picked up the ball and ran with it.

That brings us to one of the few places where named arguments really should be used: constructing struct objects.

To demonstrate, consider a value object for a PSR-7 URL object.

<?php

class Url implements UrlInterface
{
    public function __construct(
        private string $scheme = 'https',
        private ?string $authority = null,
        private ?string $userInfo = null,
        private string $host = '',
        private ?int $port = 443,
        private string $path = '',
        private ?string $query = null,
        private ?string $fragment = null,
    ) {}
}

We have already seen how constructor promotion makes that vastly easier to write. Named arguments also makes it vastly easier to use, since many of those parameters can legitimately be absent in a valid URL, including ones early in the list like $authority. In PHP 7.4, you’d have to use it like so:

<?php

$url = new Url('https', null, null, 'platform.sh', 443, '/blog', null, 'latest');

Which is … gross, but you have to do that in order to get to the later arguments. With named parameters, that collapses to this shorter and more self-documenting alternative:

<?php

$url = new Url(host: 'platform.sh', path: '/blog', fragment: 'latest');

// Or if you prefer vertical:

$url = new Url(
    path: '/blog', 
    host: 'platform.sh', 
    fragment: 'latest',
);

And you can then put the parameters in any order to boot. The two features complement each other to make working with lightweight objects vastly cleaner than in previous versions. Value object constructors are, I would argue, one of the three key places to use named arguments.

Arguments in attributes

The second key target is in attributes. Recall this tease from the last article:

<?php

class SomeController
{
    #[Route('/path', name: 'action')]
    public function someAction()
    {
        // ...
    }
}

Attributes are syntactically an object constructor call, which gets called lazily through the reflection API as needed. Because it’s a constructor call, it can do (almost) anything a constructor call can do, including use named arguments.

In fact, I predict that most attribute usage will use named arguments. The whole point is to make flexible metadata available within the syntax of the language itself. Many Doctrine annotation usages today rely very heavily on named keys, as they are more self-documenting and more flexible when there are many optional arguments. It’s logical to expect (and encourage) attributes to follow the same pattern.

Named arguments about arguments

The third place I expect named arguments to see heavy use is on functions where the parameters are legitimately needed, and legitimately confusing, and there’s no self-evident way around that. The array_fill() example earlier is a good example.

Another good example? Everyone’s favorite canard, $haystack, $needle vs. $needle, $haystack. String functions generally use one order, array functions generally use the other, for reasons that make sense when you’re modeling your API on C but not otherwise. Now, you don’t need to remember what the order is.

<?php

if (in_array(haystack: $arr, needle: 'A')) {
    // ...
}

Is that the order those parameters are in the function, positionally? Who cares? The function call specifies exactly what is being passed, making the order irrelevant. (They’re not in order, in case you were wondering.)

At last, we can stop complaining about parameter order and citing that as a reason why PHP is bad. (I’m sure people will still continue to do so; they’ll just be even more wrong than they already were.)

API implications

One warning, however. As noted, named arguments work for all functions and methods automatically. That means the parameter names in functions, methods, and interfaces are now API significant. Changing them may break users who are calling them by name. (This would be a good time to revisit your parameter names, while checking to make sure your libraries are ready for PHP 8.0.)

Variable name significance is not, at this point, enforced in inheritance. That’s a pragmatic decision to avoid breaking too much existing code, as the vast majority of methods won’t be called with named parameters 99.9% of the time, so creating even more breakage for existing code that might be renaming arguments for entirely logical reasons didn’t make sense. Python and Ruby take the same approach and haven’t run into serious issues, which PHP took as a good sign that it was safe to do the same.

It should be no surprise that named arguments RFC come to us courtesy of Nikita Popov.

Coming soon to a container near you

This has been quite a whirlwind tour of PHP 8.0! Stay tuned for next week when the final 8.0.0 release ships. You’ll be able start using it on Platform.sh immediately.

You can try out pre-release copies of PHP 8.0 today on Platform.sh, with just a one-line change. Give it a whirl, and let us know what your favorite features are.