Что нового в PHP 8 (Функции, Улучшения и JIT Компилятор)

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

Многие RFC уже одобрены и внедрены, так что пришло время погрузиться в некоторые из наиболее захватывающих дополнений, которые должны сделать PHP более быстрым и надежным.

Так как PHP 8 все еще находится в разработке, мы можем увидеть несколько изменений до финального релиза.

Итак, какие возможности и улучшения мы должны ожидать от PHP 8? Что самое большое мы ожидаем от PHP 8, следующего основного релиза этого языка?

PHP JIT (Just in Time Compiler)

Самой известной функцией PHP 8 является компилятор Just-in-time (JIT). Что такое JIT?

RFC описывает JIT следующим образом:

«PHP JIT реализован как почти независимая часть OPcache. Он может быть включен/выключен во время компиляции PHP и во время выполнения. При включении, нативный код файлов PHP хранится в дополнительной области разделяемой памяти OPcache и op_array→opcodes[].handler(ы) хранят указатели на точки входа JIT-ed кода».

Итак, как мы попали в JIT и в чем разница между JIT и OPcache?

Чтобы лучше понять, что такое JIT для PHP, давайте посмотрим, как PHP исполняется от исходного кода до конечного результата.

Выполнение PHP представляет собой 4-ступенчатый процесс:

  • Lexing/Tokenizing: Сначала интерпретатор читает PHP-код и собирает набор лексем.
  • Parsing: Интерпретатор проверяет, соответствует ли скрипт правилам синтаксиса, и использует маркеры для построения абстрактного синтаксического дерева (AST), которое является иерархическим представлением структуры исходного кода.
  • Compilation: Интерпретатор обходит дерево и транслирует узлы AST в низкоуровневые опкоды Zend, которые являются числовыми идентификаторами, определяющими тип инструкции, выполняемой Zend VW.
  • Interpretation: Опкоды интерпретируются и запускаются на Zend VM.

Следующее изображение показывает визуальное представление основного процесса выполнения PHP.

Так как же OPcache делает PHP быстрее? И какие изменения в процессе выполнения с JIT?

Расширение OPcache

PHP является интерпретируемым языком. Это означает, что при выполнении PHP-скрипта интерпретатор разбирает, компилирует и выполняет код снова и снова при каждом запросе. Это может привести к пустой трате ресурсов процессора и дополнительного времени.

Именно здесь в игру вступает расширение OPcache:

«OPcache улучшает производительность PHP, сохраняя прекомпилированный байткод скрипта в общей памяти, тем самым устраняя необходимость загрузки и разбора скриптов PHP при каждом запросе».

При включенной функции OPcache интерпретатор PHP проходит 4-ступенчатый процесс, упомянутый выше, только при первом запуске скрипта. Так как байт-коды PHP хранятся в общей памяти, они сразу же становятся доступными в качестве низкоуровневого промежуточного представления и могут быть выполнены на VM Zend.

Процесс выполнения PHP с включенным OPcache

Начиная с PHP 5.5, расширение Zend OPcache доступно по умолчанию, и вы можете проверить, правильно ли оно настроено, просто вызовом phpinfo() из скрипта на вашем сервере или просмотром вашего php.ini файла (смотрите настройки конфигурации OPcache).

Zend OPcache раздел на странице phpinfo

Preloading (предварительная загрузка)

Недавно был улучшен OPcache с реализацией предварительной загрузки, новая функция OPcache добавлена в PHP 7.4. Предварительная загрузка позволяет хранить заданный набор скриптов в памяти OPcache «до запуска любого кода приложения», но не приносит ощутимого улучшения производительности для типичных веб-приложений.

С JIT PHP делает шаг вперед.

JIT — компилятор Just in Time

Даже если опкоды находятся в виде низкоуровневого промежуточного представления, они все равно должны быть скомпилированы в машинный код. JIT «не вводит никакой дополнительной формы IR (промежуточного представления)», но использует DynASM (динамический ассемблер для движков генерации кода) для генерации нативного кода непосредственно из байт-кода PHP.

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

Зеев Сураски, соавтор предложения PHP JIT, показывает, как много вычислений было бы быстрее с JIT:

JIT для Live Web-приложений

Согласно JIT RFC, реализация компилятора «как раз вовремя» должна улучшить производительность PHP. Но действительно ли мы испытаем такие улучшения в реальных приложениях, таких как WordPress?

