Это было на грани, но именованные аргументы — их также называют именованными параметрами — теперь поддерживаются в PHP 8! 

В этом посте я подробно разберу их особенности, но сначала давайте посмотрим, как они выглядят на реальных примерах:

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

Именованные аргументы во встроенной функции PHP

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'],
);

DTO, использующий продвижение свойств в конструкторе и именованные аргументы

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

Именованные аргументы также поддерживают распаковку массивов

Как вы уже могли догадаться по примерам, именованные аргументы позволяют передавать входные данные в функцию на основе имени аргумента, а не его порядка.

Я считаю, что именованные аргументы — отличная фича, которая сильно повлияет на мою повседневную разработку. Но вас, скорее всего, интересуют детали: что если передать неверное имя, и как работает синтаксис распаковки массива? Что ж, давайте подробно рассмотрим все эти вопросы.

Зачем нужны именованные аргументы?

Скажем так: эта фича вызвала много споров, и были аргументы против её добавления. Однако, на мой взгляд, их польза намного перевешивает страх проблем с обратной совместимостью или раздувания API. По-моему, они позволят нам писать более чистый и гибкий код.

Во-первых, именованные аргументы позволяют пропускать значения по умолчанию. Взгляните еще раз на пример с cookie:

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

Сигнатура этого метода на самом деле выглядит так:

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

В показанном примере нам не нужно было устанавливать cookie $value , но потребовалось задать время истечения. Именованные аргументы сделали вызов этого метода более лаконичным:

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

setcookie  без именованных аргументов

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

setcookie  с именованными аргументами

Помимо пропуска аргументов со значениями по умолчанию, есть и преимущество в наглядности: сразу понятно, какая переменная за что отвечает. Это особенно полезно в функция с длинными сигнатурами методов. Можно сказать, что большое количество аргументов — это «запах кода» (code smell), но с ними всё равно приходится работать, так что лучше иметь адекватный способ делать это, чем не иметь никакого.

Именованные аргументы подробнее

Разобравшись с основами, давайте посмотрим, что именованные аргументы могут, а чего не могут.

Прежде всего, именованные аргументы можно комбинировать с безымянными — также называемыми позиционными — аргументами. В этом случае позиционные аргументы всегда должны идти первыми.

Возьмем наш предыдущий пример с DTO:

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

Вы могли бы сконструировать его так:

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

Однако позиционный аргумент после именованного вызовет ошибку:

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

Далее, с именованными аргументами можно использовать распаковку массива (array spreading):

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

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

Однако, если в массиве отсутствуют обязательные элементы или есть ключ, который не указан как именованный аргумент, будет выброшена ошибка:

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

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

Входной массив можно комбинировать с именованными и позиционными аргументами, но только если позиционные аргументы следуют тому же правилу, что и раньше: они должны идти первыми!

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

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

 Посмотрите мое последнее видео: DX matters

Если вы используете variadic-функции, именованные аргументы будут переданы со своим именем ключа в массив variadic-аргументов. Рассмотрим следующий пример:

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',
);

В этом случае $args  в CustomerData::new  будет содержать следующие данные:

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

Атрибуты — также известные как аннотации — тоже поддерживают именованные аргументы:

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

Использовать переменную в качестве имени аргумента нельзя:

$field = 'age';

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

И наконец, именованные аргументы прагматично справляются с изменением имен при наследовании. Рассмотрим пример:

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

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

PHP без лишних сообщений разрешит изменить имя $event  на $myEvent , а $handler  на $myHandler ; но если вы решите использовать именованные аргументы, используя имя из родительского класса, это приведет к ошибке во время выполнения (runtime error):

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

Ошибка во время выполнения, если $listener  является экземпляром MyListener 

Этот прагматичный подход был выбран для предотвращения существенных ломающих изменений, при которых все унаследованные аргументы должны были бы сохранять одно и то же имя. Мне кажется, это хорошее решение.

Вот и всё основное, что можно рассказать об именованных аргументах. Если вы хотите узнать немного больше о предыстории некоторых архитектурных решений, советую прочитать RFC.