6 заметок с тегом

php5

PHP7

Всё, PHPNG отправился в «мастер», по всей видимости, это означает, что он появится сразу после версии 5.6, которую мы увидим буквально на днях (я уже немного писал о том, что в ней нового).

PHPNG — новая версия интерпретатора, которая, по всей видимости, будет носить номер семь, чтобы избежать путаницы с PHP6 — неудавшейся попыткой перевести ПХП на Юникод, которую широко в своё время освещали, но сообщество решило от неё отказаться — большинство разработчиков англоязычны и выгоды от этого трудоёмкого процесса не видели.

Новая версия ПХП интересна переработанными внутренностями, изменение которых связано со скоростью. Установленный «ВордПресс» работает на новой версии в два раза быстрее, что впечатляет, конечно. Плохая новость — придётся переделать бо́льшую часть модулей (если не вообще все). Процесс этот сейчас движется, но только для модулей, входящих в язык, несколько модулей (в том числе важный для меня OCI8) пока ещё не поправлены.

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

Передача массива как аргументов в конструктор

Одна из вещей, из-за которых я жду ПХП версии 5.6 — это развёртывание массива в параметры. В принципе, для этого есть функция call_user_func_array, но новый синтаксис куда приятнее:

$result = call_user_func_array('myfunc', $args); // старый синтаксис
$result = myfunc(...$args); // PHP 5.6+

Но помимо приятности, он решает две проблемы.

Во-первых, второй случай более внятен для среды разработки — тут явный вызов функции, который легко находится парсером. Если вы захотите посмотреть где используется какая-то фунция, то это место легко находится. В первом же случае для этой конструкции (и для её брата call_user_func) приходится делать исключение и считать её первый аргумент именем функции.

Во-вторых, сейчас в ПХП нет нормального способа сделать то же с конструктором. Например, если в вашем фреймворке есть класс, возвращающий сервисы (в терминах «Симфони 2»), то в нём придётся делать либо switch/case (для скорости), либо обращаться через рефлекшн:

switch (count($params)) {
    case 0:
        $instance = new $class();
        break;

    case 1:
        $instance = new $class($params[0]);
        break;

    case 2:
        $instance = new $class($params[0], $params[1]);
        break;

    // и так далее…

    default:
        $refClass = new \ReflectionClass($class);
        $instance = $refClass->newInstanceArgs($params);
        break;
}

В новом же синтаксисе код невероятно упрощается:

$instance = new $class(...$params);

А, кроме того, становится более явным и простым. Так что очень радуюсь этому новому синтаксису.

PHP, UTF-8: седьмой этап, давайте что-нибудь сделаем с регулярными выражениями

В прошлый раз я рассматривал, по сути, самый сложный этап — замену всех вхождений $var[$index] и им подобных на вызов моей функции. В процессе замены я несколько раз корректировал заметку и доводил некоторые вещи руками.

Осталось совсем немного, но я столкнулся с неприятной проблемой.

В самой первой части я рассказывал о том как можно решить проблему с UTF-8 в регулярных выражениях — нужно поставить «глагол» (в терминах PCRE) UCP и добавить модификатор «u». Тогда в PCRE начинают адекватно, по отношению к UTF-8, работать «\w», «\W», «\s», «\S» и т. п.

К сожалению, этот «глагол» появился только в PCRE 8.10, а в самом последнем PHP, на сегодняшний момент, содержится 8.02. Как я говорил ранее, новый PCRE можно вкомпилить в PHP при помощи специального ключа.

Внутренние сервисы, где я работаю, используют Debian-системы и Debian-пакеты, все наши проекты выкладываются через эти пакеты. Чтобы поставить нестандартный PHP+PCRE, нужно собрать три пакета (php-cgi, php-cli и модуль к Apache).

Поскольку мы сейчас переходим постепенно на новую версию Ubuntu (с Hardy Heron на Lucid Lynx), то часть машин у нас пока на Цапле, а часть — уже на Рыси. Это значит, что пакетов нужно будет собрать шесть.

Дело это непростое. Я решил попробовать «выдрать» из внутренностей PHP модуль PCRE, переименовать его функции и константы и поставить его как простой подключаемый модуль.

Мне удалось сделать, модуль «свежего PCRE» (можно скачать в разделе «Храню»). Модуль называется ygx, все функции начинаются с префикса «ygx_», константы — с «YGX_».

Что это за сокращение, не спрашивайте, когда я начинал работать, я это знал, а сейчас забыл.

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

Ставится модуль примерно следующим образом:

