244 заметки с тегом

php

Позднее Ctrl + ↑

PHP8 и strpos

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

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

The needle argument of strpos(), strrpos(), stripos(), strripos(), strstr(), strchr(), strrchr(), and stristr() will now always be interpreted as a string. Previously non-string needles were interpreted as an ASCII code point. An explicit call to chr() can be used to restore the previous behavior.

Я даже не подозревал, что в качестве параметра в функции поиска подстроки сейчас можно (в восьмой версии будет нельзя) передать код символа. Так как в ПХП мало кто следит за типом переменной, можно получить несколько обескураживающие результаты:

// объявили строковую переменную и где-то по коду она превратилась в число,
// 49 — это код символа «1»
$needle = (int) "49";

// выдаёт «Есть»
echo @strpos("12349", $needle) === false ? "Нету\n" : "Есть\n";

// Несколько неожиданно выдаёт «Нету», ищется символ с кодом «49» (единица)
echo @strpos("02349", $needle) === false ? "Нету\n" : "Есть\n";

В ПХП8 число будет преобразовано в строку и искаться будет именно строка, то есть оба этих примера выдадут «Есть».

Выполнение запроса с таймаутом

В продукте, который мы делаем в нашей компании, не используется PDO для соединения с базой данных — так исторически сложилось и вряд ли имеет смысл менять. Причина тому — оптимизации; мы широко используем эскуэль и задействуем диалектные особенности используемой СУБД (в нашем случае это «Постгрес»). То есть универсальность PDO нам ничего бы не дала.

Недавно возникла задача выполнения запроса с таймаутом — если выполнение запроса занимает больше времени, чем по нашему мнению пользователь готов ждать, то лучше прервать запрос и показать заглушку. В PDO кажется такая возможность есть (параметр можно задать, но мы не тестировали), а в постгресовском модуле для ПХП готового способа не оказалось.

Мы долгое время перебирали идеи (вплоть до внешнего процесса с таймером), но ничего не выглядело достаточно хорошо, чтобы помещать это в код. В конечном счёте мой братишка предложил очень простое решение. Ответ лежал в работе с асинхронным АПИ. Обычно это выглядит как-то так:

$con = pg_pconnect("…");
if ($con === false) {
    throw new \RuntimeException('Could not connect');
}
// асинхронно выполняем запрос, в синхронном варианте тут было бы pg_query
if (pg_send_query($conn, "select * from users")) {
    // тут можно делать что угодно, пока мы не будем готовы принять результат
    // …
    // синхронно получаем результат
    var_dump(pg_get_result($conn));
}

Поскольку pg_send_query неблокирующий, то можно было бы попытаться как-то проконтролировать не истёк ли таймаут. Но как это сделать? Вот что он придумал:

function waitForResult($connection, callable $callback, int $timeout): void
{
    $stream = pg_socket($connection);
    while (pg_connection_busy($connection)) {
        $read = [$stream];
        $write = $except = null;

        $numChangedStreams = stream_select($read, $write, $except, 0, $timeout);
        if ($numChangedStreams > 0 || $numChangedStreams === false) {
            break;
        }

        $callback();
    }
}

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

SplFileObject

ПХП, как известно, вырос из процедурного языка — во многих местах до сих пор видны «уши» процедурного подхода. Одно из таких мест — работа с файлами. Большинство ребят используют тут исключительно процедурный подход. Что-то вроде:

$fp = @fopen("test.log", "ab");
if ($fp !== false) {
    // в реальном коде надо бы проверить результат этих функций,
    // тут упрощённый код для иллюстрации принципа
    flock($fp, LOCK_EX);
    fwrite($fp, "Log line\n");
    fclose($fp);
} else {
    throw new \RuntimeException(error_get_last()['message']);
}

К счастью с 2005 года в языке существует класс SplFileObject, который умеет делать тоже самое, но в ООП-стиле.

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

$fp = new \SplObjectFile("test.log", "ab");
$fp->flock(LOCK_EX);
$fp->fwrite("Log line\n");
$fp->flock(LOCK_UN);

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

$x = [
    new \SplFileObject('test.log', 'ab'),
    &$x
];

$x[0]->flock(LOCK_EX);
unset($x);

