Что нового в PHP 8.4: разбор обновлений версии

Вышедшая 21 ноября 2024 года версия PHP 8.4 внесла ряд значительных улучшений, открывая новую главу в развитии языка. В этой статье мы рассмотрим ключевые изменения и приведем примеры практического применения.

Хуки свойств

Первым обновлением, которое мы рассмотрим, станут хуки свойств — механизм, позволяющий перехватывать чтение и запись в объектах без необходимости писать геттеры и сеттеры вручную.

Традиционно для установки и получения значений свойств, а также для выполнения дополнительных операций над ними используются публичные методы. Такой подход широко применяется, например, в моделях сущностей популярных фреймворков:

class User extends Model
{
    private string $name;

    public function getName(): string
    {
        return ucfirst($this->name);
    }

    public function setName(string $name): void
    {
        $this->name = trim($name);
    }
}

С внедрением хуков свойств в PHP 8.4 такие методы (геттеры и сеттеры) могут быть заменены на хуки get и set. Эти хуки автоматически вызываются при обращении к свойству: get — при чтении значения, set — при его изменении.

Хуки get и set создаются следующим образом. После имени публичного свойства открываются фигурные скобки, в пределах которых вставляются хуки. Хуки также содержат собственные фигурные скобки, где определяется логика при обращении со свойством. После выполнения операции хук get должен вернуть результирующее значение, а хук set — обновить значение свойства.

В примере ниже мы перехватываем обращение к свойству $cryptoPair. При сохранении значения в это свойство мы валидируем новую строку — проверяем, является ли строка криптопарой. Если да, то разбиваем пару на два значения и сохраняем в свойствах $firstCoin и $secondCoin. При получении криптопары из свойства $cryptoPair мы предварительно форматируем значения, используя сохраненные тикеры:

class Crypto
{
    public const AVAILABLE_COINS = ['BTC', 'ETH', 'USDT', 'BNB', 'XRP', 'ADA', 'DOGE', 'SOL'];

    public function __construct(private string $firstCoin, private string $secondCoin)
    {
    }

    public string $cryptoPair
    {
        get {
            return sprintf('%s/%s', $this->firstCoin, $this->secondCoin);
        }
        // Запись хука можно сократить:
        // get => sprintf('%s/%s', $this->firstCoin, $this->secondCoin);
        
        set (string $value) {
            if (!str_contains($value, '/')) {
                throw new InvalidArgumentException('The crypto pair is not correct.');
            }
            [$firstCoin, $secondCoin] = explode('/', $value, 2);

            $isFirstCoinAvailable = in_array(strtoupper($firstCoin), self::AVAILABLE_COINS, true);
            $isSecondCoinAvailable = in_array(strtoupper($secondCoin), self::AVAILABLE_COINS, true);
            if (!$isFirstCoinAvailable || !$isSecondCoinAvailable) {
                throw new InvalidArgumentException('The coin is not available.');
            }

            $this->firstCoin = strtoupper($firstCoin);
            $this->secondCoin = strtoupper($secondCoin);
        }
    }
}

$crypto = new Crypto('BTC', 'USDT');
echo $crypto->cryptoPair; // BTC/USDT

$crypto->cryptoPair = 'eth/xrp';
echo $crypto->cryptoPair; // ETH/XRP

Асимметричная область видимости свойств

Асимметричная видимость свойств позволяет более гибко управлять доступом к свойствам классов. В отличие от традиционной симметричной видимости, где модификаторы доступа (public, protected, private) применяются как для чтения, так и для записи, асимметричная видимость позволяет устанавливать разные уровни доступа для этих операций.

Благодаря этому нововведению, в PHP 8.4 для каждого свойства можно задавать область видимости отдельно как для чтения, так и для записи. Это осуществляется с помощью добавления новых модификаторов записи private(set), protected(set) и public(set).

В примере ниже свойство $author доступно для чтения из любого места (public), но его можно изменять только внутри класса Book (private(set)):

class Book {
    public private(set) Author $author;
}