mkdir /tmp/pcre
cd !:1
wget ftp://ftp.csx.cam.ac.uk/pub/software/programming/pcre/pcre-8.10.tar.bz2
tar xvfj pcre-8.10.tar.bz2
cd pcre-8.10
./configure --prefix=/usr/local/pcre-8.10 --enable-utf8 --enable-unicode-properties --disable-static
make && make install
cd -

wget /files/php_fresh_pcre.zip
unzip php_fresh_pcre.zip
phpize
./configure --with-ygx-regex=/usr/local/pcre-8.10
make && sudo make install
phpize --clean
cd ; rm -rf /tmp/pcre

потом нужно прописать в php.ini строку «extension=ygx.so», перезапустить на FastCGI-сервер или веб-сервер с модулем PHP (смотря чем вы пользуетесь) и, вуаля, у вас есть новые функции — ygx_relace, ygx_match и т. д.

Поскольку к этому моменту все вызовы функций PCRE у меня заменены на вызовы одноимённых методов статического класса UTF, то мне нужно поменять «preg_» на «ygx_» только в одном месте — внутри методов этого класса.

PHP, UTF-8: шестой этап, она же «строки, часть II». Заменяем

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

Я такой парсер начал писать несколько дней назад и очень быстро увидел, куда меня этот путь ведёт. К сожалению, из-за выбранных предпосылок код пришлось выкинуть — я парсил всё регулярными выражениями.

Код я выкинул и начал писать заново, основываясь на токенах (лексемах), которое выдаёт встроенное в язык API токинайзера. По ходу процесса я часто уточнял случаи и производил тестирование. Получился довольно неряшливый парсер, к тому же с некоторыми багами, которые я счёл несущественными. Например, не обрабатывается ситуация isset($var, $var[$idx]).

В PHP isset (а так же empty и unset) — конструкции, а не функции и принимают на вход только переменные. У меня получение символа по индексу заменяется на Utf::get(…), а внутри строки на другую конструкцию — $_ENV[0x5f3759df]->get(…), так что если произойдёт замена внутри isset, будет синтаксическая ошибка.

Т. е. у меня в коде, внутри файла с классом Utf есть ещё и такая строка — «$_ENV[0x5f3759df] = new Utf();». $_ENV я выбрал за то, что это переменная (а переменную внутри строки проще заменять на другую переменную, пусть и с вызовом метода, PHP это позволяется) и это, в терминах PHP, «суперглобальный» массив, то есть он доступен отовсюду.

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

Первая операция, которую вам нужно сделать ещё до запуска парсера — это запустить следующий скрипт. У меня парсер работает с UTF-8, а исходники у меня пока ещё в CP1251. Так что парсер сначала перекодирует код в UTF-8, потом обратно в CP1251. Но я беспокоился — не испортит ли двойная перекодировка что-то в исходниках.

Так что я перекодировал код туда и обратно и сравнил что получилось:

for file in $(find -name '*.php' -or -name '*.inc' -or -name '*.tpl'); do
    sum_new=`iconv -fcp1251 -tutf8 "$file" | iconv -futf8 -tcp1251 | md5sum -b | cut -d' ' -f1`
    sum_orig=`md5sum -b "$file" | cut -d' ' -f1`

    if [ $sum_new != $sum_orig ]; then
        echo $sum_new $sum_orig $file
    fi
done

У меня ничего не поломалось, так что я спокойно запустил пасер. После запуска я проверил синтаксис скриптов интерпретатором и вручную избавил четыре найденные ошибки:

find -name '*.php' -or -name '*.inc' -or -name '*.tpl' | xargs php -l

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

Добавлено позднее: а вот как выглядит метод get в классе Utf:

static public function get($str, $index)
    {
        if (!is_scalar($str)) {
            return isset($str[$index]) ? $str[$index] : null;
        }

        return self::substr($str, $index, 1);
    }

Добавлено позже: нашёл ещё два бага: неверно отрабатываются «$prop[$idx]->value» и «$str[$idx] ==». Второе надо править в регулярном выражении. Я их править не буду, они запросто ищутся утилитой «grep» и правятся руками. Кроме того, не заменяются переменные в heredoc (мне это не нужно).

Конструкции heredoc я нашёл при помощи «grep -FR ’<<<’ *» и просмотрел их все, их было очень мало и ни одна из них не содержала переменных.

