php
🐞 Немного об отладке через var_dump
Недавно поймал, посещая «Вконтакт», такую ситуацию. Владельцы её заметили нескоро — висело ещё минимум час.
«Вконтакте» написан на диалекте ПХП и для тех не знаком с этим языком поясню — это
strtr vs. str_replace
Кроме того, часто выход
Среди подобных текстов есть и вредные. Речь об одном из них и пойдёт. Впервые на сравнение производительности функций str_replace и strtr (с одним параметром) я наткнулся очень много лет назад, тогда с ним разобрался и тему для себя закрыл.
Но такие исследования возникают постоянно, а авторы совершенно не утруждают себя ответом на вопрос «почему», так что я решил написать этот небольшой текст.
Итак, есть две функции, которые ведут себя очень похожим образом — заменяют в строке
Обратите внимание на этот код:
$r = ['xo' => 'xaxе', 'xa' => 'xo'];
$s = 'xoxo';
var_dump(str_replace(array_keys($r), array_values($r), $s)); // xoxеxoxе
var_dump(strtr($s, $r)); // xaxеxaxеОчевидно результаты разные, но почему? Потому что str_replace делает замену «в лоб» — проходя строку несколько раз, по числу заданных замен. Поэтому получается «xoxo» → «xaxexaxe» → «xoxexoxe».
Функция strtr куда более хитрая, но работает более… естественным что ли образом — т. е. так как обычно человек ждёт от функции множественной замены. Самое главное — в сделанной замене она уже ничего не заменяет.
Есть и другое отличие:
$r = ['user1' => 'one', 'user11' => 'eleven'];
$s = 'user11';
var_dump(str_replace(array_keys($r), array_values($r), $s)); // one1
var_dump(strtr($s, $r)); // elevenМой пример выглядит несколько искусственно, но это часть случая из жизни, правда очень упрощённого. Тут беда в том, что str_replace применяет свои замены слева направо — в порядке перечисления, тогда как strtr сначала сортирует их по длине.
Как видите, эти функции совершенно разные по своему действию и сравнивать по производительности их просто некорректно.
Void в PHP
ПХП7
Я сначала не понял зачем вводить в язык новое ключевое слово, можно было бы использовать для такого указания уже существующее ключевое слово null, но оказалось, что авторы под void имели ввиду, что функция не может вернуть никакого значения:
function returns_null(): void {
return null; // Fatal error: A void function must not return a value
}включая null, потому что это определённо значение, хоть и со специальным смыслом. При этом вот такое работает:
function returns_nothing(): void {
return; // valid
}Таким образом в языке произошло неявное изменение: раньше ситуации с пустым return и с return null не различались, сейчас они будут иметь разный смысл.
Отрицательное количество элементов (PHP)
Сегодня утром с разработчиками обсуждали фрагмент кода, где автор очень уж параноидально подошёл к проверке:
if (count($this->to) <= 0) {
$this->validation_errors[] = "Неверный отправитель";
}Проверять возвращает ли функция count значение меньшее нуля действительно странно, но я вдруг подумал — а можно ли в ПХП в припципе заставить эту функцию вернуть такое значение?
Дело в том, что функцию count можно использовать не только с примитивными типами (чаще всего её используют с массивами), но и с объектами, которые реализуют интерфейс Countable.
Оказалось вполне нормально работает:
$var = new class implements \Countable {
public function count()
{
return -1;
}
}
var_dump(count($var)); // выведет int(-1)Выше код написан в синтаксисе ПХП7, который выйдет сегодня, но его можно переписать и на «пятёрку» — надо только дать имя классу.
Сложение массивов в PHP
Некоторые ребята не знают, что в ПХП есть операция «плюс» над массивами, а открыв её для себя случайным чтением инструкции по языку, не знают чем она отличается от array_merge. Между тем отличия разительные, если их не знать, легко наплодить ошибок в коде.
Операция сложения («плюс») с массивами работает проще всего: если в левом массиве уже есть значение с таким ключом, оно и остаётся:
["L"] + ["R"]; // будет ["L"]
["a" => "L"] + ["a" => "R", "b" => "R"]; // будет ["a" => "L", "b" => "R"]Операция слияния массивов (array_merge) работает
array_merge(["L"], ["R", 4 => "R"]); // ["L", "R", "R"], значения целых ключей правого массива потерялись, сам массив добавился в конец
array_merge(["a" => "L"], ["a" => "R"]); // ["a" => "R"], совпадение строковых ключей
array_merge(["L", "a" => "L"], ["R", "a" => "R", 4 => "R"]); // ["L", "a" => "R", "R", "R"], разные типы ключей вместеОперация «плюс» очень полезна для добавления группы ключей строковых со значениями (для объединения ассоциативных массивов):
$arr = ["key1" => "value1", "key2" => "value2"];
// вместо
$arr["key3"] = "value3";
$arr["key4"] = "value4";
// проще написать так:
$arr += ["key3" => "value3", "key4" => "value4"];Но на этом её применение не заканчивается, конечно.
Чудны́ дела твои, PHP
var_dump(array_pop(range(0,5)));
// Strict Standards: Only variables should be passed by reference in Command line code on line 1
// int(5)
var_dump(array_pop( (range(0,5)) ));
// int(5)В первом случае ПХП обращает наше внимание на то, что функция array_pop требует передачи значения по ссылке, что возможно только для переменной. Поскольку мы передаём результат работы функции, то логично, что ПХП в этом месте нам на это пеняет. Но если обернуть функцию в скобки, то интерпретатор уже всё устраивает.
Я не смотрел
Странный PHP
А вот ПХП, в отличие от ДжаваСкрипта действительно странный. Давайте посмотрим на такой вот код и его результат:
$ php -a
Interactive shell
php > $a = 1; echo $a + $a++;
3
php > $a = 1; echo $a + $a + $a++;
3Как видите, в том и другом случае у нас один результат — «3». Даже первая «тройка», казалось бы, противоречит здравому смыслу, а вторая — тем более. Что же происходит? Давайте разбираться.
Как работает первый пример?
Операция сложения левоассоциативна — разбор агрументов начинается слева направо. Двигаясь таким образом, парсер видит выражение в котором две операции — сложение и постинкремент, у постинкремента приоритет выше, поэтому вычисляется сначала он — возвращая в качестве значения «1» и увеличивая переменную на единицу, потом вычисляется операция сложения, складывая полученную единицу с двойкой (так как постинкремент увеличил значение переменной на единицу). Получается «три».
Похожим образом обрабатывается умножение вместе со сложением: 2 + 2 * 2 = 6, а не 8, потому что умножение имеет более высокий приоритет.
Во втором случае всё происходит похожим образом, но чуть иначе — парсер, обрабатывая левоассоциативное сложение, берёт первые два аргумента, складывает их, получает «двойку», двигается дальше, видит двойку, сложение и постинкремент переменной. Постинкремент более приоритетный, он его вычисляет раньше, возвращая в сложение «единицу», значение переменной увеличивается, но его уже никто не использует — складываются числа «2» (от предыдущего сложения) и «1» (вернул постинкремент). Получается «три».

