self против static, enum и readonly

Три темы, которыми отличают тех, кто пишет на современном PHP, от тех, кто застрял в 5.6.

Late static binding (позднее статическое связывание) — механизм, при котором ключевое слово static внутри метода означает «класс, у которого вызвали метод», а не «класс, где метод написан».

Вопрос 1: «Чем self отличается от static

Что проверяет интервьюер

Этот вопрос — про понимание того, как PHP разрешает статический контекст. self связывается на этапе компиляции с тем классом, в теле которого написан. static связывается во время вызова с реальным классом-получателем. Разница видна сразу, как только появляется наследование.

<?php
class Model {
    public static function create(): static {
        return new static();
    }
    public static function createSelf(): self {
        return new self();
    }
}

class User extends Model {}

echo get_class(User::create()), "\n";
echo get_class(User::createSelf()), "\n";

Вывод:

User
Model

Именно на этом механизме держатся фабричные методы ORM: User::query() из Eloquent возвращает объект нужного класса, а не базового. Тот же принцип применим к static:: при вызове методов и констант — он ищет реализацию у класса-получателя, позволяя потомку переопределить поведение.

Полезно упомянуть и третье ключевое слово — parent::, а также то, что static с PHP 8.0 можно использовать как тип возвращаемого значения, что делает fluent-интерфейсы типобезопасными.

Вопрос 2: «Зачем нужны enum и чем они лучше констант?»

До PHP 8.1 набор допустимых значений описывали константами класса — и ничто не мешало передать в метод произвольную строку. enum делает недопустимое состояние непредставимым: тип-подсказка Status примет только один из объявленных вариантов.

<?php
enum Status: string {
    case Draft     = 'draft';
    case Published = 'published';

    public function label(): string {
        return match($this) {
            Status::Draft     => 'Черновик',
            Status::Published => 'Опубликовано',
        };
    }
}

$s = Status::from('draft');
echo $s->name, " ", $s->value, " ", $s->label(), "\n";
var_dump(Status::tryFrom('deleted'));
var_dump($s === Status::Draft);
echo count(Status::cases()), "\n";

Вывод:

Draft draft Черновик
NULL
bool(true)
2

Что важно назвать в ответе:

  • Enum бывают чистые (без значений) и backed (с типом string или int) — последние удобно хранить в базе.
  • from() бросает ValueError для неизвестного значения, tryFrom() возвращает null. Для данных извне берут tryFrom().
  • Кейсы — синглтоны, поэтому их корректно сравнивать через ===.
  • Enum может иметь методы, константы и реализовывать интерфейсы, но не может иметь состояния: свойств у него нет и new для него запрещён.

Вопрос 3: «Что даёт readonly

readonly-свойство (PHP 8.1) можно инициализировать ровно один раз — из области видимости класса — и после этого любая попытка записи бросает Error. Это дешёвый способ получить неизменяемые объекты-значения.

<?php
final class Money {
    public function __construct(
        public readonly int $amount,
        public readonly string $currency,
    ) {}

    public function add(int $delta): static {
        return new static($this->amount + $delta, $this->currency);
    }
}

$m = new Money(100, 'RUB');
echo $m->amount, " ", $m->currency, "\n";

try {
    $m->amount = 500;
} catch (Error $e) {
    echo "Error: ", $e->getMessage(), "\n";
}

echo $m->add(50)->amount, "\n";

Вывод:

100 RUB
Error: Cannot modify readonly property Money::$amount
150

Обратите внимание на приём: метод add() не меняет объект, а возвращает новый — так работают все неизменяемые типы. Уточнения, которые отличают сильного кандидата: readonly требует объявленного типа свойства; свойство нельзя переинициализировать даже внутри класса; а сама «неизменяемость» поверхностная — если внутри лежит массив, его нельзя заменить, но объект по ссылке внутри может менять своё состояние. В PHP 8.2 появилась возможность помечать readonly класс целиком.

Типичные ошибки кандидатов

  • Считают self и static синонимами — до первого наследника фабричного метода.
  • Думают, что enum — это просто набор констант, и пытаются создать его через new.
  • Используют from() для данных из запроса и получают необработанный ValueError вместо 400-й ошибки.
  • Ожидают, что readonly сделает объект глубоко неизменяемым.
  • Пытаются присвоить readonly-свойству значение по умолчанию в объявлении — это запрещено.

Как ответить кратко

«self жёстко указывает на класс, где написан код, static — на класс, у которого метод вызвали: это late static binding, на нём строятся фабрики в ORM. enum из 8.1 задаёт закрытый набор значений, backed-варианты умеют from/tryFrom, кейсы — синглтоны и сравниваются через ===. readonly разрешает одну инициализацию и превращает объект в неизменяемое значение — изменения возвращаются новым экземпляром».

Проверьте себя
1. class Model { public static function create(): static { return new static(); } } class User extends Model {}. Что вернёт get_class(User::create())?
AModel
Bstatic
CФатальная ошибка
DUser
2. Чем tryFrom() отличается от from() у backed enum?
AtryFrom() возвращает null для неизвестного значения, from() бросает ValueError
BtryFrom() работает только с int, from() — со string
CОни идентичны, tryFrom() — устаревший псевдоним
DtryFrom() создаёт новый кейс, если такого нет
3. Что произойдёт при попытке изменить readonly-свойство снаружи после инициализации?
AЗначение изменится, readonly действует только внутри класса
BБудет брошен Error: Cannot modify readonly property
CБудет выведено предупреждение, значение не изменится
DСвойство станет null