PHP 8.6: Readonly свойства по-умолчанию
Помните старый пост про про добавление функций array_first() и array_last() в PHP 8.5?
Кратко - "ввод в эксплуатация" задуманного ранее функционала спустя несколько лет и данная тенденция радует, что core разработчики PHP в последние годы периодически пересматривают это с учетом реализованного нового.
Так вот, так это и получилось с "допиливанием" readonly свойств,
Как мы помним когда в PHP 8.1 появились readonly properties, у них была довольно простая модель:
class User
{
public readonly string $name;
public function __construct(string $name)
{
$this->name = $name;
}
}Свойство можно инициализировать один раз. А при попытке перезаписать значение:
$user->name = 'Sergey';получаем ошибку.
Возможно много кто пытался сделать подобное:
class User
{
public readonly string $role = 'user';
}И закономерно получал:
Readonly property User::$role cannot have default valueПотому что было ограничение, в оригинальном RFC по свойствам только для чтения от 2021 года это было прямо указано:
Поскольку значение по умолчанию считается инициализирующим присваиванием, свойство только для чтения со значением по умолчанию по сути эквивалентно константе и, следовательно, не особенно полезно. В будущем эта концепция может стать более полезной, если в качестве значений по умолчанию для свойств будут разрешены новые выражения.
И вот будущее наступило 😀 - в PHP 8.6 появилась небольшая, но довольно приятная возможность - readonly свойства теперь могут иметь значение по умолчанию.
В PHP 8.6 это будет уже корректный код:
class User
{
public readonly string $role = 'user';
}А все благодаря тому, что в PHP 8.4 появились свойства в интерфейсах и property hooks. Так как теперь интерфейс может описывать не только методы но и свойства:
interface UserInterface
{
public string $name { get; }
}А контракт буквально говорит что у объекта должно быть свойство $name, которое можно прочитать, то и const здесь уже ни к месту.
До PHP 8.6
Чтобы реализовать такой контракт с фиксированным значением, приходилось писать примерно так:
final class User implements UserInterface
{
public readonly string $name;
public function __construct()
{
$this->name = 'Sergey';
}
}Конструктор в данном случае нужен только для одного присваивания.
Особой пользы от него нет.
После PHP 8.6
Теперь это сделать становитс проще:
final class User implements UserInterface
{
public readonly string $name = 'Sergey';
}И это уже выглядит намного логичнее.
Особенно если речь идёт о классах, которые фактически являются описанием какой-либо конфигурации или структуры.
Например:
interface Importer
{
public string $name { get; }
public string $topic { get; }
public array $handlers { get; }
}Реализация:
final readonly class ContragentImporter implements Importer
{
public string $name = 'Contragent';
public string $topic = 'contragents';
public array $handlers = [
ValidateMessage::class,
ImportToOneC::class,
SendResult::class,
];
}Получается практически декларативное описание:
Без конструктора.
Без getter'ов.
Без возможности случайно изменить значение.
Но есть некоторые нюансы, которые следует учитывать. Вот это код теперь работать не будет:
final readonly class User
{
public string $role = 'user';
public function __construct(string $role)
{
$this->role = $role;
}
}Потому что к моменту выполнения конструктора $role уже инициализировано значением user. И попытка заново перезаписать значение не увенчается успехом.
То есть:
public string $role = 'user';означает уже не:
если значение не передали, используй
user
А:
сразу установи
userи больше это свойство не изменяй
Это, пожалуй, главное, что нужно запомнить.
А вот если нужен именно значение по умолчанию для конструктора, тогда будет работать привычная запись типа:
final readonly class User
{
public function __construct(
public string $role = 'user',
) {}
}Здесь user - дефолт значение аргумента конструктора.
Соответственно:
new User();
//role = user
new User('admin');
//new User('admin');
readonly всё ещё остаётся readonly
Добавление дефолтного значения по умолчанию никак не меняет правила доступа.
Можно читать:
$user->role;Нельзя писать:
$user->role = 'admin';И нельзя превратить property в обычное mutable property через интерфейс.
Например:
interface UserInterface
{
public string $role { get; set; }
}readonly реализация такой контракт не удовлетворит и компилятор сообщит вам об этом. Это так и задумано.
Наследование тоже работает
Можно переопределить дефолт значение в наследнике, если соблюдаются обычные правила совместимости свойств:
class User
{
public readonly string $role = 'user';
}
class Admin extends User
{
public readonly string $role = 'admin';
}(new User())->role; // user
(new Admin())->role; // adminИ здесь появляется ещё одна интересная особенность, ка помните в PHP 8.5 появился clone with, который очень хорошо сочетается с readonly объектами. Например можно получить новый объект с изменённым значением:
readonly class User
{
public function __construct(
public string $name,
public string $role,
) {}
}$admin = clone($user, [
'role' => 'admin',
]);Исходный объект при этом не меняется.
В результате получается, что последние версии PHP постепенно складывают довольно приятную модель работы с immutable objects:
readonly
+
clone with
+
property hooks / interface properties
+
readonly property defaultsИ всё это позволяет писать довольно декларативный код без большого количества "портянок бойлерплейта".
Зачем это вообще нужно?
На мой взгляд, главное применение - это классы, которые описывают что-либо, а не выполняют сложную логику.
Например:
конфигурация;
метадаты;
DTO;
value objects;
registry;
описания обработчиков;
различные blueprint-классы;
реализации интерфейсов с фиксированными значениями.
Получилось интересное изменение Разрешили то что искусственно запретили ранее:
// PHP < 8.6
❌ public readonly string $foo = 'bar';
// PHP 8.6
✅ public readonly string $foo = 'bar'; Что нужно делать? Да получается, ничего, до релиза в ноябре.
Единственная задача, стоящая перед экосистемой, которая явно указана в разделе влияния RFC, - это инструменты: IDE (тут например JetBrains уже реагируют), статические анализаторы, которые в настоящее время будут ругаться при попытке присовить значение по умолчанию readonly свойству,

Что думаешь?