Так как по умолчанию область видимости считается public, пример выше можно записать в сокращенном виде:

class Book {
    private(set) Author $author; // То же самое, что и public private(set)
}

Пример со свойством асимметричной видимости $area, где демонстрируется вычисление площади круга:

class Circle {
    private float $radius;
    public private(set) float $area;

    public function __construct(float $radius) {
        $this->radius = $radius;
        $this->area = pi() * ($this->radius ** 2); // Вычисление площади круга
    }
}

$circle = new Circle(5);
echo 'Площадь: ' . $circle->area; // Площадь: 78.539816339745
$circle->area = 100; // Ошибка, так как свойство недоступно для записи извне

Атрибут Deprecated

В PHP 8.4 представлен новый атрибут #[Deprecated], который позволяет помечать функции, методы и константы классов, а также элементы перечислений как устаревшие. При использовании элемента, помеченного атрибутом #[Deprecated], PHP автоматически генерирует сообщение об устаревании каждый раз, когда этот элемент используется.

Атрибут #[Deprecated] поддерживает два необязательных параметра: $message и $since, позволяющие уточнить причину устаревания и версию, с которой элемент считается устаревшим.

Пример класса с устаревшей константой и методом:

class SomeClass {
    #[Deprecated("use NEW_CONSTANT instead", "8.4")]
    public const OLD_CONSTANT = 'constant_value';

    #[Deprecated("use newMethod() instead", "8.0")]
    public function oldMethod(): void {
        echo "This is the old method.";
    }
}

echo SomeClass::OLD_CONSTANT; // Deprecated: Constant SomeClass::OLD_CONSTANT is deprecated since 8.4, use NEW_CONSTANT instead in...

$instance = new SomeClass();
$instance->oldMethod(); // Deprecated: Method SomeClass::oldMethod() is deprecated since 8.0, use newMethod() instead in…

Новые функции работы с массивами

В PHP 8.4 добавлены новые функции для работы с массивами: array_find, array_find_key, array_any и array_all.

array_find

Функция array_find возвращает значение первого элемента, для которого callback-функция вернёт true:

$books = [
    ['title' => 'Book A', 'year' => 2005],
    ['title' => 'Book B', 'year' => 2012],
    ['title' => 'Book C', 'year' => 2008],
    ['title' => 'Book D', 'year' => 2015],
];

// Поиск первой книги, опубликованной после 2010 года
$firstNewBook = array_find($books, fn($book) => $book['year'] > 2010);

echo $firstNewBook ? $firstNewBook['title'] : 'No such books found'; // Book B


array_find_key

Функция array_find_key возвращает ключ первого элемента, для которого callback-функция вернёт true:

$users = [
    'Megan' => ['age' => 27, 'city' => 'New York'],
    'Andy' => ['age' => 38, 'city' => 'Los Angeles'],
    'Richard' => ['age' => 22, 'city' => 'Chicago'],
];

// Поиск ключа первого пользователя старше 30 лет
$firstAdultKey = array_find_key($users, fn($user) => $user['age'] > 30);

echo $firstAdultKey 
    ? "$firstAdultKey from {$users[$firstAdultKey]['city']}"
    : "No users over 30"; // Andy from Los Angeles

array_any

Функция array_any проверяет наличие хотя бы одного элемента в массиве, удовлетворяющего условию callback-функции:

$emails = ['user@example.com', '@example.com', 'user_example.com'];

// Проверка наличия хотя бы одного корректного email адреса
$result = array_any($emails, fn($email) => filter_var($email, FILTER_VALIDATE_EMAIL));

var_dump($result); // bool(true)

array_all

Функция array_all проверяет, все ли элементы в массиве удовлетворяют условию callback-функции:

$numbers1 = [1, 2, 3];
$numbers2 = [-1, 2, 3];

// Все ли числа массива положительные.
$result1 = array_all($numbers1, fn($number) => $number > 0);
$result2 = array_all($numbers2, fn($number) => $number > 0);