echo "Written\n";
sleep(10);

Если попробовать этот код запустить в двух разных консолях, то второй процесс будет ждать первый. По крайней мере по «МакОС» и ПХП 7.4.

Но, если заменить unset на $x = null, произойдёт удивительное — второй процесс получит возможность записать в файл сразу же, то есть сборщик мусора убирает его моментально. Я не знал о таком различии между этими двумя способами уничтожения объекта. Любопытно — с чем это связано и изменится ли в будущем?

Добавлено позднее: я догадался почему такой эффект, попробуйте догадаться тоже.

Переход на PHP 7.4

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

Во-первых, в недокументированной функции net_get_interfaces появилось новое поле — состояние сетевого интерфейса, в одном месте у нас вышло предупреждение в коде из-за этого;

Во-вторых, часть функций для работы с массивами (например, key, pos и некоторые другие), которые раньше работали с объектами класса ArrayObject внезапно перестали работать:

$arr = new \ArrayObject(['key' => 'value']);
// теперь выдаёт NULL, а не «key»
var_dump(key($arr));

Маловероятно, что кто-то ещё столкнётся с этим несовместимостями — всё-таки они очень специфичные, но всё же имейте ввиду.

Добавлено позднее: однако я неправ — про прекращение поддержки объектом класса ArrayObject функций работы с массивами вскользь всё же упомянуто.

Дескрипторы файлов и PHP

На днях подошёл на работе один из программистов и спросил не знаю ли я способа как передать из ПХП во входной поток утилиты не один файл, а два. Когда мы разобрались, что он имеет ввиду, оказалось, что задача следующая: имеется утилита командной строки, которая делает PDF из двух файлов — тела и заголовочной части. Нужно работать с ней из ПХП, но создавать промежуточные файлы не хочется.

Один файл передаётся во входной поток, с этим многие знакомы и проблемы нет, но как передать два файла? В командной строке такой способ есть — создаём ещё один файловый дескриптор, связываем его с файлом или вводом/выводом какой-то команды и радуемся. Например:

#!/bin/bash
# связываем третий дескриптор с чтением из указанного файла
exec 3< /etc/passwd
# связываем четвёртый со входом команды, которая будет писать в другой файл
exec 4> >(cat > /tmp/passwd)
# читаем из третьего дескриптора, пишем в четвёртый
cat <&3 >&4

А можно ли так в ПХП? Документация к proc_open говорит, что да:

The file descriptor numbers are not limited to 0, 1 and 2 — you may specify any valid file descriptor number and it will be passed to the child process. This allows your script to interoperate with other scripts that run as «co-processes».

Ну что же, давайте попробуем:

<?php
// заполняем первые три файловых дескриптора как обычно и,
// кое-что новое, — создаём ещё один
$ds = [
    'stdin'  => ['file', '/dev/null', 'r',],
    'stdout' => ['pipe', 'w',],
    'stderr' => ['file', '/dev/null', 'w',],
    'stdnew' => ['pipe', 'r',],
];

// передаём наши дескрипторы команде ???, специальный файл
// /???/??/3 связан с третьим (с нуля) дескриптором
$process = proc_open("cat /dev/fd/3", array_values($ds), $pipes);

// связываем дескрипторы с переменными, указанными в массиве $??
// (пожалуйста не используйте ??????? без ?ℎ???? в своём коде)
extract(array_combine(array_intersect_key(array_keys($ds), $pipes), $pipes));

// пишем в наш новый дескриптор
fwrite($stdnew, "Hello world!\n");
fclose($stdnew);

// читаем из вывода переданное
fpassthru($stdout);
fclose($stdout);

proc_close($process);

Всё работает! Фраза, переданная на такой не совсем стандартный вход, благополучно выводится на экран.

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

PHP: доступ к приватным свойствам

Кинули ссылку на один из способов получения доступа к приватным свойствам в ПХП, тут создаётся функция, которая вызывается в контексте объекта с закрытым свойством:

function inspect_closure(object $o, string $p)
{
    return (function () use ($p) {
        return $this->$p;
    })->call($o);
}

Я решил вспомнить сколько таких способов вообще существует в ПХП.

Сначала, очевидно, на ум должен прийти рефлекшн, ведь он для этого и предназначен:

