<? phpukraine СПІВБЕСІДИ
Пошук по платформі
CORE PHP · MIDDLE

Named arguments і constructor property promotion: що дають і де підводять?

Named arguments (PHP 8.0) дозволяють передавати аргументи за іменем параметра й пропускати необовʼязкові, а constructor property promotion (PHP 8.0) оголошує властивість прямо в сигнатурі конструктора; ціна — імена параметрів стають частиною публічного API, а промотована властивість ініціалізується ще до тіла конструктора.

У нас конструктор на вісім параметрів — як зробити виклик читабельним, не роблячи білдер?
Чому після перейменування параметра `$str` на `$string` у сервісі впав чужий код, хоча сигнатура сумісна?
Можна передати масив з рядковими ключами як іменовані аргументи — і з якої версії PHP?
Чому `$this->name = trim($name)` у тілі конструктора падає з «Cannot modify readonly property», якщо параметр промотований?
named arguments constructor promotion PHP 8.0 PHP 8.1 DTO readonly BC break

Обидві фічі приїхали в PHP 8.0, але лікують різні болі. Constructor property promotion прибирає дублювання в оголошенні: замість трьох рядків на кожну залежність (властивість, параметр конструктора, присвоєння) модифікатор видимості просто ставиться перед параметром — public function __construct(private LoggerInterface $logger) {} — і PHP сам створює властивість із тим самим типом та імʼям. Named arguments лікують місце виклику: аргумент передається за іменем параметра, а не за позицією, тому необовʼязкові параметри можна пропускати вибірково, а не «дотягувати» дефолтами до потрібного. new SearchQuery(text: 'php', city: 'Львів') читається без заглядання в сигнатуру, тоді як new SearchQuery('php', 1, 20, 'Львів') — ні.

Механіка named arguments має кілька жорстких правил, і саме на них ловлять. Позиційні аргументи мають іти першими: f(text: 'php', 3) — це помилка парсингу, а не рантайму. Передати той самий параметр двічі (позиційно й за іменем) не можна — буде Error: Named parameter $x overwrites previous argument. Неіснуюче імʼя дає Error: Unknown named parameter $x. Іменовані аргументи, що не збіглися з жодним параметром, збираються у варіадик із рядковими ключами — саме тому прозорі декоратори виду handle(...$args) продовжують працювати. Розпакування масиву з рядковими ключами в іменовані аргументи (f(...['page' => 2])) дозволене з PHP 8.1; у 8.0 воно кидало Cannot unpack array with string keys. Окремо варто памʼятати, що func_get_args() бачить виклик так, ніби все передали позиційно: пропущені необовʼязкові параметри підставляються своїми дефолтами, тому старий код на func_num_args() починає рахувати інакше.

Головна ціна named arguments — імена параметрів стають публічним API. PHP звіряє сумісність сигнатур за типами й кількістю параметрів, але не за іменами: клас, що реалізує інтерфейс, може назвати параметр як завгодно і завантажиться без жодної помилки. Тому виклик $gateway->charge(amount: 100) через тип-інтерфейс впаде в рантаймі на тій єдиній реалізації, де параметр названо $sum. Так само перейменування $str на $string у власній бібліотеці — це BC break, навіть якщо тип і позиція незмінні. Практичне правило: іменовані аргументи безпечні для конструкторів конкретних класів, DTO, атрибутів і вбудованих функцій (їх імена якраз і причесали в PHP 8.0 саме заради цього), а для поліморфних викликів через абстракцію надійніше лишатися позиційним.

У промоції своя пастка, і вона про порядок. Присвоєння промотованих властивостей відбувається до тіла конструктора, тому тіло вже працює з $this->…. Для звичайних властивостей це нічого не змінює, а для readonly (PHP 8.1+) означає, що властивість уже ініціалізована, і $this->text = trim($text) у тілі дасть Cannot modify readonly property. Трансформацію значень доводиться виносити або на рівень виклику — приватний конструктор плюс статичний fromRequest(), який чистить дані, — або в окремі value-обʼєкти, що нормалізують себе самі. Валідація ж без зміни значення (кинути InvalidArgumentException, прочитавши $this->perPage) у тілі конструктора цілком легальна.

Межі промоції варто називати списком: тільки __construct не-абстрактного класу, не варіадичний параметр, не тип callable (замість нього — Closure), не можна дублювати ту саму властивість у тілі класу, модифікатор видимості обовʼязковий. readonly доступний з 8.1, асиметрична видимість public private(set) — з 8.4. Атрибут перед промотованим параметром вішається і на параметр, і на властивість, а ReflectionProperty::isPromoted() дозволяє відрізнити такі властивості в рантаймі. Останнє — документація типів: узагальнені типи промотованих властивостей описують @param list<string> $tags у docblock конструктора, бо окремого місця для @var більше немає; при міграції старих DTO на promotion це найчастіша тиха втрата типів для PHPStan.

