tempest I'm building a framework called Tempest, take a look or read the roadmap.

PHP 8: named arguments

It was a close call, but named arguments — also called named parameters — are supported in PHP 8! In this post I'll discuss their ins and outs, but let me show you first what they look like with a few examples in the wild:

setcookie(
    name: 'test',
    expires: time() + 60 * 60 * 2,
);

Named arguments used on a built-in PHP function

class CustomerData
{
    public function __construct(
        public string $name,
        public string $email,
        public int $age,
    ) {}
}

$data = new CustomerData(
    name: $input['name'],
    email: $input['email'],
    age: $input['age'],
);

A DTO making use of promoted properties, as well as named arguments

$data = new CustomerData(...$customerRequest->validated());

Named arguments also support array spreading

You might have guessed it from the examples: named arguments allow you to pass input data into a function, based on their argument name instead of the argument order.

I would argue named arguments are a great feature that will have a significant impact on my day-to-day programming life. You're probably wondering about the details though: what if you pass a wrong name, what's up with that array spreading syntax? Well, let's look at all those questions in-depth.

# Why named arguments?

Let's say this feature was a highly debated one, and there were some counter arguments to not adding them. However, I'd say their benefit far outweigh the fear of backwards compatibility problems or bloated APIs. The way I see it, they will allow us to write cleaner and more flexible code.

For one, named arguments allow you to skip default values. Take a look again at the cookie example:

setcookie(
    name: 'test',
    expires: time() + 60 * 60 * 2,
);

Its method signature is actually the following:

setcookie ( 
    string $name, 
    string $value = "", 
    int $expires = 0, 
    string $path = "", 
    string $domain = "", 
    bool $secure = false, 
    bool $httponly = false,
) : bool

In the example I showed, we didn't need to set the a cookie $value, but we did need to set an expiration time. Named arguments made this method call a little more concise:

setcookie(
    'test',
    '',
    time() + 60 * 60 * 2,
);

setcookie without named arguments

setcookie(
    name: 'test',
    expires: time() + 60 * 60 * 2,
);

setcookie with named arguments

Besides skipping arguments with default values, there's also the benefit of having clarity about which variable does what; something that's especially useful in functions with large method signatures. Now we could say that lots of arguments are usually a code smell; we still have to deal with them no matter what, so it's better to have a sane way of doing so, than nothing at all.

Noticed a tpyo? You can submit a PR to fix it. If you want to stay up to date about what's happening on this blog, you can subscribe to my mailing list: send an email to brendt@stitcher.io, and I'll add you to the list.

# Named arguments in depth

With the basics out of the way, let's look at what named arguments can and can't do.

First of all, named arguments can be combined with unnamed — also called ordered — arguments. In that case the ordered arguments must always come first.

Take our DTO example from before:

class CustomerData
{
    public function __construct(
        public string $name,
        public string $email,
        public int $age,
    ) {}
}

You could construct it like so:

$data = new CustomerData(
    $input['name'],
    age: $input['age'],
    email: $input['email'],
);

However, having an ordered argument after a named one would throw an error:

$data = new CustomerData(
    age: $input['age'],
    $input['name'],
    email: $input['email'],
);

Next, it's possible to use array spreading in combination with named arguments:

$input = [
    'age' => 25,
    'name' => 'Brent',
    'email' => 'brent@stitcher.io',
];

$data = new CustomerData(...$input);

If, however, there are missing required entries in the array, or if there's a key that's not listed as a named argument, an error will be thrown:

$input = [
    'age' => 25,
    'name' => 'Brent',
    'email' => 'brent@stitcher.io',
    'unknownProperty' => 'This is not allowed',
];

$data = new CustomerData(...$input);

It is possible to combine named and ordered arguments in an input array, but only if the ordered arguments follow the same rule as before: they must come first!

$input = [
    'Brent',
    'age' => 25,
    'email' => 'brent@stitcher.io',
];

$data = new CustomerData(...$input);


If you're using variadic functions, named arguments will be passed with their key name into the variadic arguments array. Take the following example:

class CustomerData
{
    public static function new(...$args): self
    {
        return new self(...$args);
    }

    public function __construct(
        public string $name,
        public string $email,
        public int $age,
    ) {}
}

$data = CustomerData::new(
    email: 'brent@stitcher.io',
    age: 25,
    name: 'Brent',
);

In this case, $args in CustomerData::new will contain the following data:

[
    'age' => 25,
    'email' => 'brent@stitcher.io',
    'name' => 'Brent',
]

Attributes — also known as annotations — also support named arguments:

class ProductSubscriber
{
    #[ListensTo(event: ProductCreated::class)]
    public function onProductCreated(ProductCreated $event) { /* … */ }
}

It's not possible to have a variable as the argument name:

$field = 'age';

$data = CustomerData::new(
    $field: 25,
);

And finally, named arguments will deal in a pragmatic way with name changes during inheritance. Take this example:

interface EventListener {
    public function on($event, $handler);
}

class MyListener implements EventListener
{
    public function on($myEvent, $myHandler)
    {
        // …
    }
}

PHP will silently allow changing the name of $event to $myEvent, and $handler to $myHandler; but if you decide to use named arguments using the parent's name, it will result in a runtime error:

public function register(EventListener $listener)
{
    $listener->on(
        event: $this->event,
        handler: $this->handler, 
    );
}

Runtime error in case $listener is an instance of MyListener

This pragmatic approach was chosen to prevent a major breaking change when all inherited arguments would have to keep the same name. Seems like a good solution to me.


That's most there is to tell about named arguments. If you want to know a little more backstory behind some design decisions, I'd encourage you to read the RFC.

Are you looking forward to using named arguments? Let me know via Twitter or via e-mail!