ПХП и строгая типизация

В ПХП много странностей, ещё одна дала о себе знать в неожиданном месте. Сначала немного теории.

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

ПХП использует эту схему везде, кроме расширений и встроенных функций. Понятие «необязательный параметр» там есть, но обрабатывается иначе — у параметра указывается тип (например «строка»), необязательность и «нулабельность» (можно ли в этом параметре принимать null в качестве значения).

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

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

Например, у нас преспокойно работал примерно такой код:
    public function put(Serialized $object, $eventName, $extraEventData, $uniqueId = null)
    {
        return DI::gearman_client()->doBackground(
            $this->queueName,
            igbinary_serialize(
                [
                    'object' => $object,
                    'event_data' => $extraEventData,
                    'event_name' => $eventName,
                ]
            ),
            $uniqueId
        );
    }
Всё работало корректно, пока не пришёл ПХП7 и мы не стали потихоньку переползать на строгую типизацию. Вечером я закоммитил изменения в этом файле, которые позволили включить строгую типизацию, а за завтраком поймал в логах странную ошибку, которая сообщала мне, что в метод doBackground время от времени получает в качестве последнего параметра null, а так нельзя.

Сначала я недоумевал, а потом догадался, что случилось — у doBackground последний, необязательный парамер имеет тип «строка» и он не «нулабельный». То есть в строгой типизации я его должен либо не передавать вовсе, либо передавать туда исключительно строку. А null, который передавался туда до перехода на строгую типизацию более не подходит, ибо он не строка.

Пришлось переписать более уродливо:
    public function put(Serialized $object, string $eventName, $extraEventData, string $uniqueId = null)
    {
        $args = [
            $this->queueName,
            igbinary_serialize(
                [
                    'object' => $object,
                    'event_data' => $extraEventData,
                    'event_name' => $eventName,
                ]
            ),
        ];

        if ($uniqueId !== null) {
            $args[] = $uniqueId;
        }

        return DI::gearman_client()->doBackground(...$args);
    }
Странно то, что у необязательного параметра нет никакого значения по-умолчанию, которое можно было бы указать. В принципе, даже если бы оно было, это тоже не очень удобно.

Не смог найти, но я помню, что было чьё-то предложение расширить синтаксис ПХП — разрешить при вызове функции или метода использовать ключевое слово «default» для указания, что в данном месте нужно использовать значение по-умолчанию. Мне кажется тут бы оно пригодилось.
7 комментариев
25 июля 2017 23:18

PostgreSQL и PHP — слон слону не товарищ

Продолжаю серию удивительных открытий в мире перехода на «Постгрес». В документации к функции pg_execute есть малозаметное примечание к последнему параметру — в нём передаются значения для запроса:

Warning Elements are converted to strings by calling this function.

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

Перевод такой: все значения, которые передаются, приводятся к строкам. Код, который это выполняется выглядит так (взял из ПХП 7.2):
if (num_params > 0) {
        int i = 0;
        params = (char **)safe_emalloc(sizeof(char *), num_params, 0);

        ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(pv_param_arr), tmp) {
                ZVAL_DEREF(tmp);
                if (Z_TYPE_P(tmp) == IS_NULL) {
                        params[i] = NULL;
                } else {
                        zval tmp_val;

                        ZVAL_COPY(&tmp_val, tmp);
                        convert_to_cstring(&tmp_val);
                        params[i] = estrndup(Z_STRVAL(tmp_val), Z_STRLEN(tmp_val));
                        zval_ptr_dtor(&tmp_val);
                }
                i++;
        } ZEND_HASH_FOREACH_END();
}

pgsql_result = PQexecParams(pgsql, query, num_params,
                                NULL, (const char * const *)params, NULL, NULL, 0);
Вышеупомянутое примечание есть только у этой функции, но на деле в любом месте, где привязываются значения, всё выглядит примерно так же (это касается и модуля ПДО).

Думаю, это связано с типизацией «Постгреса». Взять к примеру числа — два числовых типа ПХП нельзя адекватно преобразовать в россыпь типов «Постгреса», а если привести к неверному типу будут проблемы — в этой СУБД есть понятие перерузки функций, то есть функция выбирается не только по имени, но и по числу и типам параметров.

