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

php

Позднее Ctrl + ↑

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)

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

Злой ПХП

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

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

Когда-то в ПХП жили без копирования при записи (copy-on-write). Думаю некоторые олдскульщики ещё помнят, как когда-то приходилось для экономии обмазывать всё ссылками. Копирование при записи очень облегчает жизнь, но настолько расслабляет, что всё реже задумываешься как всё работает под капотом.

Многие ли увидят тут проблему?

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 объявлена устаревшей.

Ранее Ctrl + ↓