Что нового в PHP 7.2? +77


Несмотря на то, что мы активно работаем с Python и Go, всё же существенная часть нашего серверного кода написана на PHP. Поэтому мы внимательно следим за всеми нововведениями языка. Прошло меньше года после релиза предыдущей минорной версии, и вот уже последний бета-релиз запланирован на 17 августа. Его ещё не рекомендуется использовать в production, но уже можно скачать docker-образ. Пора разбираться, что изменилось в новой версии языка.



Содержание



Оптимизация


В Opcache добавлена глобальная оптимизация на основе анализа потока данных с использованием SSA (Static single assignment form): Sparse Conditional Constant Propagation (SCCP), удаление мертвого кода (Dead Code Elimination — DCE) и удаление неиспользуемых локальных переменных.


Оптимизирована работа встроенной функции in_array() с помощью поиска хеша в перевернутом массиве.


Новая функциональность


Добавлена возможность загружать расширения по имени (RFC)


Раньше extension= и zend_extension= в файле php.ini содержали пути до файла расширения.
Но, к сожалению, имя файла зависело от платформы. Например, в unix-подобных системах оно строилось как <extension-name>.<suffix>, где suffix это .so на всех системах кроме HP-UX, где он sl. В Windows имя файла формируется как php_<extension-name>.dll. Всё это порождало много ошибок.


Теперь вы можете писать:


extension=bz2
zend_extension=xdebug

И нужные расширения будут подгружены в зависимости от ОС.


Этот механизм будет работать при установке extension и zend_extension в ini-файле, а также как аргумент для функции dl().


Но абсолютные пути по-прежнему необходимо будет указывать при флаге -z в CLI-режиме, а также при указании абсолютного пути. Следующий пример работать не будет:


extension=/path/to/extensions/bz2


Добавлена возможность перегружать абстрактные функции (RFC)


Теперь вы сможете перегрузить абстрактные функции точно так же, как и обычные функции:


abstract class A           { abstract function bar(stdClass $x);  }
abstract class B extends A { abstract function bar($x): stdClass; }
class C extends B          { function bar($x): stdClass{} }

До PHP 7.2 выдавалась ошибка вида:


Fatal error: Can't inherit abstract function A::bar() (previously declared abstract in B)


Запрещено number_format() возвращать -0 (RFC)


Вызов number_format(-0.00)возвращал string(1) “0”, однако number_format(-0.01) возвращал string(2) “-0”. Сейчас же будет возвращен 0 без знака.


Добавлена возможность конвертировать нумерованные ключи при приведении типов object/array (RFC)


Предыстория:


В PHP есть два типа данных, которые содержат ключ/значение. Первый — это массивы, которые могут содержать ключи в виде строк или чисел. При этом если строка удовлетворяет правилу /^(0|(-?[1-9][0-9]*))$/ и она достаточно маленькая PHP_INT_MIN ? n ? PHP_INT_MAX, то она конвертируется в числовой ключ.


Второй тип — это объекты, в которых недопустимы числовые ключи, и ключи конвертируются в строки.


При этом в Zend Engine они представлены в виде одной структуры HashTable.


Теперь это исправлено.


Давайте посмотрим на пару примеров:


$obj = new stdClass;

$obj->{'0'} = 1;
$obj->{'1'} = 2;
$obj->{'2'} = 3;
$arr = (array) $obj;

var_dump($arr); // Видим, что массив содержит ключи/значения
var_dump($arr[1]); // Не можем обратиться по ключу. В PHP 7.2 это исправлено.

$arr = [0 => 1, 1 => 2, 2 => 3];
$obj = (object)$arr;

var_dump($obj); // Видим, что объект содержит ключи/значения
var_dump($obj->{'0'}); // Не можем обратиться по ключу. В PHP 7.2 это исправлено.

Запрещено передавать null в качестве параметра для get_class() (RFC)


Когда null передается как параметр get_class() внутри контекста класса, поведение функции может быть весьма неожиданным:


class Foo
{
    function bar($repository)
    {
        $result = $repository->find(100);
        return get_class($result);
    }
}

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


Если $result содержит null, выход будет иметь контекст класса, из которого вызывается get_class(), в этом случае Foo.


Эта особенность нарушает принцип наименьшего удивления: «если необходимая функция имеет высокий коэффициент удивления, может потребоваться перепроектирование этой функции».
Теперь будет выдаваться предупреждение:
Warning: get_class() expects parameter 1 to be object, null given in %s on line %d
Если вы хотите сохранить старое поведение, придётся переписать код:


