PHP — це суцільний хаос. Давайте визнаємо це одразу.
Я люблю цю мову, але деякі її аспекти відверто дратують. Яскравий приклад — необхідність обирати між властивостями readonly та властивостями з приватним доступом на запис. Поясню, у чому проблема.
Властивості readonly з'явилися в PHP 8.1. Вони дозволяють встановити значення лише один раз, після чого змінити його неможливо:
final class Book { public function __construct( public readonly string $title, ) {} }
$book = new Book('Timeline Taxi'); $book->title = 'Timeline Taxi 2';
До слова, PHP дозволяє створювати об'єкт без миттєвого присвоєння значення readonly властивості. Валідність перевіряється лише під час читання — це частина логіки типізованих властивостей, закладеної ще в PHP 7.4:
final class Book { public readonly string $title; } $book = new Book();
echo $book->title; // Присвоєння значення після конструктора працює коректно: $book->title = 'Timeline Taxi';
Потім у PHP 8.4 з'явилася «асиметрична видимість». Вона дозволяє встановлювати різні рівні доступу (public, protected або private) залежно від операції: читання (get) чи запису (set).
Наприклад, ви можете визначити властивість як private(set):
final class Book { public function __construct( private(set) string $title, ) {} }
private(set) означає, що властивість доступна для читання ззовні, але змінювати її можна лише всередині класу. Це скорочення від public private(set). Тобто ви можете змінити заголовок книги через метод класу:
final class Book { public function __construct( private(set) string $title, ) {} public function markDraft(): self { // Зміна всередині класу дозволена $this->title .= ' (Draft)'; return $this; } }
Але спроба змінити її ззовні призведе до помилки:
$book = new Book('Timeline Taxi'); $book->title .= ' (Draft)';
Чому це важливо? Здавалося б, це різні інструменти: один запобігає будь-яким змінам після ініціалізації, інший — обмежує коло тих, хто може ці зміни вносити.
Коли з'явився readonly (за три роки до асиметричної видимості), його масово почали використовувати для створення data objects — структур, що передають типізовані дані в системі. Це надзвичайно зручний патерн, про який я писав ще у 2018 році.
Завдяки readonly ми отримали можливість створювати класи з публічними властивостями без геттерів та сеттерів. Оскільки дані все одно неможливо змінити, навіщо писати зайвий шаблонний код (boilerplate)?
final class Book { public function __construct( public readonly string $title, public readonly Author $author, public readonly ChapterCollection $chapters, public readonly Publisher $publisher, public readonly null|DateTimeImmutable $publishedAt = null, ) {} }
Цей підхід став настільки популярним, що в PHP 8.2 додали синтаксис для цілих readonly класів:
final readonly class Book { public function __construct( public string $title, public Author $author, public ChapterCollection $chapters, public Publisher $publisher, public null|DateTimeImmutable $publishedAt = null, ) {} }
Але тепер ми маємо PHP 8.4 з асиметричною видимістю. І хоча технічно це інша функція, вона дозволяє досягти того самого результату — захисту об'єкта від зовнішніх втручань — просто позначивши властивості як private(set):
final class Book { public function __construct( private(set) string $title, private(set) Author $author, private(set) ChapterCollection $chapters, private(set) Publisher $publisher, private(set) null|DateTimeImmutable $publishedAt = null, ) {} }
Можна стверджувати, що асиметрична видимість краща за readonly, бо вона гнучкіша — ви все ще можете змінювати дані всередині класу.
Крім того, іноді виникає потреба змінити readonly об'єкт шляхом копіювання даних у новий екземпляр. На жаль, у PHP досі немає повноцінного виразу clone with для перезапису таких властивостей під час клонування. Навіть заплановані зміни в clone для PHP 8.5 не вирішують цю проблему повністю. Це досить глибока тема, про яку я зняв окреме відео:
Заперечувати марно: для об'єктів даних readonly програє асиметричній видимості. Проблема в тому, що readonly вже три роки як стандарт і використовується всюди. Я помічаю, як у новому коді (навіть у моєму власному фреймворку Tempest) з'являється асиметрична видимість, тоді як старі частини залишаються на readonly. Це створює плутанину. Між readonly та private(set) є семантичні відмінності, особливо в питанні клонування, але для типових задач вони виглядають взаємозамінними.
То що тепер? Замінити readonly на private(set) всюди? Чи варто депрекувати readonly у майбутньому? Чи, можливо, ігнорувати нову фічу заради звички? Як новачкам зрозуміти, що саме обрати?
Найгірше те, що ми це передбачали. Коли асиметрична видимість була лише ідеєю, я писав, як ці дві функції конфліктуватимуть між собою та плутатимуть розробників роками. Мені більше до вподоби асиметрична видимість, але її поява після readonly створює безлад.
Іронія в тому, що всі ці нововведення — readonly властивості та класи, асиметрична видимість, constructor property promotion — це лише милиці. Нам просто потрібні нормальні struct для типізованого представлення даних.
struct Book { string $title; Author $author; ChapterCollection $chapters; Publisher $publisher; null|DateTimeImmutable $publishedAt = null; }
Я впевнений, що з радістю обміняв би всі ці складні фічі на прості та зрозумілі struct.
Так, PHP — це хаос. Прекрасний, улюблений хаос, який я ні на що не проміняю.
Але все ж таки, який же це хаос.