PHP 8.4 — Property Hooks и Asymmetric Visibility
PHP 8.4 (ноябрь 2024) — революционный релиз с property hooks и асимметричной видимостью. Эти возможности фундаментально меняют работу со свойствами классов.
Property Hooks (get/set)
Базовый синтаксис
<?php
class User
{
// Property hook с get и set
public string $fullName {
get {
return "{$this->firstName} {$this->lastName}";
}
set (string $value) {
[$this->firstName, $this->lastName] = explode(' ', $value, 2);
}
}
public function __construct(
private string $firstName,
private string $lastName,
) {}
}
$user = new User('Alice', 'Smith');
echo $user->fullName; // "Alice Smith" — вызывает get hook
$user->fullName = 'Bob Johnson'; // Вызывает set hook
echo $user->fullName; // "Bob Johnson"
Короткий синтаксис (arrow)
<?php
class Temperature
{
// Короткий синтаксис для однострочных hooks
public float $celsius {
get => $this->fahrenheit * (5 / 9) - 32 * (5 / 9);
set => $this->fahrenheit = $value * 9 / 5 + 32;
}
public function __construct(
private float $fahrenheit,
) {}
}
// Только get (вычисляемое свойство)
class Circle
{
public float $area {
get => M_PI * $this->radius ** 2;
// Нет set — свойство только для чтения
}
public function __construct(
public float $radius,
) {}
}
$circle = new Circle(5.0);
echo $circle->area; // 78.5398...
// $circle->area = 100; // Error: Property Circle::$area is read-only
Валидация через set hook
<?php
class Product
{
public string $name {
set (string $value) {
if (strlen($value) < 2) {
throw new InvalidArgumentException('Name too short');
}
if (strlen($value) > 255) {
throw new InvalidArgumentException('Name too long');
}
$this->name = trim($value); // Присвоение backing value
}
}
public int $price {
set (int $value) {
if ($value < 0) {
throw new DomainException('Price cannot be negative');
}
$this->price = $value;
}
}
public string $email {
set (string $value) {
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email');
}
$this->email = strtolower($value);
}
}
public function __construct(
string $name,
int $price,
string $email,
) {
$this->name = $name; // Вызывает set hook
$this->price = $price; // Вызывает set hook
$this->email = $email; // Вызывает set hook
}
}
$product = new Product('Widget', 1999, '[email protected]');
echo $product->email; // "[email protected]" — нормализовано через set hook
Сокращённый set
<?php
class User
{
// Если set hook не присваивает явно, значение присваивается автоматически
// из того, что возвращает set
public string $name {
set (string $value) => trim($value);
// Эквивалентно: set (string $value) { $this->name = trim($value); }
}
public string $email {
set (string $value) => strtolower(trim($value));
}
// $value — имя параметра по умолчанию, можно не указывать тип
public string $slug {
set => strtolower(preg_replace('/[^a-z0-9]+/i', '-', $value));
}
}
Property hooks и интерфейсы
<?php
// Интерфейсы могут требовать свойства с hooks
interface HasFullName
{
// Свойство должно быть readable
public string $fullName { get; }
}
interface Nameable
{
// Свойство должно быть readable и writable
public string $name { get; set; }
}
class Person implements HasFullName, Nameable
{
public string $fullName {
get => "{$this->firstName} {$this->lastName}";
}
public string $name {
get => $this->firstName;
set => $this->firstName = $value;
}
public function __construct(
private string $firstName,
private string $lastName,
) {}
}
Property hooks и constructor promotion
<?php
class User
{
public function __construct(
// Hooks прямо в constructor promotion
public string $name { set => trim($value); },
public string $email { set => strtolower(trim($value)); },
public int $age {
set {
if ($value < 0 || $value > 150) {
throw new InvalidArgumentException('Invalid age');
}
$this->age = $value;
}
},
) {}
}
$user = new User(' Alice ', ' [email protected] ', 25);
echo $user->name; // "Alice"
echo $user->email; // "[email protected]"
Ограничения property hooks
<?php
class Example
{
// ❌ НЕЛЬЗЯ: hooks на статических свойствах
// public static string $name { get => 'test'; }
// ❌ НЕЛЬЗЯ: readonly + hooks
// public readonly string $name { get => 'test'; }
// ❌ НЕЛЬЗЯ: ссылка на свойство с hooks
// $ref = &$obj->hookedProperty; // Error
// ✅ МОЖНО: abstract hooks в абстрактных классах
}
abstract class AbstractModel
{
// Абстрактное свойство с hooks
abstract public string $id { get; }
}
Asymmetric Visibility (Асимметричная видимость)
<?php
class User
{
// Чтение: public, Запись: private
public private(set) string $name;
// Чтение: public, Запись: protected
public protected(set) int $age;
// Чтение: protected, Запись: private
protected private(set) string $internalId;
public function __construct(string $name, int $age)
{
$this->name = $name; // OK — из своего класса
$this->age = $age;
$this->internalId = uniqid();
}
}
$user = new User('Alice', 30);
echo $user->name; // OK — public get
echo $user->age; // OK — public get
// $user->name = 'Bob'; // Error: Cannot modify private(set) property from outside
// $user->age = 31; // Error: Cannot modify protected(set) property from outside
// Наследник:
class Admin extends User
{
public function updateAge(int $age): void
{
$this->age = $age; // OK — protected(set) доступен наследникам
// $this->name = 'New'; // Error — private(set)
}
}
Asymmetric visibility с constructor promotion
<?php
// Самый элегантный синтаксис
readonly class Product
{
public function __construct(
public readonly int $id,
public private(set) string $name,
public protected(set) float $price,
) {}
}
// DTO с контролируемой записью
class OrderDto
{
public function __construct(
public private(set) int $id,
public private(set) string $status,
public private(set) float $total,
public private(set) \DateTimeImmutable $createdAt,
) {}
// Только методы класса могут менять состояние
public function markAsPaid(): void
{
$this->status = 'paid';
}
}
$order = new OrderDto(1, 'pending', 99.99, new \DateTimeImmutable());
echo $order->status; // "pending" — чтение OK
// $order->status = 'paid'; // Error!
$order->markAsPaid();
echo $order->status; // "paid"
Правила асимметричной видимости
<?php
class Rules
{
// Видимость set ДОЛЖНА быть <= видимости get
public private(set) string $a; // OK: public > private
public protected(set) string $b; // OK: public > protected
protected private(set) string $c; // OK: protected > private
// ❌ НЕЛЬЗЯ: set шире чем get
// private public(set) string $d; // Error: set шире чем get
// Одинаковая видимость — используйте обычный синтаксис
// public public(set) string $e; // Бессмысленно, используйте: public string $e;
}
new без скобок
<?php
// PHP 8.4: можно вызывать методы/свойства на new без скобок
// До PHP 8.4
$name = (new User('Alice'))->getName();
$date = (new \DateTime())->format('Y-m-d');
// PHP 8.4+
$name = new User('Alice')->getName();
$date = new \DateTime()->format('Y-m-d');
// Цепочки вызовов
$result = new Builder()
->setTable('users')
->where('active', true)
->orderBy('name')
->get();
// С конструктором без аргументов — скобки не нужны вообще
$items = new Collection->add('first')->add('second')->toArray();
Новые функции для массивов
<?php
$users = [
['name' => 'Alice', 'age' => 25, 'active' => true],
['name' => 'Bob', 'age' => 30, 'active' => false],
['name' => 'Charlie', 'age' => 35, 'active' => true],
];
// array_find() — находит первый элемент, соответствующий условию
$first = array_find($users, fn($u) => $u['age'] > 28);
// ['name' => 'Bob', 'age' => 30, 'active' => false]
// array_find_key() — находит ключ первого совпадения
$key = array_find_key($users, fn($u) => $u['age'] > 28);
// 1
// array_any() — хотя бы один элемент соответствует
$hasInactive = array_any($users, fn($u) => !$u['active']);
// true
// array_all() — все элементы соответствуют
$allActive = array_all($users, fn($u) => $u['active']);
// false
// Практический пример
$cart = [
['product' => 'Laptop', 'price' => 999, 'quantity' => 1],
['product' => 'Mouse', 'price' => 25, 'quantity' => 2],
['product' => 'Keyboard', 'price' => 75, 'quantity' => 1],
];
$hasExpensiveItem = array_any($cart, fn($item) => $item['price'] > 500);
$allInStock = array_all($cart, fn($item) => $item['quantity'] > 0);
$firstCheap = array_find($cart, fn($item) => $item['price'] < 50);
Lazy Objects
<?php
// Ленивые объекты — создаются, но инициализируются только при первом обращении
// Полезно для DI-контейнеров и ORM (lazy loading)
class HeavyService
{
public function __construct()
{
// Дорогая инициализация: подключения, загрузка конфигов
sleep(2); // Имитация
}
public function process(): string
{
return 'processed';
}
}
// Создание lazy ghost
$reflector = new ReflectionClass(HeavyService::class);
$lazy = $reflector->newLazyGhost(function (HeavyService $obj) {
// Initializer вызывается при первом обращении к свойству/методу
$obj->__construct();
});
// Объект создан, но конструктор НЕ вызван
var_dump($lazy instanceof HeavyService); // true
// Только сейчас вызывается конструктор:
echo $lazy->process(); // "processed"
// Lazy proxy — другой вариант
$proxy = $reflector->newLazyProxy(function () {
// Возвращает реальный объект
return new HeavyService();
});
Практический пример: DI Container
<?php
class Container
{
private array $factories = [];
private array $instances = [];
public function register(string $class, callable $factory): void
{
$this->factories[$class] = $factory;
}
public function get(string $class): object
{
if (isset($this->instances[$class])) {
return $this->instances[$class];
}
$reflector = new ReflectionClass($class);
// Создаём lazy proxy — реальный объект создаётся только при использовании
$lazy = $reflector->newLazyProxy(function () use ($class) {
return ($this->factories[$class])($this);
});
$this->instances[$class] = $lazy;
return $lazy;
}
}
HTML5 DOM API
<?php
// Новый DOM API с поддержкой HTML5
use Dom\HTMLDocument;
// Парсинг HTML5
$doc = HTMLDocument::createFromString('<div class="wrapper"><p>Hello</p></div>');
$doc = HTMLDocument::createFromFile('page.html');
// querySelector / querySelectorAll — как в браузере!
$element = $doc->querySelector('.wrapper p');
echo $element->textContent; // "Hello"
$items = $doc->querySelectorAll('li.active');
foreach ($items as $item) {
echo $item->textContent . "\n";
}
// Создание элементов
$div = $doc->createElement('div');
$div->classList->add('container');
$div->setAttribute('id', 'main');
$div->textContent = 'Content';
// Сериализация
echo $doc->saveHTML(); // Корректный HTML5 (не XHTML как раньше)
Отличие от старого DOMDocument
<?php
// Старый API — XML-ориентированный, проблемы с HTML5
$old = new DOMDocument();
$old->loadHTML('<div>Test</div>'); // Warning: unknown tags (header, nav, section)
// Новый API — полная поддержка HTML5
$new = Dom\HTMLDocument::createFromString(
'<header><nav><a href="/">Home</a></nav></header>'
);
// Без warnings, корректный парсинг HTML5-тегов
// Новые свойства
echo $element->className; // Вместо getAttribute('class')
echo $element->id; // Вместо getAttribute('id')
echo $element->innerHTML; // Содержимое элемента как HTML
echo $element->outerHTML; // Элемент с обёрткой
BCMath с объектами
<?php
// Новый класс BcMath\Number для object-oriented арифметики
use BcMath\Number;
$a = new Number('123.456');
$b = new Number('789.012');
// Операторы
$sum = $a + $b; // Number('912.468')
$diff = $a - $b; // Number('-665.556')
$product = $a * $b; // Number('97408.265472')
$quotient = $a / $b; // Number('0.156...')
// Сравнение
if ($a < $b) {
echo "a is less than b\n";
}
// Точность
$result = new Number('1') / new Number('3');
echo $result; // С настраиваемой точностью
Сводная таблица: PHP 8.4
| Возможность | Синтаксис |
|---|---|
| Property hooks | public string $name { get => ...; set => ...; } |
| Asymmetric visibility | public private(set) string $name |
new без скобок |
new User('Alice')->getName() |
array_find() |
array_find($arr, fn($x) => $x > 5) |
array_find_key() |
array_find_key($arr, fn($x) => $x > 5) |
array_any() |
array_any($arr, fn($x) => $x > 5) |
array_all() |
array_all($arr, fn($x) => $x > 5) |
| Lazy objects | ReflectionClass::newLazyGhost() |
| HTML5 DOM | Dom\HTMLDocument::createFromString() |
| BCMath objects | new BcMath\Number('123.456') |
Типичные вопросы на экзамене
- Можно ли сочетать readonly и property hooks? Нет, это несовместимо.
- Может ли set hook быть шире по видимости, чем get? Нет,
private public(set)вызовет ошибку. - Что вернёт
array_find(), если ничего не найдено?null. - Чем lazy ghost отличается от lazy proxy? Ghost инициализирует сам объект, proxy делегирует к другому объекту.
- Можно ли взять ссылку на свойство с hooks? Нет,
&$obj->hookedPropвызовет ошибку. - Что делает short set hook? Возвращённое значение автоматически присваивается свойству:
set => trim($value).