MidТеория4 min

PHP 8.2

Readonly classes, standalone types true/false/null, DNF types, constants in traits, deprecated dynamic properties

PHP 8.2 — Readonly Classes и DNF Types

PHP 8.2 (декабрь 2022) продолжил развитие типовой системы и укрепление readonly-модели.

Readonly Classes

<?php

// Все свойства readonly класса автоматически readonly
readonly class Money
{
    public function __construct(
        public int $amount,
        public string $currency,
    ) {}

    public function add(Money $other): self
    {
        if ($this->currency !== $other->currency) {
            throw new DomainException('Currency mismatch');
        }
        return new self($this->amount + $other->amount, $this->currency);
    }
}

$price = new Money(1000, 'USD');
echo $price->amount;    // 1000
// $price->amount = 2000; // Error: Cannot modify readonly property

// Readonly класс идеален для DTO и Value Objects
readonly class Address
{
    public function __construct(
        public string $street,
        public string $city,
        public string $state,
        public string $zip,
        public string $country = 'US',
    ) {}
}

Ограничения readonly classes

<?php

readonly class Example
{
    // ❌ НЕЛЬЗЯ: нетипизированные свойства
    // public $value; // Fatal error: readonly class must have typed properties

    // ❌ НЕЛЬЗЯ: статические свойства
    // public static int $count; // Fatal error

    // ✅ МОЖНО: статические методы
    public static function create(): self
    {
        return new self();
    }

    // ✅ МОЖНО: константы
    public const VERSION = '1.0';
}

// ✅ МОЖНО: наследование readonly -> readonly
readonly class ChildReadonly extends Example {}

// ❌ НЕЛЬЗЯ: обычный наследует readonly
// class Regular extends Example {} // Fatal error

Для экзамена: readonly class требует, чтобы ВСЕ свойства были типизированы. Статические свойства запрещены. Наследование возможно только к readonly классу.


Standalone Types: true, false, null

<?php

// true, false, null теперь самостоятельные типы
function alwaysTrue(): true
{
    return true;
}

// Полезно для функций, возвращающих false при ошибке
function findInArray(array $haystack, mixed $needle): int|false
{
    $index = array_search($needle, $haystack);
    return $index; // int или false
}

// null как отдельный тип
function getHeader(string $name): string|null
{
    return $_SERVER['HTTP_' . strtoupper($name)] ?? null;
}

// false полезен для обратной совместимости встроенных функций
// strpos() возвращает int|false
// file_get_contents() возвращает string|false

Примеры использования

<?php

class Validator
{
    // Метод всегда возвращает true или бросает исключение
    public function validate(mixed $data): true
    {
        if (empty($data)) {
            throw new ValidationException('Data is empty');
        }
        return true;
    }
}

class Cache
{
    // null означает "нет в кэше" (не путать с кэшированным null)
    public function get(string $key): mixed
    {
        // Проблема: как отличить "нет в кэше" от "закэшировано null"?
        // Решение: использовать false как "не найдено"
    }

    public function has(string $key): bool
    {
        // Отдельная проверка существования
    }
}

DNF Types (Disjunctive Normal Form)

<?php

// DNF = комбинация union и intersection types
// Формат: (A&B)|C — "объект, реализующий A и B, ИЛИ объект типа C"

interface Countable {}
interface Serializable {}
class Collection {}

// Параметр: (Countable&Serializable)|Collection
function process((Countable&Serializable)|Collection $data): void
{
    if ($data instanceof Collection) {
        echo "Collection\n";
    } else {
        echo "Countable + Serializable\n";
    }
}

// С null
function find(string $key): (Cacheable&Serializable)|null
{
    // Возвращает объект, реализующий оба интерфейса, ИЛИ null
    return null;
}

// Правила DNF:
// - Intersection groups ДОЛЖНЫ быть в скобках
// - Скалярные типы НЕЛЬЗЯ в intersection
// - Дублирование типов запрещено

Практический пример

<?php

interface Renderable
{
    public function render(): string;
}

interface Cacheable
{
    public function getCacheKey(): string;
    public function getTtl(): int;
}

// Функция принимает:
// 1. Объект, реализующий и Renderable, и Cacheable — будет рендерить + кэшировать
// 2. Обычную строку — просто отобразит
function display((Renderable&Cacheable)|string $content): string
{
    if (is_string($content)) {
        return $content;
    }

    $key = $content->getCacheKey();
    $cached = cache()->get($key);

    if ($cached !== null) {
        return $cached;
    }

    $rendered = $content->render();
    cache()->set($key, $rendered, $content->getTtl());

    return $rendered;
}

Константы в трейтах

<?php

trait HasVersion
{
    // PHP 8.2+: трейты могут содержать константы
    public const VERSION = '1.0.0';
    protected const MIN_VERSION = '0.9.0';
    private const INTERNAL_VERSION = 42;

    public function getVersion(): string
    {
        return self::VERSION;
    }
}

class Application
{
    use HasVersion;
}