final class SearchQuery
{
    /** @param list<string> $tags */
    public function __construct(
        public readonly string $text,
        public readonly int $page = 1,
        public readonly int $perPage = 20,
        public readonly ?string $city = null,
        public readonly array $tags = [],
    ) {
        // тіло виконується ПІСЛЯ присвоєння — властивості вже заповнені
        if ($this->perPage > 100) {
            throw new InvalidArgumentException('perPage максимум 100');
        }
        // $this->text = trim($text);  // Error: readonly вже ініціалізовано промоцією
    }
}

// називаємо лише те, що відрізняється від типового; page і perPage пропускаємо
$q = new SearchQuery(text: 'php', city: 'Львів', tags: ['remote']);

// new SearchQuery(text: 'php', 3);   // Parse error: позиційний після іменованого
// new SearchQuery('php', text: 'x'); // Error: Named parameter $text overwrites previous argument

// розпакування масиву з РЯДКОВИМИ ключами як іменованих — PHP 8.1+
$filters = ['text' => 'php', 'perPage' => 50];
$q2 = new SearchQuery(...$filters);

// зайвий ключ ламає виклик, тому вхідні дані фільтруємо явно
// new SearchQuery(...['text' => 'php', 'sort' => 'new']); // Unknown named parameter $sort

function collect(...$args): array
{
    return $args;                     // іменовані аргументи стають рядковими ключами
}

var_dump(collect(1, 2));              // [0 => 1, 1 => 2]
var_dump(collect(a: 1, b: 2));        // ['a' => 1, 'b' => 2]
Що обидві фічі зʼявилися в PHP 8.0 і вирішують різні задачі: named arguments — про місце виклику, promotion — про місце оголошення.
Що після переходу на іменовані аргументи назва параметра стає частиною контракту: перейменування `$str` → `$string` — це BC break, хоч типи й порядок незмінні.
Що позиційний аргумент після іменованого — синтаксична помилка, а повторна передача того самого параметра позиційно й за іменем дає `Error: Named parameter $x overwrites previous argument`.
Що розпакування масиву з рядковими ключами (`f(...['page' => 2])`) як іменованих аргументів працює з PHP 8.1, а в 8.0 давало `Cannot unpack array with string keys`.
Що промоція присвоює властивості ДО виконання тіла конструктора, тому для `readonly` повторний запис у тілі вже неможливий — трансформацію треба робити в аргументі або у статичному конструкторі.
Що промотувати не можна все підряд: тільки в конструкторі не-абстрактного класу, не варіадичний параметр, не тип `callable`.
Вважати named arguments «просто цукром» і вільно перейменовувати параметри в публічних класах і в реалізаціях інтерфейсів — PHP не перевіряє сумісність імен при успадкуванні, і виклик за іменем впаде вже в рантаймі.
Писати `f(text: 'php', 3)` — позиційний аргумент після іменованого не парситься взагалі, це не рантайм-помилка.
Розраховувати, що `new Dto(...$request->all())` безпечний: зайвий ключ дає `Error: Unknown named parameter $sort`, тому масив треба явно фільтрувати або валідувати.
Дублювати промотовану властивість ще й у тілі класу (`private string $name;` + `private string $name` у конструкторі) — фатальна помилка «Cannot redeclare property».
Робити `$this->name = trim($name)` у тілі конструктора для промотованого `readonly`-параметра й дивуватись `Cannot modify readonly property`.
Промотувати параметри в класі з великою логікою ініціалізації й вважати, що це «звільняє» від валідації: перевірки в тілі конструктора нікуди не діваються, просто працюють уже з `$this->…`.
ПОРАДА

Формула на дві фрази: «promotion скорочує оголошення, named arguments — виклик; разом вони роблять DTO читабельним без білдера». І одразу назвіть ціну: «але імена параметрів після цього — публічний API, перейменування = BC break».

оновлено 3 вересня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

PHP перевіряє сумісність сигнатур за типами й кількістю параметрів, але не за їхніми іменами — реалізація може назвати параметр інакше без жодної помилки при завантаженні класу. Виклик `$gateway->charge(amount: 100)` впаде вже під час виконання, з `Error: Unknown named parameter $amount`, і тільки на конкретній реалізації.