HardТеория7 min

PHP 8.4

Property hooks, asymmetric visibility, new без скобок, array_find/all/any, lazy objects, HTML5 DOM

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')

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

  1. Можно ли сочетать readonly и property hooks? Нет, это несовместимо.
  2. Может ли set hook быть шире по видимости, чем get? Нет, private public(set) вызовет ошибку.
  3. Что вернёт array_find(), если ничего не найдено? null.
  4. Чем lazy ghost отличается от lazy proxy? Ghost инициализирует сам объект, proxy делегирует к другому объекту.
  5. Можно ли взять ссылку на свойство с hooks? Нет, &$obj->hookedProp вызовет ошибку.
  6. Что делает short set hook? Возвращённое значение автоматически присваивается свойству: set => trim($value).

Связанные темы