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

php

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

Модуль PHP на Go

Потешную штуку показали на днях — php-go, позволяет писать модули для ПХП на Гоу. У меня как-то такой потребности не возникало, но попробовать-то интересно!

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

Ссылка давно протухла, но «Вебархив», к счастью, имеет копию страницы — статья называется «Ridiculous UTF-8 character counting».

Программа там на Си, я её минимально подпилил, чтобы она нормально компилировалась, завернул через cgo в Гоу, а оттуда при помощи php-go протащил как модуль ПХП.

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

Зато попробовал php-go, вещь предельно простая, в случае нужды можно будет что-то быстренько набросать с её использованием.

Функция sleep и сигналы

Век живи, век учись. Вот уж не знал, что при получении сигнала программой на ПХП, моментально прерывается выполнение функции sleep (и родственных ей time_nanosleep и usleep):

$ php -r 'pcntl_signal(SIGUSR1, "_"); var_dump(sleep(PHP_INT_MAX));' &
[1] 3177
$ kill -USR1 3177
$ int(4294967288)

Вообще в Си так же, но где ПХП и где Си. Как и в Си, назад возвращается количество секунд, которые функция не «доспала» (time_nanosleep возвращает массив из секунд и наносекунд, а usleep — NULL). Правда под Виндой есть особенность — sleep при «недосыпе» всегда возвращает 192 (значение константы WAIT_IO_COMPLETION).

Любопытное исключение из правила — time_sleep_until, она сигналы игнорирует. Так что если вам нужна гарантированная по времени задержка, используйте её или напишите обёртку:

function bulletproof_sleep(int $seconds): bool
{
    $period = ['seconds' => $seconds, 'nanoseconds' => 0];

    while (array_sum($period) > 0) {
        $period = time_nanosleep($period['seconds'], $period['nanoseconds']);

        if (is_bool($period)) {
            return $period;
        }
    }

    return true;
}

Heredoc и nowdoc в PHP

Не очень-то люблю использовать heredoc и nowdoc в ПХП из-за того, что они портят форматирование (а оно важно в больших проектах). Хотя вещь удобная — внутри можно использовать оба вида кавычек без экранирования, это бывает актуально в эскуэль-запросах.

В Перле давным-давно можно нормально смещать эту конструкцию вправо, она сама обрежет лишние пробелы, а теперь и в ПХП сделали то же — с версии 7.3 можно будет делать вот так:

$values = [<<<END
      a
     b
    c
    END];

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

Ранее Ctrl + ↓