Нововведения в PHP 7.4
Полгода назад вышла стабильная версия PHP 7.3, но все уже предвкушают новую версию 7.4. Причем там будет тестироваться функционал для версии 8.0, которая кардинально изменит скорость PHP, так как будет полная поддержка JIT, что существенно ускорит выполнение многих вещей. Тем не менее версия 7.4 уже сделает язык быстрее и повысит безопасность.
Подробнее о JIT можно почитать в php wiki https://wiki.php.net/rfc/jit, а на Github можно посмотреть тесты скорости на примере теста Мандельброта. Небольшой спойлер: в PHP 8 скорость выполнения будет быстрее, чем код на С скомплированный компилятором gcc (4.9.2) с флагом оптимизации -О2
Что нового в версии PHP 7.4
- Даты выхода релизов PHP 7.4
- Поддержка оператора spread (...) внутри массивов
- Новый оператор присваивания и сравнения
??= - Стрелочные функции (короткие замыкания)
- Типизированные свойства классов
- Слабые ссылки
- Ковариантные возвращаемые значения и контравариантные параметры
- Предзагрузка
- Новый механизм сериализации пользовательских объектов
- Рефлексия для ссылок
- Изменение приоритета оператора конкатенации
- Левоассоциативный тернарный оператор объявлен устаревшим
- Добавление разрешения на вызов Exception в магическом методе __toString
- short_open_tag объявлен устаревшим
- Визуальный числовой разделитель
- Интерфейс для внешних функций на Си
- Список всех изменений
- Докер образы для тестирования
Даты выхода релизов PHP 7.4
Минорный выпуск PHP 7.4 ожидается 28 ноября 2019 года. Примерный график выхода:
Подробнее график выхода версий PHP 7.4 смотрите на официальном сайте.
Поддержка оператора spread (...) внутри массивов
Начиная с версии 7.4 использование array_merge() будет считаться уже не оптимальным решением, чтобы смерджить массивы. Дело не в том, что в данном случае будет сравнение вызова функции и языковой конструкции, а скорее в том что сама компиляция кода с использованием данного кода будет оптимальнее и заметно быстрее.
Изначально оператор "три точки" был доступен с версии 5.4 и использовался для аргументов функций. При помощи этого оператора можно было "распаковать" массивы аргументов и объекты Traversables (итерируемые объекты)
function test(...$args) { var_dump($args); }
test(1, 2, 3);
Теперь стандарт расширили до использования массивов, вот что написано в PHP 7.4 RFC о преимуществах перед array_merge():
1. Spread operator should have a better performance thanarray_merge. It's because not only that spread operator is a language structure whilearray_mergeis a function call, but also compile time optimization can be performant for constant arrays.
2.array_mergeonly supports array, while spread operator also supports objects implementingTraversable.
То есть работать будет быстрее, плюс можно использовать объекты.
Пример применения:
$arr = [...$args];
Использовать это можно будет так:
$parts = ['apple', 'pear']; $fruits = ['banana', 'orange', ...$parts, 'watermelon']; var_dump($fruits);
Если вы запустите этот код в версии 7.3 или более ранней, то получите ошибку парсинга
Parse error: syntax error, unexpected '...' (T_ELLIPSIS), expecting ']' in /home/omentes/php7.4-test/spread-operator.php on line 3
В версии 7.4 результат будет такой:
array(5) {
[0]=>
string(6) "banana"
[1]=>
string(6) "orange"
[2]=>
string(5) "apple"
[3]=>
string(4) "pear"
[4]=>
string(10) "watermelon"
}
Использовать оператор spread можно сколько угодно раз, и такой пример кода будет работать корректно:
$arr1 = [1, 2, 3]; $arr2 = [4, 5, 6]; $arr3 = [...$arr1, ...$arr2]; $arr4 = [...$arr1, ...$arr3, 7, 8, 9];
По мимо всего этого, можно использовать вызовы функций, которые возвращают массивы:
function buildArray(){
return ['red', 'green', 'blue'];
}
$arr1 = [...buildArray(), 'pink', 'violet', 'yellow'];
И даже генераторы:
function generator() {
for ($i = 3; $i <= 5; $i++) {
yield $i;
}
}
$arr1 = [0, 1, 2, ...generator()];
Тем не менее, нельзя использовать передачу массива по ссылке:
$arr1 = ['red', 'green', 'blue']; $arr2 = [...&$arr1]; Parse error: syntax error, unexpected '&' in /home/omentes/php7.4-test/ spread-operator.php on line 2
Хотя при этом массивы, которые содержат ссылки - скопируются вместе с ссылками:
$arr0 = 'red';
$arr1 = [&$arr0, 'green', 'blue'];
$arr2 = ['white', ...$arr1, 'black'];
array(5) {
[0]=>
string(5) "white"
[1]=>
&string(3) "red"
[2]=>
string(5) "green"
[3]=>
string(4) "blue"
[4]=>
string(5) "black"
}
Этот RFC был одобрен с 43 за и 1 против
Новый оператор присваивания и сравнения
Это улучшение оператора ??, который появился в версии 7. Данный оператор успешно заменил тернарный оператор в случаях, когда надо проверять существование переменной через isset()
$email = $_POST['email'] ?? ‘test@localhost';
То есть он позволил сделать строку реально короче, так как "под капотом" он работает как тернарный оператор с проверкой существования переменной. Но в реалиях современной разработки очень часто переменные хранятся в атрибутах класса в виде многмерных массивов, или во вложенных объектах, и так далее. Что даже такую запись делает сложной.
$this->response->data->article->comments->list['username'] = $this->response->data->article->comments->list['username'] ?? 'test record';
Это строка длиной 128 символов, где есть повторения одного и того же. Поэтому разработали оператор нулл-слияния (я затрудняюсь корректно перевести на русский). Подробнее можно посмотреть сам RFC. Данный оператор позволяет не дублировать левую часть в правой, и сразу писать значение, которое нужно подставить если переменная не равна null.
$this->response->data->article->comments->list['username'] ?? = 'test record'
Этот RFC был одобрен с 37 за и 4 против
Стрелочные функции (короткие замыкания)
Данный RFC является не первой попыткой добавить короткие замыкания в PHP. Авторы его таки и назвали - версия 2.0. Цель этого изменения сделать код более читаемым, понятным, простым.
function cube($n){
return ($n * $n * $n);
}
$a = [1, 2, 3, 4, 5];
$b = array_map('cube', $a);
print_r($b);
В новом стандарте этот же код можно будет написать так:
$a = [1, 2, 3, 4, 5]; $b = array_map(fn($n) => $n * $n * $n, $a); print_r($b);
На данный момент анонимные функции могут наследовать переменные, определенные в родительской области, если использовать ключевое слово use
$factor = 10;
$calc = function($num) use($factor){
return $num * $factor;
};
В PHP 7.4 же переменные области неявно фиксируют свои значения, и их нельзя изменить, но все переменнные можно использовать, как буд-то мы использовали ключевое слово use. Cледовательно в новой версии этот код можно записать так
$factor = 10; $calc = fn($num) => $num * $factor;
То есть запись в одну строку, которая стала более читаемой. Так же была добавлена строгая типизация:
fn(array $x) => $x; fn(): int => $x; fn($x = 42) => $x; fn(&$x) => $x; fn&($x) => $x; fn($x, ...$rest) => $rest;
Кроме этого в данном RFC определено следующее поведение переменной $this внутри класса. Ключевое слово static может запретить его использование внутри анонимной функции:
class Test {
public function method() {
$fn = fn() => var_dump($this);
$fn(); // object(Test)#1 { ... }
$fn = static fn() => var_dump($this);
$fn(); // Error: Using $this when not in object context
}
}
Этот RFC был одобрен с 51 голосами против 8. (Прошлая попытка два года назад проиграла со счетом 22 к 30)
Типизированные свойства классов
Тайпхинтинги, которые ввели в версии 7.2 кардинально изменили разработку, и язык продолжает разиваться в данном направлении. Данный RFC позволит типизировать свойства классов любым типом, за исключением void и callable. Достурпные типы для типизации: bool, int, float, string, array, object, iterable, self, parent, nullable, а так-же типы классов.
class Test {
// Legal default values
public bool $a = true;
public int $b = 42;
public float $c = 42.42;
public float $d = 42; // Special exemption
public string $e = "str";
public array $f = [1, 2, 3];
public iterable $g = [1, 2, 3];
public ?int $h = null;
public ?object $i = null;
public ?Test $j = null;
// ILLEGAL default values
public bool $k = 1;
public int $l = null;
public Test $m = null;
// Prefix notation
public int $x, $y, $z;
// Suffix notation
public $x: int, $y: int, $z: float;
// Self
public self $prop;
}
Один и тот же тип применяется ко всем свойствам в одном объявлении:
public float $x, $y;
Поведение, если мы допустим ошибку в типе свойств:
<?php
class User {
public int $id;
public string $name;
}
$user = new User;
$user->id = 10;
$user->name = [];
Fatal error: Uncaught TypeError: Typed property User::$name must be string, array used in /home/omentes/php7.4-test/types.php:9
Данный RFC победил на голосовании со счетом 70 к 1.
Слабые ссылки
В этом RFC PHP 7.4 вводит класс WeakReference, который позволяет программистам сохранять ссылку на объект, который не предотвращает уничтожение самого объекта.
В настоящее время PHP поддерживает Weak References, используя расширение вроде pecl-weakref. В любом случае, новый API отличается от документированного класса WeakRef.
Вот пример от автора этого предложения, Никиты Попова, во время презентации нововведений на PHPRussia 2019:
$object = new stdClass; $weakRef = WeakReference::create($object); var_dump($weakRef->get()); unset($object); var_dump($weakRef->get());
Вторый вызов дампа напечатает NULL, так как объект уже удален.
Данный RFC победил на голосовании со счетом 28 к 5.
Ковариантные возвращаемые значения и контравариантные параметры
Дисперсия - это свойство иерархий классов, описывающее, как типы конструктора типов влияют на подтипы. В общем случае конструктор типа может быть:
- Инвариант: если тип супертипа ограничивает тип подтипа.
- Ковариант: если порядок типов сохраняется (типы упорядочены от более специфических к более общим).
- Контрвариант: если он меняет порядок (типы упорядочены от более общих к более конкретным).
В настоящее время PHP имеет в основном инвариантный параметр и возвращаемый тип, за редким исключением. В этом RFC предлагается разрешить ковариацию и контравариантность для типов параметров и типов возвращаемых данных.
Вот пример ковариантного типа возвращаемого значения:
interface Factory {
function make(): object;
}
class UserFactory implements Factory {
function make(): User;
}
А вот пример контравариантного типа параметра:
interface Concatable {
function concat(Iterator $input);
}
class Collection implements Concatable {
// accepts all iterables, not just Iterator
function concat(iterable $input) {/* . . . */}
}
Для более детального изучения ковариации и контравариантности в PHP 7.4 прочитайте данный RFC. Так же стоит обратить внимание, что внедрение такого функционала позволит полноценно пользоваться подстановкой Барбары Лисков (та самая буква L в слове SOLID)
Этот RFC принят 39 голосами против 1.
Предзагрузка
Это предложение Дмитрия Стогова - одно из моих любимых, поскольку оно должно значительно повысить производительность. Предварительная загрузка - это процесс загрузки библиотек и каркасов в OPCache при инициализации модуля (подробнее почитать о жизненном цикле PHP можно тут и тут).
Вот как работает предварительная загрузка в словах Дмитрия:
При запуске сервера - перед запуском любого кода приложения - мы можем загрузить определенный набор файлов PHP в память - и сделать их содержимое «постоянно доступным» для всех последующих запросов, которые будут обслуживаться этим сервером. Все функции и классы, определенные в этих файлах, будут доступны для запросов из коробки, точно так же, как и внутренние объекты.
Эти файлы загружаются при запуске сервера, выполняются перед любым приложением и остаются доступными для любых будущих запросов. Это здорово с точки зрения производительности.
Предварительная загрузка контролируется специальной директивой php.ini: opcache.preload. Эта директива указывает PHP-скрипт, который будет скомпилирован и выполнен при запуске сервера. Этот файл можно использовать для предварительной загрузки дополнительных файлов, включая их, или с помощью функции opcache_compile_file() (подробнее в документации PHP).
Но есть и обратная сторона. На самом деле RFC прямо заявляет:
preloaded files remain cached in opcache memory forever. Modification of their corresponding source files won’t have any effect without another server restart.
То есть предварительно загруженные файлы остаются в кеше в памяти opcache навсегда. Модификация их соответствующих исходных файлов не будет иметь никакого эффекта без перезапуска сервера.
Однако все функции, определенные в предварительно загруженных файлах, будут постоянно загружены в таблицы функций и классов PHP и останутся доступными для каждого будущего запроса. Это приведет к хорошим улучшениям производительности, даже если эти улучшения могут быть значительно изменчивыми.
Вы можете прочитать больше об ограничениях и исключениях предварительной загрузки на официальной странице предварительной загрузки.
Этот RFC принят 48 голосами против 0.
Новый механизм сериализации пользовательских объектов
В настоящее время у нас есть два различных механизма для кастомной сериализации объектов в PHP:
- Магические методы __sleep() и __wakeup()
- Интерфейс Serializable
По словам автора, Никиты Попова, оба этих варианта имеют проблемы, которые приводят к сложному и ненадежному коду. В этом RFC очень много рассуждений и примеров, подробнее можно ознакомится на оффициальной странице этого RFC. Здесь я только упомяну, что новый механизм сериализации должен предотвращать эти проблемы, предоставляя два новых магических метода, __serialize() и __unserialize(), которые объединяют два существующих механизма.
Этот RFC принят 20 голосами против 7.
Рефлексия для ссылок
Данный RFC предлагает добавить глобальный класс, часть Reflection API - ReflectionReference. По сути он будет работать как spl_object_hash(), но отвечать за работу с ссылками а не объектами.
final class ReflectionReference {
/* Returns ReflectionReference if array element is a reference, null otherwise. */
public static function fromArrayElement(array $array, int|string $key): ?ReflectionReference;
/* Returns unique identifier for the reference. The return value format is unspecified. */
public function getId(): int|string;
private function __construct(); // Always throws
private function __clone(); // Always throws
}
Доступный изначально статический метод fromArrayElement(array $array, int|string $key) вернет экземпляр ReflectionReference если $array[$key]является ссылкой, иначе null.
Если $array не является массивом или $key не строка или число, вызовется TypeError. В сценарии, когда $array[$key] не существует будет вызвано исключение ReflectionException.
Этот RFC принят 30 голосами против 1.
Изменение приоритета оператора конкатенации
В настоящее время в PHP арифметические операторы «+» и «-» и строковый оператор «.» ассоциативны и имеют одинаковый приоритет (подробнее о приоритетах операторов).
В качестве примера рассмотрим следующую строку:
echo "sum: " . $a + $b;
В PHP 7.3 этот код выдает следующее предупреждение:
Warning: A non-numeric value encountered in /home/omentes/php7.4-test/types.php on line 4
Это происходит потому, что операторы оценивается слева направо. Это то же самое, что написать следующий код:
echo ("sum: " . $a) + $b;
Этот RFC предлагает изменить приоритет операторов, давая «.» Более низкий приоритет, чем операторы «+» и «-», так что сложения и вычитания всегда будут выполняться до объединения строк. Эта строка кода должна быть эквивалентна следующей:
echo "sum: " . ($a + $b);
Это двухэтапное изминение:
- Начиная с версии 7.4, PHP должен выдавать уведомление об устаревании при обнаружении выражения без скобок с «+», «-» и «.».
- Фактическое изменение приоритета этих операторов должно быть добавлено в PHP 8.
Этот RFC принят 30 голосами против 7 для версии 7.4 и 31 против 4 для версии 8.
Левоассоциативный тернарный оператор объявлен устаревшим
В PHP тернарный оператор, в отличие от многих других языков, является левоассоциативным. По словам Никиты Попова, это может сбивать с толку программистов, которые переключаются между разными языками.
В настоящее время в PHP следующий код является корректным:
$b = $a == 1 ? 'one' : $a == 2 ? 'two' : $a == 3 ? 'three' : 'other';
Это интерпретируется как:
$b = (($a == 1 ? 'one' : $a == 2) ? 'two' : $a == 3) ? 'three' : 'other';
И это может привести к ошибкам, потому что это может быть не тем, что мы намерены делать. Таким образом, этот RFC предлагает отказаться от использования левой ассоциативности для тернарных операторов без использования использование скобок.
Это еще одно двухэтапное изменение:
- Начиная с PHP 7.4, вложенные тернарные операторы без явного использования скобок будут выдавать предупреждение об устаревании.
- Начиная с PHP 8.0, во время выполнения будет ошибка компиляции.
Этот RFC принят 35 голосами против 10.
Добавление разрешения на вызов Exception в магическом методе toString
Создание исключений из __toString()в настоящее время запрещено и приведет к фатальной ошибке. Это делает опасным вызов произвольного кода внутри __toString()и делает его использование в качестве общего API проблематичным. Этот RFC направлен на устранение этого ограничения.
Обоснование текущего поведения заключается в том, что преобразования строк выполняются во многих местах движка и стандартной библиотеки, и не все места подготовлены к тому, чтобы обрабатывать исключения «правильно», в том смысле, чтоб исключение обрабатывать как можно раньше.
Это ограничение в конечном счете бесполезно с технической точки зрения, потому что исключения во время преобразования строк могут по-прежнему вызываться обработчиком ошибок, который преобразует восстанавливаемые ошибки в исключения.
Этот RFC принят 42 голосами против 0.
short_open_tag объявлен устаревшим
За прошедшие годы PHP предоставил различные способы обозначения начала кода PHP, кроме стандартных открытых тегов. Большинство из этих открывающих тегов были удалены с помощью PHP 7.0.0 (RFC), однако короткие открытые теги PHP все еще остаются и часто конфликтуют с другими, например <?xml ?>
Это еще одно двухэтапное изменение:
- В версии 7.4 короткая запист
<? будетобъявлена устаревшей - В версии 8.0 короткие теги будут удалены
Это RFC не затрагивает тег <?=
Этот RFC принят 38 голосами против 18 для версии 7.4 и 42 против 15 для версии 8.
Визуальный числовой разделитель
Для лучшего представления чисел появится разделитель, который никак не будет влиять на сами числа. Например:
$threshold = 1_000_000_000; // a billion! $testValue = 107_925_284.88; // scale is hundreds of millions $discount = 135_00; // $135, stored as cents
Разделитель будет поддерживать все числовые типы:
6.674_083e-11; // float 299_792_458; // decimal 0xCAFE_F00D; // hexadecimal 0b0101_1111; // binary 0137_041; // octal
Данный RFC никак не влияет на сами числа, только на сепарацию чисел разделителем:
var_dump(1_000_000); // int(1000000)
Этот RFC принят 33 голосами против 11.
Интерфейс для внешних функций на Си
Foreign Function Interface - это API, которое позволит использовать язык Си в разработке на php. Изначально это апи разрабатывали в связке предзагрузкой, которая была описана выше. Для связывания использовали LuaJIT, и теперь не нужно учить "промежуточный" язык [Lua], чтобы, например, добавить в свой проект TensorFlow. Биндинг именно этой технологии на php был реализован с использованием FFI Дмитрием Стоговым, чтобы доказать полезность и работоспособность предлагаемого улучшения
У нового класса FFI на данный момент доступны следующие методы:
FFI::cdef([string $cdef = "" [, string $lib = null]]): FFIFFI::new(mixed $type [, bool $own = true [, bool $persistent = false]]): FFI\CDataFFI::free(FFI\CData $cdata): voidFFI::cast(mixed $type, FFI\CData $cdata): FFI\CDataFFI::addr(FFI\CData $cdata): FFI\CDataFFI::type(string $type): FFI\CTypeFFI::arrayType(FFI\CType $type, array $dims): FFI\CTypeFFI::typeof(FFI\CData $data): FFI\CTypeFFI::sizeof(mixed $cdata_or_ctype): intFFI::alignof(mixed $cdata_or_ctype): intFFI::memcpy(FFI\CData $dst, mixed $src, int $size): voidFFI::memcmp(mixed $src1, mixed $src2, int $size): intFFI::memset(FFI\CData $dst, int $c, int $size): voidFFI::string(FFI\CData $src [, int $size]): stringFFI::load(string $file_name): FFIFFI::scope(string $scope_name): FFI
Код будет выглядеть примерно так:
<?php
// create FFI object, loading libc and exporting function printf()
$ffi = FFI::cdef(
"int printf(const char *format, ...);", // this is regular C declaration
"libc.so.6");
// call C printf()
$ffi->printf("Hello %s!\n", "world");
Доступ к структурам данных FFI значительно (примерно в 2 раза) медленнее, чем к собственным массивам и объектам PHP. Нет смысла использовать их для скорости, но может иметь смысл уменьшить потребление памяти. Это верно для всех подобных реализаций FFI в режиме интерпретации.
В следующей таблице показано время выполнения ary3 бенчмарка со следующим кодом на php (в секундах, чем ниже, тем лучше).
<?php
function ary3($n, bool $use_ffi = false) {
if ($use_ffi) {
$X = FFI::new("int[$n]");
$Y = FFI::new("int[$n]");
}
for ($i=0; $i<$n; $i++) {
$X[$i] = $i + 1;
$Y[$i] = 0;
}
for ($k=0; $k<1000; $k++) {
for ($i=$n-1; $i>=0; $i--) {
$Y[$i] += $X[$i];
}
}
$last = $n-1;
print "$Y[0] $Y[$last]\n";
}
Этот RFC принят 24 голосами против 15.
Список всех изменений
Полный список всех изменения в PHP 7.4 можно посмотреть на github
Докер образы для тестирования
На докер хабе уже уже появляются кастомные образы с новой версией PHP. Рекомендую использовать https://github.com/devilbox/docker-php-fpm-7.4
Так же уже можно протестировать и 8 версию: https://github.com/devilbox/docker-php-fpm-8.0
Статья написана для @response418