Поэтому и выбраны строки — они приведутся к нужному числовому типу сами собой, со строками это работает. К сожалению в этом преобразовании кроются и проблемы.

Ещё когда мы работали только с «Ораклом», заметили, что если вместо чисел привязывать строки, то иногда планы выполнения запросов меняются в худшую сторону. Лёгкость обращения с типами в ПХП иногда к этому приводит — переменная, используемая для хранения числа, имеет строковый тип.

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

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

Мой братишка придумал оригинальное решение — определять позиции на которых мы привязываем числа и автоматически внутри нашего фреймворка в этом месте запроса указывать тип bigint явным образом. То есть добавлять после плейсхолдера параметра конструкцию «::bigint».

Пришлось изменить несколько наших хранимых процедур, но в целом всё плошло довольно гладко.
1 комментарий
13 апреля 2017 21:19

🐘 Кое-что новое об анонимных функциях PHP

Век живи, век учись. Даже чтение документации не избавляет от дыр в её знании, стоит пропустить одно-два предложения и не узнаешь о какой-то конструкции. Совершенно случайно заметил в документации любопытное сочетание ключевых слов. Внимание на пример ниже:
class Tester
{
	public function returnFunc()
	{
		return function() {
			return "Function call.\n";
		};
	}

	public function returnStaticFunc()
	{
		return static function() {
			return "Static function call.\n";
		};
	}

	public function __destruct()
	{
		echo "Tester died.\n";
	}
}

$holder = (new Tester)->returnFunc();
gc_collect_cycles();
echo $holder(); // «Function call.»
unset($holder);

$holderStatic = (new Tester)->returnStaticFunc();
gc_collect_cycles(); // дважды «Tester died»
echo $holderStatic(); // «Static function call.»
Обратите внимание на последовательность «return static». Дело в том, что анонимные функции в ПХП, если они создаются в методе, неявно захватывают его контекст, то есть переменную $this. В этом нет никакой беды, если функция короткоживующая.

В моём примере другая ситуация — объект возвращает анонимную функцию, но сам он уже не нужен. Логично было бы предположить, что его при следующей сборке прибъёт сборщик мусора (который я принудительно вызываю функцией gc_collect_cycles), но нет. Ссылка на него содержится в анонимной функции, порождённой методом returnFunc, ПХП интерпретатор и понятия не имеет нужен ли будет внутри контекст объекта, поэтому он его захватывает внутри на всякий случай.

Во втором подпримере конструкция «static function» говорит интерпретатору, что в ссылке на объект мы не нуждаемся, потому вызов gc_collect_cycles убивает два объекта — как предыдущего подпримера (так как переменная $holder уничтожена), так и из текущего, так как такая анонимная функция не содержит ссылку на объект.
2 комментария
9 августа 2016 10:12

💢 OCI8: проблемы с переходом на PHP7

Ещё в копилку проблем с ПХП7: если при привязке переменных в Оракле (bind) поля типа LONG/LOB не находятся в конце списка, то вы получите ошибку ORA-24816. Все столбцы таких типов должны быть привязаны последними, в общей куче. Мы пока столкнулись с проблемой только при сохранении (тестирование пока идёт), в итоге в сохраняющем методе модели пересортировали привязки, основываясь на описании модели.
13 комментариев
25 июля 2016 09:34

💢 Проблема с переходом на PHP7: Memcached, часть вторая

В прошлый раз я писал о проблемном Мемкешд в ПХП7 и оказалось, что я не совсем прав. Проблема есть, но её корень я понимал неверно.

Я-то думал, что получение токена cas просто сломали при переезде на следующую версию ПХП, а оказалось это особенность — в ПХП7 используется третья версия модуля, а ней токен получается иначе — надо передать специальный параметр и после вызова токен будет в результирующем массиве.