Тут с восьмой строки начинается второй пример (правда присваивание единицы во втором примере я опустил — как видите второй операции ASSIGN нет).
PHP/FI
На ПХП я начинал программировать с третьей версии, предыдущую, вторую, никогда в глаза не видел. Занялся сегодня археологией, что интересно, интерпретатор собрался под «Мак»
По описанию, вторая версия — удивительно бедный язык, я попытался написать подобие функции var_dump (которой тогда в языке не было), описал в комментариях с чем столкнулся. Надо сказать, некоторые вещи очень сильно удивили.
Вообще, я хотел написать одну функцию, а отступ для вывода скаляра передавать вторым параметром, но оказалось,
В общем, пришлось сделать две разные функции.
#!php.cgi -q
<?
# однострочные комментарии только такие, два слеша не работают
/* так описываются функции */
function var_dump_scalar $var, $indent (
if ($indent) {
/* вот так выглядит конкатенация строки — плюс, а не точка */
/* интерполяции нет, а одинарные кавычки вернут код символа в них */
$indent = strtr(sprintf("%' '" + $indent + "d", 0), "0", " ");
/* функции str_repeat тоже нет, приходится вот так выкручиваться */
} else {
$indent = "";
}
/* всего три типа данных, нет булевого типа, нет ресурсов — fopen вернёт число */
/* объектов тоже нет, конечно же */
switch (gettype($var)) {
case "integer"; /* после case точка с запятой, а не двоеточие */
echo "%sint(%d)\n" $indent, $var;
break;
case "string";
echo "%sstring(%d) \"%s\"\n" $indent, strlen($var), $var;
break;
case "double";
echo "%sfloat(%f)\n" $indent, $var;
break;
}
/* можно было бы вернуть всё через return, но мне не удалось бы продемонстрировать несколько вещей */
);
function var_dump $var (
/* смотрим — передан ли параметр */
if (isset($var)) {
/* пытаемся определить — не массив ли это, отдельного типа нет */
$cnt = count($var);
if ($cnt > 1 || key($var) != "0") {
reset($var);
/* echo умеет принимать форматирующую строку */
echo "array(%d) {\n" $cnt;
/* других способов проверять конец массива нет — только итерировать по длине */
/* цикла for в языке тоже нет */
while ($cnt > 0) {
$key = key($var);
echo " [\"%s\"]=>\n" $key;
next($var);
/* тут только скаляры, так как массивов, кроме одномерных не бывает */
var_dump_scalar($var[$key], 2);
$cnt--;
}
echo "}\n";
} else {
var_dump_scalar($var, 0);
}
} else {
echo "Warning: var_dump() expects exactly 1 parameter, 0 given\n";
}
);
/* ассоциативный массив */
$b["aaa"] = "aaa";
/* числовой массив, отдельного типа массива нет, любой тип является нулевым элементом массива */
/* конструкции array(...) тоже нет, массивы задаются только так — в строку */
$a = 1;
$a[1] = 2;
/* двухмерных массивов у нас нет, это просто операция слияния массивов */
$a[] = $b;
var_dump($a);
/* Результат работы:
array(3) {
["0"]=>
int(1)
["1"]=>
int(2)
["2"]=>
string(3) "aaa"
}
*/
/* вот так заканчиваются PHP-скрипты, знака вопроса у закрывающего тега нет */
>Но самый сок, конечно, это две функции — ClearStack и SecureVar. Их я не использовал, просто не придумал как. Первая является костылём к парсеру (почитайте описание, у меня нет сил это описывать), вторая — фильтрует переменные из ГЕТ-
Itertools для PHP: продолжение
Библиотеку itertools для ПХП я всё же переписал: теперь на вход всем функциям можно передавать что угодно итерируемое. Из ограничений осталось только два: функция iter в моём варианте принимает только один аргумент, а у функций product и izip_longest последний аргумент является обязательным.
Вот, кстати, как переписывается программка итерстрип, написанная мной семь лет назад на «Пайтоне», теперь её можно повторить на ПХП почти
require 'itertools.php';
use function itertools\groupby, itertools\xrange, itertools\chain, itertools\repeat;
function ntrim($letter, $n=3, $replby=1)
{
$shrink_groups = function($letter, $n) {
foreach (groupby(chain(repeat('', $n), $letter, repeat('', $n))) as list($item, $grp)) {
$grp = iterator_to_array($grp);
yield $item || sizeof($grp) < $n ? $grp : '';
}
};
return array_slice(iterator_to_array(chain(...$shrink_groups($letter, $n))), $replby, -$replby);
}
print_r(ntrim(['', 1, 2, 3, '', '', '', '', 4, '', '']));Увы, на ПХП всё ещё не так компактно
Itertools для PHP
Внимание! Библиотеку я модифицировал, описанные ограничения уже не соответствуют действительности!
В прошлом, программируя на «Пайтоне» я не раз восторгался модулем itertools и тем как «Пайтон» легко позволяет обращаться с генераторами — есть несколько способов, хорошее разнообразие.
В ПХП недавно генераторы тоже появились, в версии 5.5 есть ключевое слово yield, позволяющее их создавать. Вчера у меня появилась мысль познакомиться с ними поближе и я решил переписать библиотеку itertools на PHP. В интернете я уже встречал несколько таких попыток, но ни одна из этих реализаций не использовала генераторы.
Пример использования ниже (для запуска примера и работы библиотеки требуется ПХП 5.6 и выше):
require 'itertools.php';
use function itertools\islice, itertools\cycle, itertools\iter;
foreach (islice(cycle(iter("ABC")), 10) as $el) {
echo $el;
}От оригинальной библиотеки несколько отличий.
Если не заброшу библиотеку, то, возможно,
Моя маленькая лепта в PHP7