Добавлено ещё позже: опаснее оказалась ситуация, которую просто так можно и не заметить: замена вызова func($var[$index]) на func(Utf::get($var, $index), где прототип func описан как func(&$var). Синтаксис PHP вполне допускает такие ситуации, а на деле в массив могут не попасть изменения, которые должны были переданы быть по ссылке.

Способов исправить такую ситуацию два. Первый — логгировать все вызовы Utf::get, которые применяются к массивам и править их на получение переменной по индексу. Второй — найти и просмотреть все такие ситуации. Я выбрал второй способ:

find \( -name '*.php' -or -name '*.tpl' -or -name '*.inc' \) |
xargs egrep -oh 'ion[\t ]+\w+[\t ]*([^{]+' | awk -F'[( ]' '/&\$/{print tolower($2)}' |
sort -u | egrep -v '^preg_' | xargs -I{} egrep -iR  --exclude-dir=.svn {}'[\t ]*(.*?Utf::get' *

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

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

(cat<<FUNCS
'\b(?:array_multisort|array_pop|array_push|\
array_replace_recursive|array_replace|\
array_shift|array_splice|\
array_unshift|array_walk_recursive|\
array_walk|arsort|asort|current|each|\
end|key|krsort|ksort|natcasesort|natsort|\
next|pos|prev|reset|rsort|shuffle|\
sort|uasort|uksort|usort)[ \t]*\([\t ]*[^$)]'
FUNCS
) | xargs -I[] find \( -name '*.tpl' -or -name '*.php' -or -name '*.inc' \) -exec \
egrep [] {} \; -print

PHP, UTF-8, всё ещё строки: упрощаем задачу, часть I

Итак, я достаточно долго думал над задачей как мне заменить все операции получения символа из строки по индексу, чтобы понять насколько она сложна. Решить её можно, но скучно и не нужно. Я подумал, что очень вряд ли у меня в коде встречаются все возможные случаи, которые я рассмотрел в прошлый раз, поэтому я решил посмотреть что же у меня встречается на самом делом.

Выделив типичные случаи использования, частые и простые можно заменить автоматом, редкие и сложные — просто руками. Первым делом было бы неплохо узнать каково общее количество строк, с которыми мне придётся работать:

bolk@dev:~/daproject$ find \( -name '*.php' -or -name '*.inc' -or -name '*.tpl' \) -exec \
egrep '\$\{?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*[\[\{]' -- {} \; |
sort -u | tee out-vars-1.log | wc -l

3670

Около 3,5 тысяч. Я временами дьявольски усидчив, но рассматривать каждую строку из нескольких тысяч, это для меня перебор. Что же делать?

Для начала мне хочется упростить своё регулярное выражение. Конечно, по синтаксису PHP имена переменных можно писать, например, по-русски (в UTF-8), но в реальности, у меня имён, которые содержат не только цифры и латинские буквы, только шестнадцать (в этом я убедился, сделав «egrep -v ’\$\{?\w+’ out-vars.log | wc -l») и все эти строки содержат ерунду — совпадение внутри строк в апострофах, поэтому я с чистым сердцем выбрасываю все такие строки и упрощаю себе жизнь.

Безусловно, моё регулярное выражение захватило очень много лишнего, надо эти ситуации обработать. Например, можно исключить добавление элемента в массив, это легко узнаваемая конструкция ($arr[] = $elm):

bolk@wiki:~/daproject$ IFS='\n'
for line in $(cat out-vars-1.log); do
(echo $line | sed -r 's/\$\w+\[\]//g' | egrep -q '\$\{?\w+[\[\{]') && echo $line
done | sed -r 's/^\s+//' | awk '!t[$0]++' | tee out-vars-2.log | wc -l

3357

Ситуация мало изменилась, давайте попробуем улучшить её значительно. Например, удалить все строки, где происходит только явное обращение к массиву: $arr[’index’] (но не $arr[’index’][0]):

bolk@dev:~/daproject$ IFS='\n'
for line in $(cat out-vars-2.log); do
(echo $line | sed -r 's/\$\w+[\[\{](["'\''])[^\1]*\1(\]|\})([^\[\{])/\2/g' | egrep -q '\$\{?\w+[\[\{]') && echo $line
done | tee out-vars-3.log | wc -l

1353

Ощутимая разница. Открываем получившийся файл и смотрим что бы мы ещё могли выкинуть…

Продолжение следует, я сегодня рано встал, пойду домой.

NOWDOC в PHP 5.3.0

В PHP 5.3.0 появилась новая синтаксическая конструкция: «NOWDOC». Сильно похожа на «heredoc», но с тем различием, что в «nowdoc» нет интерполяции переменных:

Следующий пример выведет в точности тот текст, который находится внутри nowdoc:

echo <<<'EOT'
My name is "$name". I am printing some $foo->foo.
Now, I am printing some {$foo->bar[1]}.
This should not print a capital 'A': x41
EOT;

Второе отличие от heredoc, что nowdoc можно использовать в любом статическом контексте, например так:

class foo {
    public $bar = <<<'EOT'
bar
EOT;
}