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