Ранние тесты показывают, что JIT заставит CPU-емкие рабочие нагрузки работать значительно быстрее, однако RFC предупреждает:

«… как и в предыдущих попытках — в настоящее время, похоже, не наблюдается существенного улучшения реальных приложений, таких как WordPress (с opcache.jit=1235 326 req/sec против 315 req/sec).
Планируется приложить дополнительные усилия, улучшив JIT для реальных приложений, используя профилирование и спекулятивные оптимизации».

При включенной JIT код будет выполняться не VM Zend, а самим процессором, и это увеличит скорость вычислений. Веб-приложения, такие как WordPress, также полагаются на другие факторы, такие как TTFB, оптимизация базы данных, HTTP-запросы и т.д.

Поэтому, когда речь заходит о WordPress и подобных приложениях, не стоит ожидать значительного увеличения скорости выполнения PHP. Тем не менее, JIT может принести ряд преимуществ для разработчиков.

По словам Никиты Попова:

«Преимущества компилятора JIT примерно (и это уже указано в КСФ):

  • Значительно лучшая производительность для численного кода.
  • Немного лучше производительность для «типичного» кода PHP веб-приложений.
  • Потенциал для переноса большего количества кода с C на PHP, так как PHP теперь будет достаточно быстрым».

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

Обратной стороной, однако, будет большая сложность, которая может привести к увеличению расходов на обслуживание, стабильность и отладку. По словам Дмитрия Стогова:

«JIT чрезвычайно прост, но в любом случае он увеличивает уровень всей сложности PHP, риск ошибок нового типа и стоимость разработки и сопровождения».

Предложение о включении JIT в PHP 8 было принято 50 голосами против 2.

PHP 8 Улучшения и новые функциональные возможности

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

  • Validation for Abstract Trait Methods
  • Incompatible Method Signatures
  • Arrays Starting With a Negative Index
  • Union Types 2.0
  • Consistent Type Errors for Internal Functions
  • throw Expression
  • Weak Maps
  • Trailing Comma in Parameter List
  • Allow ::class syntax on objects
  • Attributes v2

Validation for Abstract Trait Methods

Трейты определяются как «механизм повторного использования кода на единичных языках наследования, таких как PHP». Обычно они используются для объявления методов, которые могут быть использованы в нескольких классах.

Трейт может также содержать абстрактные методы. Эти методы просто объявляют описание метода, но реализация метода должна выполняться внутри класса с использованием трейта.

Согласно руководству по PHP,

«Трейты поддерживают использование абстрактных методов для того, чтобы предъявлять требования к классу экспонентов».

Это также означает, что подписи методов должны совпадать. Другими словами, тип и количество требуемых аргументов должны быть одинаковыми.

В любом случае, по словам автора RFC Никиты Попова, в настоящее время проверка подписи осуществляется только выборочно:

  • Она не принудительна в наиболее распространенном случае, когда реализация метода обеспечивается классом use: https://3v4l.org/SeVK3
  • Она принудительно исполняется, если реализация идет от родительского класса: https://3v4l.org/4VCIp
  • Она принудительно исполняется, если исполнение происходит в детском классе: https://3v4l.org/q7Bq2

Следующий пример от Никиты относится к первому случаю (не принудительная подпись):

trait T {
	abstract public function test(int $x);
}
 
class C {
	use T;

	// Allowed, but shouldn't be due to invalid type.
	public function test(string $x) {}
}

При этом данная RFC предлагает всегда возвращать ошибку, если метод реализации не совместим с методом абстрактного признака, независимо от его происхождения:

Fatal error: Declaration of C::test(string $x) must be compatible with T::test(int $x) in /path/to/your/test.php on line 10

Данная RFC была единогласно одобрена.

Incompatible Method Signatures (Несовместимый метод Подписи)

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

Если класс реализует интерфейс, то несовместимые сигнатуры методов приводят к фатальной ошибке. Согласно Object Interfaces documentation:

«Класс, реализующий интерфейс, должен использовать сигнатуру метода, совместимого с LSP (Liskov Substitution Principle). Невыполнение этого требования приведет к фатальной ошибке».

Вот пример ошибка наследования интерфейса:

interface I {
	public function method(array $a);
}
class C implements I {
	public function method(int $a) {}
}

В PHP 7.4 вышеприведенный код приведет к следующей ошибке:

Fatal error: Declaration of C::method(int $a) must be compatible with I::method(array $a) in /path/to/your/test.php on line 7

