PHP 8.0: Новые функции, классы и JIT (4/4)

4 года назад от David Grudl  

Вышла версия PHP 8.0. Она настолько насыщена новинками, как ни одна версия до этого. Их представление потребовало целых четыре отдельных статьи. В этой последней мы рассмотрим новые функции и классы и представим Just in Time Compiler.

Новые функции

Стандартная библиотека PHP насчитывает сотни функций, и в версии 8.0 появилось шесть новых. Кажется, что это мало, но большинство из них закрывают слабые места языка. Что хорошо соответствует общему настроению версии 8.0, которая дорабатывает и консолидирует PHP, как ни одна версия до этого. Обзор всех новых функций и методов вы найдете в руководстве по миграции.

str_contains() str_starts_with() str_ends_with()

Функции для определения, начинается ли строка, заканчивается ли или содержит подстроку.

if (str_contains('Nette', 'te')) {
	...
}

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

str_contains('Nette', '')     // true
str_starts_with('Nette', '')  // true
strpos('Nette', '')           // 0 (раньше false)

Благодаря этому поведение тройки полностью идентично аналогам в Nette:

str_contains()      # Nette\Utils\String::contains()
str_starts_with()   # Nette\Utils\String::startsWith()
str_ends_with()     # Nette\Utils\String::endsWith()

Почему эти функции так важны? Стандартные библиотеки всех языков всегда обременены историческим развитием, и нельзя избежать возникновения несоответствий и ошибок. Но в то же время это визитная карточка каждого языка. Удивительно, когда у 25-летнего PHP отсутствуют функции для таких базовых операций, как возврат первого или последнего элемента массива, экранирование HTML без подвохов (htmlspecialchars не экранирует апостроф), или именно поиск строки в строке. То, что это можно как-то обойти, не выдерживает критики, потому что результатом тогда не является читаемый и понятный код. Это урок для всех авторов API. Когда вы видите, что значительную часть документации функции занимает объяснение подводных камней (как, например, возвращаемые значения strpos), это явный сигнал к изменению библиотеки и добавлению именно str_contains.

get_debug_type()

Заменяет уже устаревшую get_type(). Вместо длинных типов, таких как integer, возвращает ныне используемые int, в случае объектов возвращает сразу тип:

Значение gettype() get_debug_type()
'abc' string string
[1, 2] array array
231 integer int
3.14 double float
true boolean bool
null NULL null
new stdClass object stdClass
new Foo\Bar object Foo\Bar
function() {} object Closure
new class {} object class@anonymous
new class extends Foo {} object Foo@anonymous
curl_init() resource resource (curl)
curl_close($ch) resource (closed) resource (closed)

Объективизация ресурсов

Значения типа resource происходят из времен, когда в PHP еще не было объектов, но они фактически были нужны. Так появились ресурсы. Сегодня у нас есть объекты, и по сравнению с ресурсами они гораздо лучше работают со сборщиком мусора, поэтому в планах постепенно заменить все ресурсы объектами.

С PHP 8.0 ресурсы изображений, соединений curl, openssl, xml и т.д. заменяются на объекты. В PHP 8.1 наступит очередь FTP-соединений и т.д.

$res = imagecreatefromjpeg('image.jpg');
$res instanceof GdImage  // true
is_resource($res)        // false - BC break

У этих объектов пока нет методов, и вы не можете напрямую создавать их экземпляры. Пока что речь идет действительно только о том, чтобы избавиться от устаревших ресурсов в PHP без изменения API. И это хорошо, потому что создание хорошего API — это отдельная и сложная задача. Никто не хочет, чтобы в PHP появились еще классы вроде SplFileObject с методами, названными fgetc() или fgets().

PhpToken

В объекты перемещается и токенизатор, а следовательно, и функции вокруг token_get_all. На этот раз речь идет не об избавлении от ресурсов, а о получении полноценного объекта, представляющего один PHP-токен.

<?php

$tokens = PhpToken::tokenize('<?php $a = 10;');
$token = $tokens[0];         // экземпляр PhpToken

echo $token->id;             // T_OPEN_TAG
echo $token->text;           // '<?php'
echo $token->line;           // 1
echo $token->getTokenName(); // 'T_OPEN_TAG'
echo $token->is(T_STRING);   // false
echo $token->isIgnorable();  // true

Метод isIgnorable() возвращает true для токенов T_WHITESPACE, T_COMMENT, T_DOC_COMMENT и T_OPEN_TAG.