// Было:
$x = get_class($some_value_that_may_be_null);

// Стало:
if ($some_value_that_may_be_null === null) {
    $x = get_class();
} else {
    $x = get_class($some_value_that_may_be_null);
}

Вызов Count с параметром, который нельзя посчитать (RFC)


Теперь вызов count() с параметром, который является скалярным, null или объектом, который не реализовал интерфейс Countable, будет выдавать Warning.


Возможность расширения типа параметра (RFC)


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


class ArrayClass {
  public function foo(array $foo) { /* ... */ }
}

 class EverythingClass extends ArrayClass {
  public function foo($foo) { /* ... */ }
}

До PHP 7.2 возвращалась ошибка:


Warning: Declaration of EverythingClass::foo($foo) should be compatible with ArrayClass::foo(array $foo) in %s on line 18

В пулл реквесте мнения разделились.
Одни говорили:


"Конечно, давайте отправим SOLID в ад. Кто такая Барбара Лисков??? Какая-то безумная женщина? Конечно! Давайте разрешим ломать принципы и идеи".

Другие считают, что:


"Ограничения вроде принципа единой ответственности (single responsibility) должны быть реализованы не на уровне языка, а в коде приложения. И теперь и буква L в SOLID будет на усмотрение разработчика".

Добавлена возможность указывать запятую в конце группированных неймспейсов (RFC)


// Раньше это работало только для массивов
$array = [1, 2, 3,];

// Теперь и для группировки неймспейсов
use Foo\Bar\{ Foo, Bar, Baz, };

Планировалось добавить такую возможность и для других списков, но на стадии голосования они были отменены и будут по-прежнему возвращать Parse error:


// Аргументы функций и методов
fooCall($arg1, $arg2, $arg3,);

// Перечисление реализуемых интерфейсов
class Foo implements
    FooInterface,
    BarInterface,
    BazInterface,
{
    // Перечисление трейтов
    use
        FooTrait,
        BarTrait,
        BazTrait,
    ;

    // Перечисление свойств и констант
    const
        A = 1010,
        B = 1021,
        C = 1032,
        D = 1043,
    ;
    protected
        $a = 'foo',
        $b = 'bar',
        $c = 'baz',
    ;
    private
        $blah,
    ;

    // Декларация функций и методов
    function something(FooBarBazInterface $in, FooBarBazInterface $out,) : bool
    {
    }
}

// Наследование переменных из родительской области в анонимных функциях
$foo = function ($bar) use (
    $a,
    $b,
    $c,
) {
    // . . . 
};

Реализовано семейство функций socket_getaddrinfo (RFC)


Теперь из PHP будет доступна информация из getaddrinfo(), реализованная на C. Это недостающая функция для текущей библиотеки сокетов. При работе с различными сетями было бы полезно разрешить libc рассказать нам, какие методы подключения/прослушивания будут наиболее подходящими с учетом набора подсказок.


Были утверждены четыре функции:


socket_addrinfo_lookup(string node[, mixed service, array hints]) : array
socket_addrinfo_connect(resource $addrinfo) : resource
socket_addrinfo_bind(resource $addrinfo) : resource
socket_addrinfo_explain(resource $addrinfo) : array

Улучшены TLS-константы (RFC)


Теперь:


  • tls:// имеет дефолтное значение TLSv1.0 + TLSv1.1 + TLSv1.2
  • ssl:// это алиас к tls://
  • константа STREAM_CRYPTO_METHOD_TLS_* имеет дефолтное значение TLSv1.0 + TLSv1.1 + TLSv1.2 вместо TLSv1.0
  • константа STREAM_CRYPTO_METHOD_SSLv23_CLIENT считается устаревшей и позже будет удалена.

Object typehint (RFC)


Добавлен новый тип для хинта: object


function acceptsObject(object $obj) {
    // ...
}

acceptsObject(json_decode('{}')); 
acceptsObject(new \MyObject());
acceptsObject("Будет ошибка");

function correctFunction() : object {
    $obj = json_decode('{}');
    return $obj;
}

// Будет ошибка
function errorFunction() : object {
    return [];
}

LDAP EXOP (RFC)


Добавлены функции для использования расширенных операций LDAP в php-ldap.