Функция в дочернем классе с несовместимой сигнатурой будет выводить предупреждение. См. следующий код из RFC:

class C1 {
	public function method(array $a) {}
}
class C2 extends C1 {
	public function method(int $a) {}
}

В PHP 7.4 вышеприведенный код будет просто выдавать предупреждение:

Warning: Declaration of C2::method(int $a) should be compatible with C1::method(array $a) in /path/to/your/test.php on line 7

Теперь эта RFC предлагает всегда выдавать фатальную ошибку для сигнатур несовместимых методов. В PHP 8 код, который мы видели ранее, будет выглядеть следующим образом:

Fatal error: Declaration of C2::method(int $a) must be compatible with C1::method(array $a) in /path/to/your/test.php on line 7

Arrays Starting With a Negative Index (Массивы, начинающиеся с отрицательного индекса)

В PHP, если массив начинается с отрицательного индекса (start_index < 0), то следующие индексы будут начинаться с 0 (подробнее об этом в документации по массиву array_fill). Посмотрите на следующий пример:

$a = array_fill(-5, 4, true);
var_dump($a);

В PHP 7.4 результат будет следующим:

array(4) {
	[-5]=>
	bool(true)
	[0]=>
	bool(true)
	[1]=>
	bool(true)
	[2]=>
	bool(true)
}

В PHP 8 массивы, начинающиеся с отрицательного индекса, меняют свое поведение. Узнайте больше об обратных несовместимостях в RFC.

Union Types 2.0 (Союзные Типы 2.0)

Типы союзов принимают значения, которые могут быть разных типов. В настоящее время PHP не поддерживает союзные типы, за исключением синтаксиса ?Type и специального итерабельного (iterable) типа.

До PHP 8 союзные типы могли быть указаны только в аннотациях phpdoc, как показано в следующем примере из RFC:

class Number {
	/**
	 * @var int|float $number
	 */
	private $number;

	/**
	 * @param int|float $number
	 */
	public function setNumber($number) {
		$this->number = $number;
	}

	/**
	 * @return int|float
	 */
	public function getNumber() {
		return $this->number;
	}
}

Теперь, Union types 2.0 RFC предлагает добавить поддержку союзных типов в функциональных сигнатурах, так что мы больше не будем полагаться на встроенную документацию, а будем определять союзные типы с помощью синтаксиса T1|T2|… вместо него:

class Number {
	private int|float $number;

	public function setNumber(int|float $number): void {
		$this->number = $number;
	}

	public function getNumber(): int|float {
		return $this->number;
	}
}

Как объяснил Никита Попов в RFC,

«Поддержка союзов типов в языке позволяет нам перемещать больше информации о типах из phpdoc в функциональные сигнатуры, с обычными преимуществами, которые это дает:

— Типы на самом деле принуждаются, так что ошибки могут быть пойманы рано.
— Из-за того, что они принудительно применяются, информация о типах с меньшей вероятностью устареет или пропустит крайние случаи.
— Типы проверяются во время наследования, применяя принцип замещения Лискова.
— Типы доступны через Reflection.
Синтаксис намного меньше, чем у phpdoc».

Союзные типы поддерживают все доступные типы, с некоторыми ограничениями:

  • Тип void не может быть частью объединения, поскольку void означает, что функция не возвращает никакого значения.
  • null поддерживается только в союзных типах, но его использование в качестве самостоятельного типа не допускается.
  • Нотация нулевого типа (?T) также разрешена, имея в виду T|null, но мы не можем включить нотацию ?T в союзные типы (?T1|T2 не разрешена и мы должны использовать T1|T2|null вместо этого).
  • Так как многие функции (например, strpos(), strstr(), substr() и т.д.) возвращают false среди возможных возвращаемых типов, поддерживается и псевдотип false.

Вы можете прочитать больше о Union Types V2 в RFC.

Consistent Type Errors for Internal Functions (Последовательные ошибки типа для внутренних функций)

При передаче параметра недопустимого типа внутренние и пользовательские функции ведут себя по-разному.

Пользовательские функции вызывают ошибку TypeError, но внутренние функции ведут себя по-разному, в зависимости от нескольких условий. В любом случае, типичным поведением является вывод предупреждения и возврат null. Смотрите следующий пример в PHP 7.4:

var_dump(strlen(new stdClass));

Это приведет к следующему предупреждению:

