PHP 18 ~ 4 мин.

PHP 8.6: Readonly свойства по-умолчанию

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 свойству,

Что думаешь?

Категории
  • PHP 70
  • Заметки 18
  • Безопасность 4
  • Флуд 2
  • Nginx 2
  • ИТ новости 2
  • Видео 1
  • Docker 1
  • Roadmap 1
  • Архитектура 0

Хочешь поддержать сайт?

Делаем из мухи слона

sergeymukhin.com

персональный блог о веб-разработке от Сергея Мухина. Блог был основан в 2018 году, и собирался уделять основное внимание последним тенденциям, учебным пособиям, а также советам и рекомендациям, позволяющим начинающим девелоперам встать быстрее на правильную дорогу веб разработки, но что-то пошло не так 😃

Релизы PHP 8.5

Дата Релиз
3 Июля 2025 Альфа 1
17 Июля 2025 Альфа 2
31 Июля 2025 Альфа 3 пропущена
31 Июля 2025 Альфа 4
12 Августа 2025 Feature freeze
14 Августа 2025 Бета 1
28 Августа 2025 Бета 2
11 Сентября 2025 Бета 3
25 Сентября 2025 RC 1
09 Октября 2025 RC 2
23 Октября 2025 RC 3
06 Ноября 2025 RC 4
20 Ноября 2025 GA

Что нового?