Читал исходники ПХП7, заметил небольшую ошибку в функции копирования потоков (stream), сегодня мой коммит перенесли в код.
PHP7: scalar hinting
Продолжаю потихоньку наблюдать за развитием ПХП 7. Сегодня с утра выписывал из репозитория и компилировал новую версию, заметил, что в языке появилось указание типов для скаляров — поддерживаются int, float, string и bool.
Выглядит всё довольно естественно:
<?php
class Test
{
public function __toString()
{
return "test";
}
}
function len(string $a): int {
return strlen($a);
}
echo len(new Test);
echo len(new stdClass); // Fatal error: Argument 1 passed to len() must be of the type string, object givenВ данном случае первый вызов ошибку не вызовет (в отличие от второго), так как у объекта есть магический метод __toString, который вызывается, когда объект требуется привести к строке, так что всё логично. Возможные преобразования описаны в документации и ничего нового там нет, всё в соответствии с текущей логикой языка.
Есть и второй режим работы — строгий. Его можно включить при помощи конструкции declare, которая должна быть первой конструкцией языка (теперь у неё приоритет даже над namespace) в файле:
<?php
declare(strict_types=1);
namespace Test;
class Test
{
public function __toString()
{
return "test";
}
}
function len(string $a): int {
return strlen($a);
}
echo len(new Test); // Fatal error: Argument 1 passed to Test\len() must be of the type string, object givenВ этом случае все проверки на тип становятся строгими и никаких неявных преобразований не допускается.
По правде говоря, мне больше импонирует второй режим — несколько лет программирования на «Гоу» и «Пайтоне» позволили оценить все прелести строгой типизации, но существующий код большого продукта привести за приемлемое к строгому режиму будет часто невозможно, на мой взгляд. Видимо поэтому создатели языка ограничили действие строгого режима рамками файла — новые части можно писать, не затрагивая поведение старых, это удобно.
PHP7: функции → опкоды
Как известно, в ПХП7 некоторые функции будут заменены на опкоды для ускорения работы. Вызов функции — дорогая операция в этом интерпретаторе, а опкоды — дёшевы. Я заглянул в исходные коды (см. функцию zend_try_compile_special_func, если интересно) и нашёл там список функций, которые заменяются в текущей версии «семёрки».
Это strlen, все функции is_* (is_float, is_string и прочие), defined, call_user_func_array, call_user_func и assert.
PHP: бесполезная микрооптимизация
Годы назад, когда многие увлекались микрооптимизациями ПХП, была среди них и такая — двойные кавычки заменяли на одинарные, первые были немного медленее. Причина вполне понятна — в строке, окружённой двойными кавычками, разрешена интерполяция — вставка переменных прямо в строку.
Выигрыш был мал, как по мне, но многим и этой разницы было достаточно.
И вот недавно мне кинули ссылку, где утверждается, что много лет назад ситуация поменялась — все виды кавычек обрабатываются с одинаковой скоростью. Я провёл замеры — так и есть, по ссылке не обманули. Я тогда подумал, что с введением в ПХП
Сегодня проверил — примерно так оно и есть (ниже — опкоды ПХП):
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
2 0 E > ADD_STRING ~0 'double+'
1 ADD_VAR ~0 ~0, !0
2 ADD_STRING ~0 ~0, '+quotes'
3 ECHO ~0
4 4 > RETURNНет никакой разницы какой вид кавычек использован (в моём примере — двойные с переменной посередине) — внутреннее представление не различается.
Вывод такой: можно спокойно использовать двойные кавычки везде, кроме мест, где вам важно экранирование — там можно использовать одинарные, если это удобнее.
PHP7: spaceship operator
В ПХП7 вошёл новый оператор — «космический корабль». Я его помню со времён программирования на Перле. Наиболее частое применение он, по всей видимости, найдёт в сортировках:
usort($data, function ($o1, $o2) {
return $o1->value <=> $o2->value;
});Комбинация «меньше», «равно» и «больше» — и есть «космический корабль». Результат его выполнения следующий — если левое меньше, получается
Новый оператор сравнивает даже массивы и объекты, в RFC есть соответствующие примеры:
echo [1, 2, 3] <=> [1, 2, 1]; // 1
echo [1, 2, 3] <=> [1, 2, 4]; // -1
$a = (object) ["a" => "b"];
$b = (object) ["a" => "c"];
echo $a <=> $b; // -1
$a = (object) ["a" => "c"];
$b = (object) ["a" => "b"];
echo $a <=> $b; // 1
// only values are compared
$a = (object) ["a" => "b"];
$b = (object) ["b" => "b"];
echo $a <=> $b; // 0В общем,
Производительность PHP 7 на уровне HHVM?