Warning: strlen() expects parameter 1 to be string, object given in /path/to/your/test.php on line 4
NULL

Если включен strict_types, или информация об аргументах указывает на типы, поведение будет иным. В таких сценариях ошибка типа обнаруживается и приводит к ошибке TypeError.

Такая ситуация привела бы к ряду проблем, хорошо объясненных в разделе «RFC issues«.

Чтобы устранить эти несоответствия, RFC предлагает сделать внутренний парсинг параметров API, чтобы всегда генерировать ThrowError в случае несовпадения типа параметра.

В PHP 8 вышеприведенный код выдает следующую ошибку:

Fatal error: Uncaught TypeError: strlen(): Argument #1 ($str) must be of type string, object given in /path/to/your/test.php:4
Stack trace:
#0 {main}
  thrown in /path/to/your/test.php on line 4

throw Expression

В PHP throw — это оператор, поэтому его нельзя использовать в местах, где разрешено только выражение.

Этот RFC предлагает преобразовать оператор throw в выражение, чтобы его можно было использовать в любом контексте, где допустимы выражения. Например, стрелочных ф-ций, оператор null coalesce, тернарные и эльвисовые операторы и т.д.

См. следующие примеры

$callable = fn() => throw new Exception();

// $value is non-nullable.
$value = $nullableValue ?? throw new InvalidArgumentException();
 
// $value is truthy.
$value = $falsableValue ?: throw new InvalidArgumentException();

Weak Maps

Weak Maps — это набор данных (объектов), в которых ключи имеют слабые ссылки, что означает, что им не мешают собирать мусор.

В PHP 7.4 добавлена поддержка слабых ссылок, как способ сохранить ссылку на объект, который не препятствует уничтожению самого объекта.

Вот почему этот RFC вводит класс WeakMap для создания объектов, которые будут использоваться в качестве слабых ключей карты, которые могут быть уничтожены и удалены со слабой карты, если нет больше никаких ссылок на ключевой объект.

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

$map = new WeakMap;
$obj = new stdClass;
$map[$obj] = 42;
var_dump($map);

В PHP 8 вышеприведенный код даст следующий результат

object(WeakMap)#1 (1) {
	[0]=>
	array(2) {
		["key"]=>
		object(stdClass)#2 (0) {
		}
		["value"]=>
		int(42)
	}
}

При удалении объекта, ключ используемые в WeakMap автоматически удалится:

unset($obj);
var_dump($map);

Теперь результат будет следующим:

object(WeakMap)#1 (0) {
}

Для более подробного ознакомления с WeakMpa, смотрите RFC.

Trailing Comma в параметрах функции

Продолжающиеся запятые — это запятые, которые добавляются к спискам пунктов в различных контекстах. В PHP 7.2 введены запятые для трейлинга в синтаксисе списка, в PHP 7.3 введены запятые для трейлинга при вызове функций.

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

class Foo {
	public function __construct(
		string $x,
		int $y,
		float $z, // trailing comma
	) {
		// do something
	}
}

Поддержка синтаксиса ::class в объектах

Для получения имени класса можно воспользоваться синтаксисом Foo\Bar::class. Этот RFC предлагает распространить тот же синтаксис на объекты, чтобы теперь можно было получить имя класса данного объекта, как показано в примере ниже:

$object = new stdClass;
var_dump($object::class); // "stdClass"
 
$object = null;
var_dump($object::class); // TypeError

В PHP 8 $object::class дает тот же результат, что и get_class($object). Если $object не является объектом, он выбрасывает исключение TypeError.

Атрибуты v2

Атрибуты, также известные как аннотации, представляют собой форму структурированных метаданных, которые могут использоваться для указания свойств объектов, элементов или файлов.

До PHP 7.4 doc-comments были единственным способом добавить метаданные в объявления классов, функций и т.д. Теперь в RFC Attributes v2 введены атрибуты для PHP, определяющие их как форму структурированных синтаксических метаданных, которые можно добавлять к объявлениям классов, свойств, функций, методов, параметров и констант.

Атрибуты добавляются перед объявлениями, на которые они ссылаются. См. следующие примеры из RFC:

<<ExampleAttribute>>
class Foo
{
	<<ExampleAttribute>>
	public const FOO = 'foo';

	<<ExampleAttribute>>
	public $x;

	<<ExampleAttribute>>
	public function foo(<<ExampleAttribute>> $bar) { }
}

$object = new <<ExampleAttribute>> class () { };