// Доступ через класс, НЕ через трейт
echo Application::VERSION; // '1.0.0'
// echo HasVersion::VERSION; // Error! Нельзя обращаться через трейт напрямую

// Конфликт констант
trait TraitA { public const X = 1; }
trait TraitB { public const X = 2; }

// class Conflict { use TraitA, TraitB; } // Fatal error: incompatible constants

Random\Randomizer

<?php

// Новый объектно-ориентированный API для генерации случайных данных
use Random\Randomizer;
use Random\Engine\Mt19937;
use Random\Engine\Secure;
use Random\Engine\Xoshiro256StarStar;

// Безопасный генератор (по умолчанию)
$random = new Randomizer();
echo $random->nextInt();              // Случайное целое
echo $random->getInt(1, 100);         // От 1 до 100
echo $random->getBytes(16);           // 16 случайных байт

// Перемешивание
$shuffled = $random->shuffleArray([1, 2, 3, 4, 5]);
$shuffledStr = $random->shuffleBytes('hello');

// Выбор случайных элементов
$picked = $random->pickArrayKeys([10, 20, 30, 40, 50], 2);
// Два случайных ключа

// Воспроизводимый генератор (для тестов)
$seeded = new Randomizer(new Mt19937(42));
echo $seeded->getInt(1, 100); // Всегда одно и то же число при seed=42

// Криптографически безопасный
$secure = new Randomizer(new Secure());
$token = bin2hex($secure->getBytes(32)); // 64-символьный hex-токен

Deprecated: Dynamic Properties

<?php

// PHP 8.2 DEPRECATES создание динамических свойств
class User
{
    public string $name;
}

$user = new User();
$user->name = 'Alice';   // OK — объявленное свойство

// Deprecated в PHP 8.2, Error в PHP 9.0
$user->age = 25;          // Deprecated: Creation of dynamic property

// Исключения:
// 1. stdClass — по-прежнему поддерживает динамические свойства
$obj = new stdClass();
$obj->anything = 'works'; // OK

// 2. Классы с __get/__set
class Flexible
{
    private array $data = [];

    public function __get(string $name): mixed
    {
        return $this->data[$name] ?? null;
    }

    public function __set(string $name, mixed $value): void
    {
        $this->data[$name] = $value;
    }
}

// 3. Атрибут #[AllowDynamicProperties]
#[\AllowDynamicProperties]
class LegacyModel
{
    // Динамические свойства разрешены
}

$model = new LegacyModel();
$model->custom = 'value'; // OK — без Deprecated

Подвох: Это DEPRECATION, не ошибка. В PHP 9.0 станет ErrorException. Многие legacy-библиотеки сломаются. Решение: #[AllowDynamicProperties] или рефакторинг.

Что ломается

<?php

// Типичные паттерны, которые ломаются:

// 1. Active Record с динамическими полями
class OldModel
{
    // Раньше можно было: $model->title = 'Hello';
    // Теперь нужно объявить свойства или использовать __set
}

// 2. Тесты с моками
// Некоторые mock-библиотеки создавали динамические свойства

// 3. Кэширование данных в объектах
class Service
{
    // Вместо $this->_cachedResult = $result;
    // Используйте объявленное свойство:
    private mixed $cachedResult = null;
}

Другие изменения PHP 8.2

Чувствительность к типу при приведении

<?php

// PHP 8.2 сделал deprecation для неявных приведений float -> int с потерей данных
$float = 3.9;

// Deprecated: Implicit conversion from float 3.9 to int loses precision
$arr = [];
$arr[3.9] = 'value'; // Ключ станет 3, потеря .9

Readonly свойства и clone

<?php

readonly class Point
{
    public function __construct(
        public float $x,
        public float $y,
    ) {}
}

$a = new Point(1.0, 2.0);
// $b = clone $a; // OK — клонирование работает
// Но изменить свойства клона нельзя (они readonly)

// Для изменяемого клонирования используйте with-pattern:
readonly class ImmutablePoint
{
    public function __construct(
        public float $x,
        public float $y,
    ) {}

    public function withX(float $x): self
    {
        return new self($x, $this->y);
    }

    public function withY(float $y): self
    {
        return new self($this->x, $y);
    }
}

Сводная таблица: PHP 8.2

Возможность Синтаксис
Readonly classes readonly class Money {}
true / false / null types function f(): true {}
DNF types (A&B)|C
Константы в трейтах trait T { const X = 1; }
Random\Randomizer new Randomizer()
Deprecated dynamic properties Используйте #[AllowDynamicProperties]

Типичные вопросы на экзамене

  1. Можно ли readonly класс наследовать обычным? Нет, только readonly наследует readonly.
  2. Могут ли readonly классы иметь статические свойства? Нет, только статические методы и константы.
  3. Что такое DNF? Disjunctive Normal Form — комбинация union и intersection: (A&B)|C.
  4. Можно ли обратиться к константе трейта через имя трейта? Нет, только через использующий класс.
  5. Что делает #[AllowDynamicProperties]? Разрешает создание необъявленных свойств без Deprecated warning.