Если изображённое на графике хоть отчасти правда, то я очень жду выхода ПХП 7 — куда больше, чем нового сезона «Игры Престолов». Автор графика сравнил производительность разных интерпретаторов ПХП на движке «ВордПресс».
По результату видно, что производительность новой версии находится на уровне HHVM!
Значительное отставание HHVM до четвёртого прогона объясняется «прогревом»
Насколько PHP работает быстрее без notices и warnings
Цитата со «СтекОверфлоу»:
У меня в закладках есть статья, в которой автор сделал некоторые замеры по этому поводу. Правда она на французском… вот ссылка (возможно
что-то вы в ней не пойдмёте): Ne faites pas d’erreur.Основные цифры, в помощь людям, не читающим
по-французски:
- 10 тысяч предупреждений, со включенными error_reporting и display_errors : 5162,76 мс
- то же, но настройка display_errors выключена: 136,18 мс
- то же, но error_reporting выключена тоже: 117,79 мс
- и, наконец, после исправления кода, после которого он перестал выдавать предупреждения: 19,51 мс
Это означает, что код на ПХП работает быстрее без сообщений/предупреждений/ошибок, даже если они не отображаются или не попадают
Скорость PHP7 на интерпретаторе Brainfuck

У меня есть старинная забава — брать свой оптимизирующий интерпретатор языка «Брейнфак» и запускать программу, вычисляющую число «Пи» под разными версиями ПХП, чтобы посмотреть их производительность. Тест довольно синтетический, но посмотреть всё равно интересно.
Сравнил в данном случае версии 5.5.14 и сегодняшнюю сборку 7.0, разница всё равно заметна, хоть и не так впечатляюща, как в случае с «Вордпрессом» — там разница в два раза.
Время у меня на скриншоте общее — то есть трансляция программы на «Брейнфаке» в ПХП и запуск получившегося через eval.
PHP7: строки с Unicode-вхождениями