Черновым кодом это выглядит примерно так (должно работать, но я его не запускал):
if (version_compare(phpversion('memcached'), '3.0.0-dev', '<')) {
	// работаем по-старому
	return $memcached;
} else {
	// возвращаем обёртку
	return new class($memcached) {
	    use \Core\ProxyTrait;

	    public function __construct($mc)
	    {
	        $this->setObject($mc);
	    }

	    public function get($key, callable $cache_cb = null, &$cas_token = null)
	    {
	        $result = $this->obj->get($key, $cache_cb, Memcached::GET_EXTENDED);

	        if ($result === Memcached::GET_ERROR_RETURN_VALUE) {
	            return false;
	        }

	        if ($result) {
	            $cas_token = $result['cas'];
	            return $result['value'];
	        }

	        return $result;
	    }

	    public function getMulti(array $keys, array &$cas_tokens = null, int $flag = null)
	    {
	        $result = $this->obj->getMulti($keys, Memcached::GET_EXTENDED | $flag);

	        if ($result === Memcached::GET_ERROR_RETURN_VALUE) {
	            return false;
	        }

	        if ($result) {
	            $values = [];
	            $cas_tokens = [];

	            foreach ($result as $key => $d) {
	                $values[$key] = $d['value'];
	                $cas_tokens[$key] = $d['cas'];
	            }

	            return $values;
	        }

	        return $result;
	    }
	}
}
Вся мякотка в последнем параметре Memcached::GET_EXTENDED, он заставляет возвращать соответствующие методы не искомое значение, а массив, содержащий в том числе и cas.

Неприятно, что в этой версии ПХП модуль для работы с Мемкешд помечен как «разработческий». В этом свете мне как-то неясна позиция тех, кто уверенно советует использовать ПХП7 в продакшне.
2 комментария
19 июля 2016 12:01

💢 Проблема с переходом на PHP7: Memcached

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

Поэтому этот шаг, к ПХП7 очень труден для больших проектов, вроде тех, который разрабатываем мы. Тем не менее, мы медленно, но верно движемся в финишу. Вчера столкнулись с неожиданной сложностью, на которую мне пришлось потратить вечер.

Оказалось, что в модуле Мемкешд для ПХП7 нет реализации получения токена cas в методах get и getMulti (наверняка нет ещё в каких-то), но мы их не используем. Это видно, например, по прототипу:
Method [ <internal:memcached> public method get ] {

  - Parameters [2] {
    Parameter #0 [ <required> $key ]
    Parameter #1 [ <optional> $cache_cb ]
  }
}
Как видите, параметра cas нет вообще (он должен быть последним). Это печальное обстоятельство подтолкнуло меня к исследованию и к ночи я сделал решение. Возможно кому-то пригодится:
class MemcachedPHP7
{
    use \Core\ProxyTrait;

    public function __construct($mc)
    {
        $this->setObject($mc);
    }

    public function get($key, callable $cache_cb = null, &$cas_token = null)
    {
        switch (func_num_args()) {
            case 1:
                return $this->obj->get($key);
            case 2:
                return $this->obj->get($key, $cache_cb);
            default:
                if ($this->obj->getDelayed([$key], true) === false) {
                    return false;
                }

                $res = $this->obj->fetchAll();

                if ($res === false || !$res) {
                    if ($cache_cb !== null) {
                        if ($cache_cb($this->obj, $key, $value)) {
                            $this->obj->set($key, $value);
                        }
                    } else {
                        $value = false;
                    }
                } else {
                    $cas_token = $res[0]['cas'];
                    $value = $res[0]['value'];
                }

                return $value;
        }
    }

    public function getMulti(array $keys, array &$cas_tokens = null)
    {
        if (func_num_args() === 1) {
            return $this->obj->getMulti($key);
        } else {
            if ($this->obj->getDelayed($keys, true) === false) {
                return false;
            }
            $res = $this->obj->fetchAll();

            if ($res === false) {
                return false;
            }

            $cas_tokens = [];
            $values = [];

            $results = array_column($res, null, 'key');

            foreach ($keys as $key) {
                $cas_tokens[$key] = $results[$key]['cas'];
                $values[$key] = $results[$key]['value'];
            }

            return $values;
        }
    }
}
Трейт ProxyTrait я тут не привожу, там идея простая — он тупо проксирует всё, что получает через магические методы __get, __set, __call и прочие, setObject — метод этого трейта. Очень удобно, если надо оставить всё как есть, за исключением каких-то методов.

В остальном всё основано на том, что в методе getDelayed реализация токена cas есть, его я и использую, чтобы заткнуть эту дыру в функциональности. Работает всё так же как в ПХП 5.6, за исключением того, что в методе getMulti нет реализации последнего параметра — флага, вместо этого всё работает так, как будто он установлен, это ничему не мешает.
8 комментариев
14 июля 2016 11:00