<<ExampleAttribute>>
function f1() { }

$f2 = <<ExampleAttribute>> function () { };

$f3 = <<ExampleAttribute>> fn () => 1;

Атрибуты могут быть добавлены до или после комментария doc-block:

<<ExampleAttribute>>
/** docblock */
<<AnotherExampleAttribute>>
function foo() {}

Каждое объявление может иметь один или несколько атрибутов, и каждый атрибут может иметь одно или несколько связанных значений:

<<WithoutArgument>>
<<SingleArgument(0)>>
<<FewArguments('Hello', 'World')>>
function foo() {}

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

PHP 8 привносит в язык несколько новых функций:

str_contains

До PHP 8 для разработчиков типичными вариантами поиска нужной строки внутри заданной строки были strstr и strpos. Проблема в том, что обе функции не считаются очень интуитивно понятными и их использование может сбить с толку новых PHP-разработчиков. См. следующий пример:

$mystring = 'Managed WordPress Hosting';
$findme = 'WordPress';
$pos = strpos($mystring, $findme);

if ($pos !== false) {
	echo "The string has been found";
} else {
	echo "String not found";
}

В приведенном примере использовался оператор сравнения !==, который также проверяет, не являются ли два значения одними и теми же. Это не позволяет получить ошибку, если вхождение нужного значения в строку равно 0:

«Эта функция может возвращать Boolean FALSE, но может возвращать и небулевое значение, которое вычисляется для FALSE». […] Используйте оператор === для проверки возвращаемого значения этой функции».

Кроме того, несколько фреймворков предоставляют вспомогательные функции для поиска значения внутри заданной строки (см. документацию Laravel Helpers в качестве примера).

Теперь в RFC предлагается ввести новую функцию, позволяющую осуществлять поиск внутри строки: str_contains.

str_contains ( string $haystack , string $needle ) : bool

Его использование довольно просто. str_contains проверяет, найден ли $needle в $haystack и возвращает true или false соответственно.

Итак, благодаря str_contains, мы можем написать следующий код.:

$mystring = 'Managed WordPress Hosting';
$findme   = 'WordPress';

if (str_contains($mystring, $findme)) {
	echo "The string has been found";
} else {
	echo "String not found";
}

Который более читабельный и менее подвержен ошибкам (смотрите этот код в действии здесь).
На момент написания статьи str_contains чувствителен к регистру, но в будущем ситуация может измениться.

str_starts_with() and str_ends_with()

В дополнение к функции str_contains, две новые функции позволяют искать значение внутри заданной строки: str_starts_with и str_ends_with.

Эти новые функции проверяют, начинается или заканчивается ли заданная строка другой строкой:

str_starts_with (string $haystack , string $needle) : bool
str_ends_with (string $haystack , string $needle) : bool

Обе функции возвращают false, если $needle длиннее $haystack.

Благодаря им, мы могли бы теперь избежать использования неоптимальных и менее интуитивных функций, таких как подложка, strpos. Обе функции чувствительны к регистру:

$str = "WordPress";
if (str_starts_with($str, "Word")) echo "Found!";

if (str_starts_with($str, "word")) echo "Not found!";

get_debug_type

get_debug_type — это новая PHP-функция, которая возвращает тип переменной. Новая функция работает аналогично функции gettype, но тип get_debug_type возвращает родные имена типов и разрешает имена классов.

Это хорошее улучшение для языка, так как gettype() не полезна для проверки типов.

RFC предоставляет два полезных примера, чтобы лучше понять разницу между новой функцией get_debug_type() и gettype(). Первый пример показывает, что gettype работает:

$bar = [1,2,3];

if (!($bar instanceof Foo)) { 
	throw new TypeError('Expected ' . Foo::class . ', got ' . (is_object($bar) ? get_class($bar) : gettype($bar)));
}

В PHP 8 мы могли бы использовать вместо этого get_debug_type:

if (!($bar instanceof Foo)) { 
	throw new TypeError('Expected ' . Foo::class . ' got ' . get_debug_type($bar));
}

В следующей таблице приведены возвращаемые значения get_debug_type и gettype:

Valuegettype()get_debug_type()
1integerint
0.1doublefloat
truebooleanbool
falsebooleanbool
nullNULLnull
“WordPress”stringstring
[1,2,3]arrayarray
A class with name “Foo\Bar”objectFoo\Bar
An anonymous classobject[email protected]

Оригинал статьи