ПХП тут сделал маленький шаг к унификации синтаксиса с Руби, Джаваскриптом и HHVM — ввёл возможность указывать Юникодные символы через последовательность на скриншоте. Сегодня я выписал текущую версию из репозитория (она, кстати, не компилируется без исправлений в коде) и убедился, что всё работает.
Ошибка оракула
В одном из проектов при исправлении ошибки всплыло неожиданное. Кусок кода, который работал ошибочно, родился
Всё работало хорошо, пока ПХП был
Беда в том, что драйвер (в этом месте), похоже, остался в пределах 32 бит, код:
$var = null;
$st = oci_parse($con, "SELECT :int FROM DUAL");
oci_bind_by_name($st, 'int', $var, -1, SQLT_INT);
foreach ([100500, PHP_INT_MAX, 2147483648] as $var) {
oci_execute($st);
echo oci_fetch_array($st, OCI_NUM)[0], "\n";
}Выведет: 100500, -1, -2147483648. Решение я пока знаю только одно: не использовать для целых, которые не помещаются в 32 бита указание типа SQLT_INT.
Проблема в файле oci8_statement.c (это текущий код драйвера из PHP 5.7):
switch (bind->array.type) {
case SQLT_NUM:
case SQLT_INT:
case SQLT_LNG:
...
ZVAL_LONG(entry, ((ub4 *)(bind->array.elements))[i]);
...Значение в этом месте приводится к четырёхбайтному целому.