Weak maps

Weak maps связаны со сборщиком мусора, который освобождает из памяти все объекты и значения, которые больше не используются (т.е. нет используемой переменной или свойства, которые бы их содержали). Поскольку жизнь PHP-потока коротка, а памяти на серверах сегодня у нас достаточно, вопросы эффективного освобождения памяти, как правило, мы вообще не решаем. Но для длительно работающих скриптов они являются ключевыми.

Объект WeakMap похож на SplObjectStorage. В обоих в качестве ключей используются объекты, и они позволяют хранить под ними любые значения. Разница в том, что WeakMap не препятствует тому, чтобы объект был освобожден сборщиком мусора. Т.е. если единственным местом, где объект еще встречается, является ключ в weak map, он будет удален из карты и памяти.

$map = new WeakMap;
$obj = new stdClass;
$map[$obj]  = 'данные для $obj';

dump(count($map));  // 1
unset($obj);
dump(count($map));  // 0

Зачем это нужно? Например, для кеширования. Пусть у нас есть метод loadComments(), которому мы передаем статью блога, и он возвращает все ее комментарии. Поскольку метод вызывается с одной и той же статьей многократно, создадим еще getComments(), который будет кешировать результат первого метода:

class Comments
{
	private WeakMap $cache;

	public function __construct()
	{
		$this->cache = new WeakMap;
	}

	public function getComments(Article $article): ?array
	{
		$this->cache[$article] ??= $this->loadComments($article);
		return $this->cache[$article]
	}

	...
}

Штука в том, что в момент, когда объект $article освобождается (например, приложение начинает работать с другой статьей), освобождается и его запись из кеша.

PHP JIT (Just in Time Compiler)

Возможно, вы знаете, что PHP компилируется в так называемый opcode, это низкоуровневые инструкции, которые можно посмотреть, например, здесь и которые выполняет виртуальная машина PHP. А что такое JIT? JIT может прозрачно компилировать PHP прямо в машинный код, который выполняет непосредственно процессор, тем самым обходясь без более медленного выполнения виртуальной машиной.

JIT, таким образом, должен ускорить PHP.

Попытки внедрить JIT в PHP восходят к 2011 году, и за ними стоит Дмитрий Стогов. С тех пор он испробовал 3 разные реализации, но ни одна из них не попала в релиз PHP по следующим причинам: результатом никогда не был существенный прирост производительности для типичных веб-приложений; усложняет поддержку PHP (т.е. никто, кроме Дмитрия, этого не понимает 😉); существовали другие пути улучшения производительности, не требующие использования JIT.

Скачкообразное повышение производительности PHP в версии 7 было побочным продуктом работы над JIT, хотя его внедрения, парадоксально, не произошло. Это происходит только в PHP 8. Но сразу буду сдерживать завышенные ожидания: вероятно, никакого ускорения вы не заметите.

Почему же JIT входит в PHP? Во-первых, другие пути улучшения производительности постепенно исчерпываются, и JIT просто на очереди. В обычных веб-приложениях он хоть и не приносит ускорения, но существенно ускоряет, например, математические вычисления. Таким образом, открывается возможность начать писать такие вещи на PHP. И фактически, так можно было бы прямо на PHP реализовать функции, которые до сих пор требовали реализации непосредственно на C из-за скорости.

JIT является частью расширения opcache и включается вместе с ним в php.ini (прочтите документацию к этим четырем цифрам):

zend_extension=php_opcache.dll
opcache.jit=1205              ; конфигурация с помощью четверки OTRC
opcache.enable_cli=1          ; чтобы работало и в CLI
opcache.jit_buffer_size=128M  ; выделенная память для скомпилированного кода

О том, что JIT работает, вы узнаете, например, в информационной панели в Tracy Baru.

JIT очень хорошо работает тогда, когда все переменные имеют четко заданные типы и не могут изменяться при повторном вызове одного и того же кода. Поэтому мне интересно, будем ли мы когда-нибудь в PHP декларировать типы и для переменных: string $s = 'Привет, это заключение серии';

David Grudl Founder of Uměligence and creator of Nette Framework, the popular PHP framework. Since 2021, he's been fully immersed in artificial intelligence, teaching practical AI applications. He discusses weekly tech developments on Tech Guys with his co-hosts and writes for phpFashion and La Trine. He believes AI isn't science fiction—it's a practical tool for improving life today.