var_dump($result1); // bool(true)
var_dump($result2); // bool(false)

Упрощенный синтаксис new выражений

Для создания экземпляров классов с помощью ключевого слова new в PHP 8.4 введен упрощенный синтаксис. Теперь можно обращаться к свойствам и методам только что инициализированного объекта без необходимости оборачивать выражение new в круглые скобки:

// Раньше
$request = (new Request())->withMethod('GET')->withUri('/posts');

// Сейчас
$request = new Request()->withMethod('GET')->withUri('/posts');

Парсер HTML5

С выходом PHP 8.4 был представлен новый встроенный класс для работы с HTML — Dom\HTMLDocument, который обеспечивает полную поддержку HTML5. В отличие от DOMDocument, основанного на libxml2, новый парсер следует спецификациям WHATWG HTML, что делает его более точным и предсказуемым при разборе современных веб-страниц.

Основные особенности Dom\HTMLDocument:

  • Поддержка HTML5 — новый парсер корректно интерпретирует HTML5-документы, включая современные теги (main, article, section, dialog, template и др.).
  • Современный API — теперь можно использовать знакомые методы querySelector и querySelectorAll, аналогичные JavaScript.
  • Устранение ограничений DOMDocument — новый парсер избавлен от проблем, связанных с некорректным разбором HTML5 и зависимостью от libxml2, которая могла приводить к ошибкам.

Класс Dom\HTMLDocument предоставляет три метода для создания документа:

  • createEmpty — создает пустой HTML-документ, который можно заполнять вручную;
  • createFromFile — загружает HTML-документ из файла;
  • createFromString — создает HTML-документ из строки с HTML-кодом.

Пример парсинга HTML-строки:

$html = <<<HTML
<!DOCTYPE html>
<html>
<head>
    <title>Новости компании</title>
</head>
<body>
    <main>
        <h1>Последние новости</h1>
        <p>На следующей неделе состоится конференция, посвященная инновациям в нашей отрасли.</p>
    </main>
</body>
</html>
HTML;

// Создаем объект Dom\HTMLDocument
$dom = \Dom\HTMLDocument::createFromString($html);

// Используем querySelectorAll для выборки всех заголовков и абзацев
$elements = $dom->querySelectorAll('h1, p');

// Выводим содержимое каждого элемента
foreach ($elements as $element) {
    echo $element->textContent . PHP_EOL;
}

Новый метод создания объекта даты

До выхода новой версии PHP создавать объекты даты из временных меток UNIX можно было с помощью нескольких подходов, каждый из которых предполагал несколько действий. Можно было использовать конструктор либо метод setTimestamp объекта, а также статический метод createFromFormat('U') классов DateTime и DateTimeImmutable:

$timestamp = 1739163600;
$date = new DateTimeImmutable('@' . $timestamp);
echo $date->format('Y-m-d'); // 2025-02-10

$timestamp = 1739163600;
$date = new DateTimeImmutable();
$date = $date->setTimestamp($timestamp);
echo $date->format('Y-m-d'); // 2025-02-10

$timestamp = 1739163600;
$date = DateTimeImmutable::createFromFormat('U', (string) $timestamp);
echo $date->format('Y-m-d'); // 2025-02-10

С выходом PHP 8.4 появился новый, более удобный способ — использование специального метода createFromTimestamp. Этот метод позволяет напрямую создавать объекты даты из временных меток UNIX как целых чисел, так и значений с плавающей запятой:

$date = DateTimeImmutable::createFromTimestamp(1739163600);
echo $date->format('Y-m-d H:i'); // 2025-02-10 05:00

$date = DateTimeImmutable::createFromTimestamp(1739163600.628);
echo $date->format('Y-m-d H:i:s.v'); // 2025-02-10 05:00:00.628

Ленивые объекты (lazy objects)

В PHP 8.4 появилась долгожданная поддержка ленивых объектов. Ленивый объект — это объект, который не создается сразу после его объявления. Вместо этого его инициализация откладывается до тех пор, пока к нему не будет обращение или не потребуется изменить его состояние. Это позволяет экономить ресурсы системы и улучшает производительность.