// Вызов EXOP whoami и сохранение результатов в $identity
if (ldap_exop($link, LDAP_EXOP_WHO_AM_I, NULL, $identity)) {
  echo "Connected as $identity\n";
} else {
  echo "Operation failed\n";
}
// Делаем то же самое, используя объект результата:
$r = ldap_exop($link, LDAP_EXOP_WHO_AM_I);
if (($r !== FALSE) && ldap_parse_exop($link, $r, $retdata)) {
  echo "Connected as $retdata\n";
} else {
  echo "Operation failed\n";
}
// То же самое с хелпером:
if (ldap_exop_whoami($link, $identity)) {
  echo "Connected as $identity\n";
} else {
  echo "Operation failed\n";
}
// Изменение пароля с хелпером:
if (ldap_exop_passwd($link, 'uid=johndoe,dc=example,dc=com', '', 'newpassword')) {
  echo "Password changed\n";
} else {
  echo "Operation failed\n";
}

В ядро PHP добавлена Libsodium (RFC)


Libsodium — современная криптографическая библиотека, которая предлагает аутентифицированное шифрование, высокоскоростную криптографию с эллиптическими кривыми и многое другое. В отличие от других криптографических стандартов (которые являются набором криптографических примитивов, например, WebCrypto), libsodium включает в себя тщательно подобранные алгоритмы, реализованные экспертами по безопасности. Это поможет избежать уязвимостей в сторонних каналах.


Добавлен алгоритм Argon2 в хешировании пароля (RFC)


Argon2 — это современный простой алгоритм, направленный на высокую скорость заполнения памяти и эффективное использование нескольких вычислительных блоков.


HashContext as Object (RFC)


Начиная с PHP5 предпочтительной структурой для хранения внутренних данных были объекты. По какой-то причине в расширении Hash для этого использовались ресурсы. Данный RFC пытается исправить недоразумение, переведя расширение Hash на хранение внутренних данных в виде объектов.


Примеры использования ресурса в качестве внутреннего представления:


resource hash_copy ( resource $context )
resource hash_init ( string $algo [, int $options = 0 [, string $key = NULL ]] )

Внутреннее представление преобразуется из ресурса в объект. Существующий код должен продолжить работать, если он не использует явных проверок is_resource(), эти проверки могут быть легко заменены на is_resource | is_object.


Добавлен отладчик PDO Prepared statements (RFC)


$db = new PDO(...);

$stmt = $db->query('SELECT 1');
var_dump($stmt->activeQueryString()); // => string(8) "SELECT 1"

$stmt = $db->prepare('SELECT :string');
$stmt->bindValue(':string', 'foo');

// возвращает необработанную строку до выполнения
var_dump($stmt->activeQueryString()); // => string(14) "SELECT :string"

// возвращает обработанную строку после выполнения
$stmt->execute();
var_dump($stmt->activeQueryString()); // => string(11) "SELECT 'foo'"

Добавлен отладчик PDO Prepared statements v2 (RFC)


$calories = 150;
$colour = 'red';