function inspect_reflection(object $o, string $p)
{
	$refClass = new \ReflectionClass(get_class($o));
	$refProp = $refClass->getProperty($p);
	$refProp->setAccessible(true);

	return $refProp->getValue($o);
}

Теперь экзотика. Например, мне вспоминается доступ через преобразование в массив, я о нём уже однажды писал. При преобразовании объекта в массив его приватные и защищённые свойства видны по специальным именам в полном соответствии с документацией:

function inspect_array(object $o, string $p)
{
    return ((array) $o)[
    	sprintf("\0%s\0%s", get_class($o), $p)
    ];
}

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

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

function inspect_serialize(object $o, string $p)
{
	$class = get_class($o);
	$serialized = serialize($o);

	if (preg_match("/s:\d+:\"\\0{$class}\\0{$p}\";([^;]+;)/s", $serialized, $m)) {
		return unserialize($m[1]);
	}

	throw new \UnexpectedValueException('Something went wrong.');
}

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

Стрелочные функции в PHP 7.4

Версия 7.4 интерпретатора ПХП, которая должна появиться осенью, смотрится очень привлекательно. Одни только стрелочные функции чего стоят — код будет выглядеть куда компактнее.

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

$f = 'magic';
($f = fn() => var_dump($f))();

Я ожидал, что в данном случае функция выведет своё представление, но на самом деле выводится слово «magic». Определённая логика тут есть конечно, — сначала создаётся тело функции, вместе с замыканием всех используемых переменных в локальной зоне видимости, и только потом получившееся присваивается переменной.

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

$f = 'magic';
($f = fn($f) => var_dump($f))($f);

Вот практическое использование рекурсии со стрелочными функциями на примере программы, генерирующей SBox для ГОСТовых алгоритмов:

$k=fn($i)=>ord('@`rFTDVbpPBvdtfR@¬p?â>4¦é{zãq5§è'[$i]);$p=fn($x)=>($f=fn($x,$f,$l=256)=>
--$l*$x^$l?$f($x+$x^($x>>7)*285,$f,$l):($l%17?$k($l%17)^$k(17+$l/17)^17:$k($l/17)^188))($x,$f);

Кстати, неделю назад началось соревнование по написанию самой короткой версии такой программы, рекорд на настоящий момент — 58 символов. У меня получилось 183, но за размером я не гнался, ПХП чересчур многословен для такого, было интересно сам синтаксис погонять.

Шило на мыло

Всё-таки я большой поклонник статической типизации в языках. Жаль, что ПХП она только-только начинает проникать.

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

<?php
$array = '';
$array[PHP_INT_MAX] = 1;

Главное тут в том, что массив почему-то проинициализирован как строка.

Несмотря на эту странность, всё работало — ПХП на втором присваивании преобразовывал строку в массив. А какое-то время назад этот код стал валиться с нехваткой памяти. Я сегодня разбирался с этой ошибкой.

Оказалось, что в ПХП 7.1 и выше (мы недавно перешли с 7.0 на 7.2), преобразование в массив тут больше не происходит. Зачем-то одно странное поведение заменили другим — теперь в этом коде создаётся гигантская строка, состоящая из пробелов и в позицию PHP_INT_MAX записывается символ «единица». Вот память и кончается. ?

Падение libmemcached

С годами «Мемкешед» всё меньше кажется хорошим выбором в связке с ПХП. Всё время какие-то проблемы. Недавно столкнулись с очередной — при выставлении определённых опций распределения ключей на несколько серверов, модуль мемкешеда в ПХП иногда крашится вместе с интерпретатором.

Баг известный и проявляется на системах с libmemcached 1.0.16, а у нас ЦентОСь, там новее нету.

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

Третий параметр define

ПХП неисчерпаем, как атом, — читал какую-то статью о развитии языка (ссылку потерял) и узнал, что у функции define есть третий, необязательный параметр. Столько лет программирую на ПХП и не знал!

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

А ещё define возвращает результат — успешна ли операция:

@var_dump(define("True", false, true)); // bool(false), будет конфликтовать со встроенной константой
var_dump(define("True", false)); // bool(true)

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

Ранее Ctrl + ↓