Поддерживаются два типа ленивых объектов:

  • Ленивые призраки (Ghost Objects) — объекты, которые создаются на месте непосредственно перед использованием. После создания они становятся полноценными экземплярами своего класса и больше неотличимы от обычных.
  • Ленивые прокси (Virtual Proxies) — промежуточные слои между кодом клиента и реальным экземпляром класса. Они перенаправляют все операции на реальный экземпляр после его создания.

Для создания ленивых объектов используются методы класса ReflectionClass: newLazyGhost() для создания ленивых призраков и newLazyProxy() для создания ленивых прокси. Оба метода принимают callback-функцию, которая вызывается при необходимости фактической инициализации.

Пример ленивого призрака:

class SomeClass {
    public function __construct(public string $value) {
    }
}

$reflector = new ReflectionClass(SomeClass::class);

$lazyObject = $reflector->newLazyGhost(function (SomeClass $object) {
    // Инициализируем объект позже — по требованию
    $object->__construct('Some value');
});

// Инициализация ещё не произошла
var_dump($lazyObject); // lazy ghost object(SomeClass)#3 (0) { ["value"]=> uninitialized(string)

// Запускаем инициализацию
echo $lazyObject->value; // Some value

// Теперь объект полностью инициализирован
var_dump($lazyObject);  // object(SomeClass)#3 (1) { ["value"]=> string(10) "Some value" }

Пример ленивого прокси:

class SomeClass {
    public function __construct(public string $value) {
    }
}

$reflector = new ReflectionClass(SomeClass::class);

$lazyProxy = $reflector->newLazyProxy(function (SomeClass $proxy): SomeClass {
    return new SomeClass('Some value');
});

// Ленивая инициализация ещё не произошла
var_dump($lazyProxy); // lazy proxy object(SomeClass)#3 (0) { ["value"]=> uninitialized(string) }

// Запускаем инициализацию при обращении к методу или свойству
echo $lazyProxy->value; // Some value

// Теперь прокси работает с реальным объектом
var_dump($lazyProxy);  // lazy proxy object(SomeClass)#3 (1) { ["instance"]=> object(SomeClass)#4 (1) { ["value"]=> string(10) "Some value" } }

Другие улучшения

Помимо вышеупомянутых изменений, кратко отметим еще некоторые следующие:

  • Добавлены функции mb_trim, mb_ltrim и mb_rtrim для работы с многобайтовыми строками, аналогично функциям trim, ltrim и rtrim. Также добавлены mb_ucfirst и mb_lcfirst для изменения регистра первого символа в строке.
  • Появилась функция request_parse_body, упрощающая обработку HTTP-запросов с методами PUT, DELETE и PATCH. Она считывает тело запроса из потока php://input, разбирает его и возвращает массивы, эквивалентные $_POST и $_FILES.
  • Добавлены новые константы Curl (CURL_HTTP_VERSION_3 и CURL_HTTP_VERSION_3ONLY) для поддержки HTTP/3.
  • Появились новые функции для работы с заголовками HTTP-ответа: http_get_last_response_headers и http_clear_last_response_headers, которые позволяют получать и очищать HTTP-заголовки последнего ответа.
  • Ключевые слова exit и die теперь являются функциями, но могут вызываться без скобок для обратной совместимости.
  • Функция round() теперь вызывает исключение ValueError при использовании неправильных режимов округления.
  • Параметр cost алгоритма PASSWORD_BCRYPT/PASSWORD_DEFAULT изменён с 10 на 12 для повышения устойчивости паролей.
  • Для классов DateTime и DateTimeImmutable добавлены методы getMicrosecond() и setMicrosecond(), позволяющие получать и устанавливать значение микросекунд в объектах, соответственно.
  • Добавлена поддержка специфичных для драйвера подклассов PDO.

Оцените статью
DevReflex
Добавить комментарий