php
Cookie в PHP 7.4
Обнаружилась очень странная проблема у меня на сайте — некоторые комментаторы стали подписываться с плюсом между именем и фамилией. В первый раз я решил, что у человека это вышло случайно или, может, это
Выяснилось, что плюс появился сам собой, люди его и не думали ставить. Так я понял, что столкнулся
В чём примерно дело, понятно стало почти сразу — в вебе пробел иногда кодируется как «+», видимо ошибка
Проблема воспроизводится у тех комментаторов, которые оставили свой первый комментарий до того, как мой сайт перешёл на ПХП 7.4. В этой версии сменился способ кодирования пробела в «куках» (см. на скриншоте) — был +, стало %20.
В принципе, можно вернуть
if (strpos($_SERVER['HTTP_COOKIE'], '+') !== false) {
parse_str(strtr($_SERVER['HTTP_COOKIE'], ['; ' => '&']), $_COOKIE);
}Но хорошо бы внедрить в движок блога код, который будет перекодировать такие куки у пользователей.
FFI: баг не будет исправлен?
В пхпешном FFI есть баг — можно, вызывав сишную функцию случайно изменить значение совершенно других переменных
Очень не хочется это признавать, но похоже тот баг, который я обнаружил в реализации FFI (интерфейса к языку Си) в ПХП, исправлен не будет. Собственно, на это мне давно указал один из читателей в комментариях к прошлой заметке на эту тему, но мне всё не верилось.
В тикете, который я завёл по проблеме, один из разработчиков языка написал, что придерживается мысли, что это преднамеренное проектное решение. Не думаю, что это действительно так, наверняка получилось случайно, но как всё поправить, не снизив существенно производительность модуля FFI, — неясно.
Видимо, придётся жить с тем, что есть.
В прошлый раз, в качестве временной меры, я предлагал использовать str_repeat (или эквивалент) для того, чтобы разорвать запутанность двух переменных и таким образом предотвратить неожиданное их изменение. Это плохой приём, но я его задумывал в качестве временной меры.
Но раз это навсегда, давайте избегать сторонние эффекты правильно. Тот же читатель предлагает более корректный способ:
Первый параметр memcpy должен быть указателем на отдельно выделенную память достаточного размера. И в FFI есть метод для выделения памяти: FFI::new. Его и надо использовать вместо str_repeat.
Таким образом правильная работа с указателями должна выглядеть примерно вот так:
function memcpy(?string &$dst, string $src):void
{
$len = strlen($src);
// изготавливаем тип, в котором уместится копируемая строка
$type = FFI::arrayType(FFI::type('char'), [$len]);
// выделяем место под изготовленный тип
$destination = FFI::new($type);
FFI::cdef('char *memcpy(char *dst, const char *src, size_t len);')
->memcpy($destination, $src, $len);
// преобразуем область памяти в строку
$dst = FFI::string($destination, $len);
}
$rock = "ROCK";
var_dump($rock); // «ROCK»
memcpy($rock, "SOCK");
var_dump($rock); // «SOCK»Что тут происходит?
Мы хотим вызвать сишную функцию для копирования одной строки в другую. В месте, куда мы собираемся копировать, должно быть выделено достаточное количество памяти, мы её выделяем методом FFI::new, передавая ему на вход специально изготовленный тип — указатель на массив однобайтовых символов длиной c копируемую строку.
После копирования преобразуем область памяти в пхпешную строку. Память, выделенная при помощи FFI::new, освободится автоматически.
Трудности перевода
Читая документацию ПХП по модулю «Enchant», наткнулся на неожиданное, для официальной доки, сравнение. В английском варианте там «old as sin» («старый, как грех»), в испанском — «antiguo como el comer» («старый, как еда»), в других языках ничего примечательного.
UnknownPlatformer