$sth = $dbh->prepare('SELECT name, colour, calories
    FROM fruit
    WHERE calories < ? AND colour = ?');
$sth->bindParam(1, $calories, PDO::PARAM_INT);
$sth->bindValue(2, $colour, PDO::PARAM_STR);
$sth->execute();

$sth->debugDumpParams();

/*
Вывод:

SQL: [82] SELECT name, colour, calories
    FROM fruit
    WHERE calories < ? AND colour = ?
Sent SQL: [88] SELECT name, colour, calories
    FROM fruit
    WHERE calories < 150 AND colour = 'red'
Params:  2
Key: Position #0:
paramno=0
name=[0] ""
is_param=1
param_type=1
Key: Position #1:
paramno=1
name=[0] ""
is_param=1
param_type=2
*/

Расширенные типы строк для PDO (RFC)


$db->quote('uber', PDO::PARAM_STR | PDO::PARAM_STR_NATL); // N'uber'
$db->quote('A'); // 'A'

$db->setAttribute(PDO::ATTR_DEFAULT_STR_PARAM, PDO::PARAM_STR_NATL);
$db->quote('uber'); // N'uber'
$db->quote('A', PDO::PARAM_STR | PDO::PARAM_STR_CHAR); // 'A'

Добавлены опции JSON_INVALID_UTF8_IGNORE и JSON_INVALID_UTF8_SUBSTITUTE (Request)


Для функций json_encode / json_decode добавлены новые опции JSON_INVALID_UTF8_IGNORE и JSON_INVALID_UTF8_SUBSTITUTE для игнорирования или замены некорректных последовательностей байтов UTF-8.


Удалено из PHP 7.2


Следующий функционал объявлен устаревшим и был удален.


Удалены строки без кавычек (bare word) (RFC)


Строки без кавычек теперь вызывают E_WARNING. В PHP2 такие строки вызывали Syntax error, но в PHP3 бета-поведение было изменено.
К примеру:


$foo = flase; // Опечатка, но раньше вызывалась ошибка E_NOTICE, которую часто отключали.  
// ...
if ( $foo ) {
   var_dump($foo); // string(5) "flase"
}

Перенос mcrypt в PECL


Расширение mcrypt, объявленное устаревшим в PHP 7.1, было перемещено в PECL.


Объявлено устаревшим в PHP 7.2 (RFC)


Следующий функционал объявлен устаревшим и больше не рекомендуется к использованию.
Этот функционал будет удален в версии 8.0.


Устарел __autoload


Функция __autoload была заменена на spl_autoload_register ещё в версии 5.1.
Основным преимуществом spl_autoload_register является возможность использовать несколько автозагрузчиков. Теперь будет выбрасываться Deprecation notice на стадии компиляции.


Устарели png2wbmp() и jpeg2wbmp()


Png2wbmp() и jpeg2wbmp() — единственные функции, изменяющие формат изображений, которые можно вызвать напрямую, доступные в ext / gd, что делает их довольно обособленными, поскольку libgd не предлагает таких функций. Кроме того, WBMP был изобретен для поддержки WAP, который в настоящее время устарел. Теперь будет выбрасываться Deprecation notice.


Устарела $php_errormsg


Переменная $php_errormsg создается в локальной области при возникновении нефатальной ошибки, если параметр track_errors включен (отключен по умолчанию), и ошибка не перехватывалась никаким обработчиком ошибок.


Помимо того, что поведение зависело от настроек ini-файла, оно также было магическим. Функция error_get_last обеспечивает более чистый способ получения последней ошибки. С PHP 7 доступна функция error_clear_last, таким образом, охватываются все возможные варианты использования $php_errormsg без манипуляции с областями видимости.


Устарела create_function()


create_function() — это тонкая оболочка вокруг конструкции языка eval(), позволяющая создавать функцию со сгенерированным именем, списком аргументов и телом в виде строковых аргументов. До введения замыканий в PHP 5.3 она обеспечивала способ создания чего-то похожего на лямбда-функции.


Из-за характера работы create_function(), помимо потенциального источника проблем безопасности, имеет очень плохие характеристики производительности и использования памяти. Использование реальных замыканий во всех отношениях предпочтительнее.


Устарел mbstring.func_overload


Параметр mbstring.func_overload в ini-файле позволяет заменить определенное подмножество строковых функций на аналоги с расширением mbstring. Например, strlen() больше не будет возвращать длину строки в байтах, вместо этого она вернет длину в символах в соответствии с текущей выбранной внутренней кодировкой.


Это означает, что код с использованием mbstring.func_overload не совместим кодом, написанным в предположении, что основные операции со строками работают нормально. Некоторые библиотеки прямо запрещают func_overload (например, Symfony), другие библиотеки перестают работать. Код, который хочет поддерживать func_overload, должен условно переключаться между обычными строковыми функциями и функциями mbstring с 8-битным кодированием (обычно только библиотеки для криптографии пытаются это сделать).
Теперь будет выбрасываться Deprecation notice если mbstring.func_overload содержит ненулевое значение.


Устарел (unset) cast


Каст (unset) превращает значение в null. Это означает, что (unset) expr — просто выражение, которое всегда возвращает null и не имеет других побочных эффектов.
Помимо бесполезности, это поведение только путает, так как многие люди разумно предполагают, что (unset) $a будет вести себя аналогично unset($a), а на самом деле этого не происходит.
Теперь будет выбрасываться Deprecation notice на стадии компиляции.


Устарела функция parse_str() без второго аргумента


Функция parse_str() разбирает строку URL и присваивает значения переменным в текущем контексте (или заносит в массив, если задан параметр result).


Использовать эту функцию без параметра result крайне не рекомендовалось, потому что динамическое создание переменных в области видимости функции ведет ровно к тем же проблемам, что и register_globals. Теперь выбрасывается Deprecation notice, если параметр result не передается.


Устарела функция gmp_random()


Функция gmp_random() генерирует случайное число. Число будет в диапазоне нуля до произведения числа limiter на количество бит в лимбе (limb). Если число limiter отрицательное, будет возвращен отрицательный результат.


Лимб — внутренний GMP-механизм. Технически это часть числа, помещающаяся в одно машинное слово. Количество бит в нем может различаться в разных системах. В основном это либо 16, либо 32, но это не гарантируется. Так происходит, потому что реализация GMP/MPIR не доступна пользователю. Таким образом, использование этой функции требует угадывания размера Лимба и может зависеть от платформы.


Чтобы исправить это, в PHP 5.6 добавили функции gmp_random_bits() и gmp_random_range(), которые позволяют точно контролировать используемый диапазон случайных чисел. Эти функции всегда должны быть предпочтительнее, чем gmp_random().
Теперь при вызове gmp_random() выбрасывается Deprecation notice.


Устарела функция each()


Функция each() может использоваться для итерации по массиву, подобно foreach. В каждом вызове он возвращает массив с текущим ключом и значением и продвигает указатель внутреннего массива на следующую позицию. Типичное использование, представленное в руководстве, выглядит следующим образом:


reset($array);
while (list($key, $val) = each($array)) {
    echo "$key => $val\n";
}

Функция each уступает foreach практически во всём, среди прочего она в 10 раз медленнее.
Поддержка этой функции создает проблему для некоторых изменений языка. Например, в предупреждении для невалидного контейнера массива (RFC) пришлось исключить list(), потому что типичное использование each полагается на факт, что вы можете получить доступ к смещениям массива на false без предупреждения.


Теперь выбрасывается Deprecation warning при первом вызове each, потому что чаще всего он используется в цикле.


Устарела функция assert() со строковым аргументом


Функция assert() имеет два режима работы: если передано что-то, кроме строки, она будет проверять, что значение является истиной. Если была передана строка, она будет запущена через eval(), и assert будет проверять истинность результата eval().


Причиной такого поведения является то, что до PHP 7 это было единственным способом предотвратить вычисление выражения. Начиная с PHP 7 опция zend.assertions в ini-файле может использоваться, чтобы избежать вычисления выражений. Таким образом, больше нет необходимости поддерживать неявное вычисление строковых аргументов.


Использование assert($value) для проверки истинности значения открывает уязвимость удаленного выполнения кода, если существует вероятность того, что $value будет строкой.
Теперь выбрасывается Deprecation notice, если assert() используется со строковым аргументом.


Устарел $errcontext аргумент в error handler


Обработчикам ошибок, заданных с помощью set_error_handler(), передается в качестве последнего аргумента $errcontext. Этот аргумент представляет собой массив, содержащий все локальные переменные в месте, где была сгенерирована ошибка.


Эта функция трудна для оптимизации, поскольку $errcontext может использоваться для изменения всех ссылок и объектов в текущей области видимости. Эта функциональность практически не используется. Если вы хотите проверить переменные состояния в месте ошибки, вы должны использовать debugger.


Обратите внимание, что контекст ошибки содержит только локальные переменные. Ошибки backtrace, включая $this и аргументы функции, останутся доступны через debug_backtrace().


В этом случае невозможно выбрасывать Deprecation warning, поэтому этот функционал будет просто отмечен в документации как устаревший.


Устарела константа INTL_IDNA_VARIANT_2003


В PHP 7.2 константа INTL_IDNA_VARIANT_2003 объявлена устаревшей.
В PHP 7.4 будут изменены idn_to_ascii() и idn_to_utf8(), в которых дефолтный $variant параметр станет INTL_IDNA_VARIANT_UTS46.
В PHP 8.0 уберут поддержку INTL_IDNA_VARIANT_2003.


Объявлены устаревшими приведение типов (binary) и b"" литералы (RFC)


Приведение типа (binary) и поддержка префикса b были добавлены в PHP 5.2.1 для совместимости с PHP 6, однако эта версия так и не появилась, и неизвестно, будут ли когда-нибудь ещё попытки реализовать двоичные строки. Тем не менее, они все еще принимаются языковым сканером, хотя и игнорируются.
Теперь выбрасывается Deprecation notice при использовании этих кастов.


Специально к версии 7.2 я подготовил репозиторий — "Что нового в PHP", который описывает изменения в версиях PHP, начиная с 5.


Список на английском языке вы можете найти в источниках:
https://github.com/php/php-src/blob/PHP-7.2/UPGRADING
https://wiki.php.net/rfc#php_next_72


Попробовать новую версию онлайн:
https://3v4l.org/


Как мы видим, глобальных изменений достаточно мало.
А как вы относитесь к изменениям в PHP? Какая фича понравилась больше всего?




К сожалению, не доступен сервер mySQL