Press "Enter" to skip to content

Named Parameters in PHP 8.0

A while back I spent some time improving Yar's performance, but I also ran into a legacy design wart that had always bothered me: for parallel RPC calls, the current method prototype is:

public static Yar_Concurrent_Client::call(string $uri, string $method, ?array $arguments = NULL, ?callable $callback = NULL, ?callable $error_callback = NULL, ?array $options = NULL):null|int|bool {}

Doesn't your head hurt just looking at it?

Because in real usage the callback and error callback are very often empty — they can be specified globally in the loop when the calls are actually dispatched:

Yar_Concurrent_Client::loop(?callable $callback = NULL, ?callable $error_callback = NULL, ?array $options = NULL):?bool {}

And $options is often useful, varying per call. So in practice, a lot of parallel-call code ends up littered with NULL arguments, like:

Yar_Concurrent_Clinet::call("https://xxx.com/api", "method", array("arguments"), NULL, NULL, array(YAR_OPT_HEADER=>array("header:val1"));

Yar_Concurrent_Clinet::call("https://xxx.com/api", "method", array("arguments"), NULL, NULL, array(YAR_OPT_HEADER=>array("header:val2"));

Yar_Concurrent_Clinet::call("https://xxx.com/api", "method", array("arguments"), NULL, NULL, array(YAR_OPT_HEADER=>array("header:val2"));

Yar_Concurrent_Clinet::call("https://xxx.com/api", "method", array("arguments"), NULL, NULL, array(YAR_OPT_HEADER=>array("header:val4"));

So I'd long been wondering how to make calls like this more elegant. At one point I considered polymorphism, or adding a new API like:

public static Yar_Concurrent_Client::callArray(array $arguments):null|int|bool {}

But my OCD told me that would sow endless trouble. Then this morning I suddenly remembered an RFC I'd once seen; after digging around, I found it had been committed as far back as PHP 5.6. But I'm fairly old-school and don't track new features closely, so I'd never really used it — not sure whether you have. 🙂

That's the first feature to introduce today: argument unpacking.

Argument unpacking

We know PHP supports variadic functions. For example, given this function definition:

function variadic(...$arguments) {
    var_dump($arguments);
}

Note the parameter definition uses three dots … (the ellipsis symbol), meaning however many arguments you pass when calling this function, they all get packed into an array named $arguments:

variadic();
//output: array(0) { }
variadic(NULL);
//output: array(1) { [0]=> NULL }
variadic("foo", "bar");
//output: array(2) { [0]=> string(3) "foo"  [1]=> string(3) "bar" }
variadic(NULL, array(), "dummy");
//output: array(3) { [0]=> NULL [1]=>[] [2]=> string(5) "dummy" }

Of course, that's not what we need today. This feature has a sibling form used at the call site, called argument unpacking:

For instance, for my problem above, suppose we define a function

function dummy($a, $b = NULL, $c = NULL, $d = NULL, $e = NULL) {
    var_dump($a, $b, $c, $d, $e);
}

If most of the time parameters b, c and d are NULL but e needs to be passed, we can use argument unpacking to avoid all those NULL arguments in the code, like:

$arguments = array(
    "First argument",
    NULL,  NULL,  NULL,
    "Fifth argument",
);

dummy(...$arguments);

//output:
// string(14) "First argument"
// NULL
// NULL
// NULL
// string(14) "Fifth argument"

Note I also used … at the call site: it means unpack the array after the … and pass its elements to the callee in order — the first element maps to the first parameter, the second to the second.

But note that position here is about fill order, not array index. That is:

$arguments = array(
    4=> "First argument",
    0=> "Fifth argument"
),

in this form, the entry with index 4 is still treated as the first parameter.

Realizing this, I suddenly saw that Yar needs nothing new at all — the opening example can become:

$arguments = array(
    "https://xxx.com/api",
    "method",
    array("arguments"),
    NULL, NULL,
    "options" => array(YAR_OPT_HEADER => array("header:val1")
)
Yar_Concurrent_Clinet::call(...$arguments);

$arguments["options"][YAR_OPT_HADER] = ["header:val2"];
Yar_Concurrent_Clinet::call(...$arguments);

$arguments["options"][YAR_OPT_HADER] = ["header:val3"];
Yar_Concurrent_Clinet::call(...$arguments);

$arguments["options"][YAR_OPT_HADER] = ["header:val4"];
Yar_Concurrent_Clinet::call(...$arguments);

Think that's the end of it?

Consider the code above: there's still one issue — you have to build an intermediate array. For the OCD among us, that still feels a bit… well, something.

But we can also lean on another RFC introduced in PHP 8.0: named parameters.

Named Parameter

Since PHP 8.0, you may specify parameter names when passing arguments. For the same example function:

function dummy($a, $b = NULL, $c = NULL, $d = NULL, $e = NULL) {
    var_dump($a, $b, $c, $d, $e);
}

We can now name the parameters we want to pass at the call site, for example:

dummy(a:"dummy", e:"foo");
//output:
// string(5) "dummy"
// NULL
// NULL
// NULL
// string(3) "foo"

That is, I specified values for a and e; everything unspecified takes its default. You can even ignore the declaration order:

dummy(e:"foo", a:"dummy");

The output is the same.

With this, the opening code becomes:

Yar_Concurrent_Client::call("https://xxx.com/api", "method", arguments:array("arguments"), options:array(YAR_OPT_HEADER=>array("header:val1")));
Yar_Concurrent_Client::call("https://xxx.com/api", "method", arguments:array("arguments"), options:array(YAR_OPT_HEADER=>array("header:val2")));
Yar_Concurrent_Client::call("https://xxx.com/api", "method", arguments:array("arguments"), options:array(YAR_OPT_HEADER=>array("header:val3")));
Yar_Concurrent_Client::call("https://xxx.com/api", "method", arguments:array("arguments"), options:array(YAR_OPT_HEADER=>array("header:val4")));

Now you can pass whichever parameter you want, however you want, right at the call site.

The code is still a bit longer than with argument unpacking, but it removes the sea of NULLs without introducing any intermediate variable.

Problem solved perfectly — and no new API needed. :)

Be First to Comment

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.