В ней пока мало что есть — лес с эффектом параллакса, персонаж на первом плане, который управляется стрелками на клавиатуре, но всё это уже очень интересно.
Демка сделана на авторском микродвижке «Bic» (Because I Can), выглядит хорошо и я надеюсь на продолжение, к счастью автор забрасывать (пока?) его не намерен — следующим шагом собирается добавить камеру, нормальный viewport и внедрить паттерн «Entity component system». Движок мультиплатформенный, работает под всеми тремя основными операционными системами — «Виндоуз», «МакОС», «Линукс».
Если тоже захотите полюбопытствовать, то просто скачайте архив, распакуйте его
composer install
:; [[ $(uname) == "Darwin" ]] && brew install sdl2_image
php app.php
Юзерпики в «Эгее»
Во второй раз занимаюсь разовой конвертацией блогов в формат «Эгеи», надо бы записать
В основном обращаться приходится к трём таблицам, имеющим довольно говорящие названия: Notes — сами заметки, Comments — комментарии к ним (там же хранятся и авторы комментариев, единого профиля у них нет) и Aliases — адреса, по которым доступны страницы (в моей схеме в этой таблице нет поля Alias, куда и записывается сам адрес).
Назначения полей, в основном, понятны из названий, кроме разве что поля Stamp — это временная метка создания чего либо в секундах (timestamp) и поля с префиксом GIP, об этом чуть ниже.
Если вам интересны метки (теги), то понадобятся ещё две таблицы: Keywords — где хранится русское название метки и её урл и NotesKeywords, таблица, связывающая метки и заметки.
Теперь про GIP. Если флаг IsGIPUsed в таблице Comments установлен в ноль, то это прежняя схема хранения автора комментарий — без юзерпика, чтобы юзерпик появился, надо выставить IsGIPUsed в единицу, в поле GIP записать сервис, через который была проведена аутентификация (facebook, vk или twitter), а в GIPAuthorID записать некий идентификатор пользователя во внешнем сервисе.
Юзерпик нужно положить в pictures/avatar/ с именем, составленным из полей GIP и GIPAuthorID, записанных через дефисоминус. Например: vk-1.jpg — это был бы юзерпик Дурова, создателя «Вконтакте», если бы он
Тут есть одна интересная деталь. Дело в том, что у меня в блоге, когда он ещё работал на самописном движке, уже были юзерпики, были они и у Олега Петровича, блог которого я переносил сегодня ночью. Причём и у меня, и у Олега авторизация была своя собственная, не обязательно через те сервисы, которые поддерживает «Эгея». Как же мне удалось перенести юзерпики?
«Эгея» в этом месте имеет простой механизм расширений — в принципе в неё несложно добавить любой внешний механизм аутентификации, что я и сделал. Подобрал в папке system/theme/images/, где движок ищет картинки внешних сервисов, подходящую картинку (email.svg) и написал фейковый плагин для сервиса email, который положил в файл system/gips/email.php:
<?php
class E2GIPEmail extends E2GIP {
protected $type = 'email';
private function _get_instance() {
return (object) [];
}
public function get_auth_url() {
return '';
}
public static function get_profile_url($id, $link) {
return false;
}
public function callback() {
return true;
}
}Файл плагина (и его тип) должны называться так же как и выбранная картинка — email и в базе его пользователи записываются с тем же значением поля GIP. В итоге, все свои старые комментарии я положил, как принадлежащие сервису email, а в качестве GIPAuthorID (это строка) взял
Добавлено позднее: невероятно, но факт, оказалось, что GIP-плагины можно класть ещё и в users/gips/, что гораздо удобнее, так как в этой папке он не перетрётся при обновлении «Эгеи».
Забавный баг в FFI (PHP 7.4.2)
В последнее время очень заинтересовался FFI в ПХП — интерфейсом к языку Си, появившимся в версии 7.4. Очень полезное, как по мне, нововведение, позволяющее расширять язык, обходясь минимальными знаниями о других языках программирования.
Очевидно, потребность
К несчастью, модуль FFI в ПХП
Небольшой код для иллюстрации проблемы:
const IRRELEVANT = "Hello ";
echo IRRELEVANT; // Выведет «Hello»
FFI::cdef('char *memcpy(char *dst, const char *src, size_t len);')
->memcpy("Hello ", "world!", 6);
echo IRRELEVANT; // Выведет «world!»В чём суть? Мы объявляем константу IRRELEVANT, убеждаемся, что она содержит присвоенное ей значение, потом вызываем через FFI копирование одной строки в другую, и константа внезапно меняет своё значение. Как это произошло? Чтобы понять, немного модифицируем программу.
function memcpy(string $dst, string $src):void
{
FFI::cdef('char *memcpy(char *dst, const char *src, size_t len);')
->memcpy($dst, $src, strlen($src));
}
$rock1 = str_repeat('ROCK', 1);
$rock2 = str_repeat('ROCK', 1);
var_dump($rock1, $rock2); // Выведет «ROCK» два раза
memcpy($rock1, "SOCK");
memcpy($rock2, "LOCK");
var_dump($rock1); // выведет «SOCK»
var_dump($rock2); // выведет «LOCK»Что у нас тут? Вызов memcpy обёрнут в функцию, которая ничего не возвращает и ничего не должна модифицировать — параметры передаются по значению, а не ссылке. Тем не менее, если вызвать объявленную функцию и передать в неё переменные, их значение будет изменено.
В ПХП для обеспечения производительности используется механизм копирования при записи — если
В силу этой особенности, переменные остаются ссылочными, попадая в таком виде в сишный код, где модифицируются — вызов memcpy меняет первую переданную в него переменную, копируя в неё значение второго параметра.
Кстати, если изменить строку инициализации переменных на вот такую:
$rock1 = $rock2 = str_repeat('ROCK', 1);то после вызовов memcpy обе переменные получат значение «LOCK» — работает тот же самый механизм.
Почему же в первом листинге изменилось значение константы? Сейчас разберёмся, осталось совсем немного.
Ещё одной распространённой техникой увеличения производительности (и снижения потребления памяти заодно) является интернирование, её используют многие интерпретаторы и компиляторы. В общем случае она заключается в том, что
Таким образом константа IRRELEVANT и строка «Hello » в первом параметре — суть одна и та же область памяти. Когда функция memcpy модифицирует эту область, меняются сразу все значения всего, что, для оптимальности, ссылается на это же место. Естественно, константа изменяется тоже.
Поскольку интернирование не работает во время выполнения, результат str_repeat('ROCK', 1); не интернируется и во втором листинге такого эффекта не создаётся.
Как же разорвать эту мистическую связь и заставить работать код так как задумывалось? Для этого нужно, чтобы интерпретатор создал новую область памяти, которую будет портить FFI, не затрагивая нашу переменную. Это можно сделать, например, при помощи уже упомянутой функции str_repeat:
function memcpy(string $dst, string $src):void
{
// Создаём новое место, которое испортит memcpy
$dst = str_repeat($dst, 1);
FFI::cdef('char *memcpy(char *dst, const char *src, size_t len);')
->memcpy($dst, $src, strlen($src));
}
$rock = 'ROCK';
var_dump($rock); // Выведет «ROCK»
memcpy($rock, "SOCK");
var_dump($rock); // Так же выведет «ROCK»Внимание! Это некорректный способ, корректный описан в более поздней заметке.
Естественно, подойдут и substr, и sprintf, и вообще любые функции, в общем случае возвращающие модифицированную строку. Интересно, что implode([$dst]) от бага не защищает, видимо для этого случая внутри работает
Всё описанное верно для версии актуальной на текущий момент — 7.4.2. Надеюсь, в скором времени этот довольно забавный баг устранят и необходимость в таких ухищрениях исчезнет.
PHP: FFI vs. exec
Нередко отсутствующую в ПХП функциональность наскоро заменяют вызовом утилит командной строки. И мне стало интересно взглянуть на досуге — может ли вызов через ffi (интерфейс к Си, который появился в ПХП 7.4) быть хорошей альтернативой.
Говорят, вызов через ffi работает очень медленно, такова природа этого способа (и мы это видим в других языках), это, конечно, очевидно, но медленно насколько?
Для сравнения была взята функция, создающая временный директорий по маске, которую я использовал в одной из заметок, и её аналог, использующий внешнюю утилиту:
$mkdtemp = function (string $template): string {
$cmd = '/usr/bin/mktemp -d '.escapeshellarg($template);
$dir = exec($cmd, $out, $ret);
if ($ret === 0) {
return $dir;
} else {
['message' => $message, 'type' => $type] = error_get_last();
throw new RuntimeException($message, $type);
}
};Весь код был обёрнут в цикл на 10000 итераций и запущен. Я провёл по десять запусков, а результат усреднил.
Создание директория через ffi заняло примерно 0,77 секунд на 10 тысяч итераций, а через утилиту командной строки — примерно 72,62 секунды на те же 10 тысяч итераций.
Конечно
PHP FFI
В ПХП 7.4 предпринята очередная попытка обзавестись интерфейсом к языку Си. Кажется наконец за это расширение взялись
Для интереса посмотрел что за АПИ получилось. В принципе, всё очень просто. Вот как выглядит
/**
* @method object mkdtemp(string $template)
* @method object strerror(int $errnum)
* @property-read int $errno
*/
$ffi = FFI::cdef('
// Импортируем функцию создания временного директория
char *mkdtemp(char *template);
// Импортируем функцию перевода кодов ошибок в строку
char *strerror(int errnum);
// Импортируем переменную, хранящую коды ошибок
int errno;
');
/**
* Фунция для создания временных директориев по маске
* @param string $template Шаблон создания (см. man mkdtemp)
* @return string Путь до временного директория
*/
$mkdtemp = function (string $template) use ($ffi): string {
$result = $ffi->mkdtemp($template);
if ($result === null) {
$errno = $ffi->errno;
$errstr = $ffi->strerror($errno);
throw new RuntimeException(FFI::string($errstr), $errno);
}
return FFI::string($result);
};Как можно понять из кода, создаётся объект, в который импортируются сущности, перечисленные в сишной нотации в первом параметре. В общем случае вторым параметром требуется указать библиотеку для импорта, но в данном случае работает и так.
Не знаю заработает ли этот код без изменений под Линуксом и Виндоузом, у кого есть под рукой, посмотрите, пожалуйста.
В этом примере параметры в импортированные функции передаются как обычные типы ПХП, а некоторые возвращаемые значения требуют конвертации. Так char * возвращается как специальный объект, который нужно преобразовывать в строку вызовом метода FFI::string.
Вот тривиальный текстовый пример использования созданной выше функции:
$template = implode(DIRECTORY_SEPARATOR, [sys_get_temp_dir(), 'ffiphp.XXXXXX']);
try {
$tmp = $mkdtemp($template);
rmdir($tmp);
echo $tmp, "\n";
} catch (RuntimeException $e) {
echo $e->getMessage(), "\n";
}Суть происходящего проста: если директорий удалось создать, то он будет уничтожен, а его имя — выведено на экран, иначе появится текстовое сообщение об ошибке.
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. Пока релиз не производит впечатления стабильного — в свежих версиях правятся очень неприятные баги. Мы при тестировании нашли два сюрприза, которые не описаны в списке изменений.
$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), преобразование в массив тут больше не происходит.
Падение libmemcached
С годами «Мемкешед» всё меньше кажется хорошим выбором в связке с ПХП. Всё время
Баг известный и проявляется на системах с libmemcached 1.0.16, а у нас ЦентОСь, там новее нету.
Пока отказались от этой опции,
Третий параметр define
ПХП неисчерпаем, как атом, — читал
Эта функция определяет константу, а третий параметр определяет чувствительность к регистру. Если он установлен в true, то такая константа может быть использована в коде в любом регистре.
А ещё define возвращает результат — успешна ли операция:
@var_dump(define("True", false, true)); // bool(false), будет конфликтовать со встроенной константой
var_dump(define("True", false)); // bool(true)Кстати, вторая строка константу вроде успешно устанавливает, но на деле воспользоваться ей невозможно — булевые константы языка к регистру нечувствительны и видимо перекрывают пользовательские. Странно, что тут нет ошибки, ещё один камень в огород ПХП.
Злой ПХП
Недавно
var_dump(DateTime::createFromFormat("d.m.Y", "01.01.1970"));
/*
object(DateTime)#1 (3) {
["date"]=>
string(26) "1970-01-01 18:43:06.000000"
["timezone_type"]=>
int(3)
["timezone"]=>
string(3) "UTC"
}*/Совершенно неинтуитивно, что в разобранную дату в данном случае подставится текущее время. У меня даже нет идей для чего это могло бы пригодиться. А чтобы текущее время не подставлялось, нужно использовать символ «восклицательный знак» или «вертикальная черта»:
var_dump(DateTime::createFromFormat("!d.m.Y", "01.01.1970"));
/*
object(DateTime)#1 (3) {
["date"]=>
string(26) "1970-01-01 00:00:00.000000"
["timezone_type"]=>
int(3)
["timezone"]=>
string(3) "UTC"
}*/Копирование при записи и reset
Многие ли увидят тут проблему?
function array_first(array $array)
{
return reset($array);
}Проблема в том, что reset меняет внутреннее состояние массива, а значит вызывает копирование при записи — в этот момент массив будет удвоен. Это небольшая неприятность для небольших массивов, но если в нём миллион элементов…
Короче, можно не заметить как исчерпаешь память. Кстати, ПХП тут мог бы делать оптимизацию (но не делает) — не менять указатель, если до вызова reset он был на первом элементе. В этом конкретном примере такая оптимизация иногда могла бы экономить память.
Но в общем случае использование тут reset — плохая идея, на мой взгляд, лучше сделать так:
function array_first(array $array)
{
return current(array_slice($array, 0, 1));
}Ту же проблему (с удвоением массива) содержит в себе и до сих пор встречающаяся итерация через each или next — вызов этих функций так же модифицирует внутренний указатель массива. Впрочем лечится легко — использованием foreach.
К слову, в ПХП 7.2 функция each объявлена устаревшей.