🐘 PostgreSQL, PHP и подготовленные запросы

Одно из отличий «Постгреса» от «Оракла» — подготовленные планы запросов лежат не в разделяемом ресурсе, а в неком хранилище в рамках одного соединения. Программист, в рамках каждого соединения, должен подготовить их оператором PREPARE, в который передаётся выбранный идентификатор запроса и сам запрос, а потом выполнить его при помощи оператора EXECUTE, используя в качестве параметра переданный при подготовке идентификатор и значения параметров.

Польза подготовленных запросов в том, что можно единожды (за время жизни соединения, напоминаю) подготовить запрос, то есть попросить «Постгрес» выполнить его компиляцию в стандартное представление и оптимизацию, а после многократно его использовать, что очевидным образом поднимает производительность приложения.

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

Если план запроса больше не нужен, его можно удалить по идентификатору оператором DEALLOCATE, если же в качестве параметра передать ключевое слово ALL, то будут уничтожены все подготовленные в этом соединении планы. С закрытием соединения, планы так же исчезают.

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

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

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

Код всего описанного выглядит вот так, я его упростил и переписал процедурно:
// открываем постоянное соединение с базой
$con = pg_pconnect($connection_string);

// загружаем подготовленные запросы
$res = pg_query("SELECT name FROM pg_prepared_statements");
$prepared = array_fill_keys(pg_fetch_all_columns($res), true);
pg_free_result($res);

// хеш от запроса, tiger — быстрый алгоритм, я люблю его использовать
$hash = base64_encode(hash('tiger160,3', $sql, true));

// смотрим — был ли уже подготовлен такой запрос
if (!isset($prepared[$hash])) {
	$prepared[$hash] = true;
	pg_prepare($con, $hash, $sql);
}

// выполняем запрос
$res = pg_execute($con, $hash, $params);
Общая идея, думаю, понятна из кода. Пока для меня остаётся открытым вопрос «просачиваются» ли в переданное соединение незакрытые транзакции или временные таблицы (предыдущий его владелец, например, мог умереть из-за нехватки памяти), эксперименты я ещё не проводил, но, думаю, да.

Надо заметить, что модуль «Постгреса» в ПХП в каждое соединение, перед его передачей в приложение, посылает команду RESET ALL, но она, насколько я понял из руководства, лишь сбрасывает различные конфигурационные переметры времени выполнения на значения по-умолчанию и не трогает транзакции и всё остальное (более того, она сама транзакционна).
7 комментариев
29 февраля 2016 21:29

🐞 Немного об отладке через var_dump

Отладка на vk (144.06КиБ)
Кто-то из разработчиков отлаживает «Вконтакт» через var_dump прямо на «бою»

Недавно поймал, посещая «Вконтакт», такую ситуацию. Владельцы её заметили нескоро — висело ещё минимум час.

«Вконтакте» написан на диалекте ПХП и для тех не знаком с этим языком поясню — это какой-то смелый программист, отлаживая что-то прямо на промышленной среде, вывел какой-то ассоциативный массив прямо в код страницы встроенной в язык функцией var_dump. Кстати, довольно распространённый способ в рядах «старой школы».
23 комментария
7 февраля 2016 13:13

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 сначала сортирует их по длине.

Как видите, эти функции совершенно разные по своему действию и сравнивать по производительности их просто некорректно.
3 комментария
4 января 2016 16:00

Void в PHP

ПХП7 только-только вышел, а авторы языка уже приступили к следующим версиям. Если 7.0.1 будет лишь работой над ошибками, в 7.1 язык продолжит своё совершенствование. Первая ласточка — реализовано указание на отсутствие возвращаемого значения у функции (void).

Я сначала не понял зачем вводить в язык новое ключевое слово, можно было бы использовать для такого указания уже существующее ключевое слово 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 не различались, сейчас они будут иметь разный смысл.
4 комментария
8 декабря 2015 09:13

Отрицательное количество элементов (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, который выйдет сегодня, но его можно переписать и на «пятёрку» — надо только дать имя классу.
2 комментария
3 декабря 2015 10:12