WordPress как на ладони
WordPress Meetup #4. Встречаемся в Москве wordpress jino

13 неожиданностей в PHP, о которых знают не все

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

Ниже поговорим про интересные и неожиданные случаи в PHP.

explode() в 2 раза быстрее unserialize()

Если для сохраняемых чисел не важен тип (число), лучше хранить числа через запятую, чем их же сериализовать.

$str = 'a:7:{i:0;i:654;i:1;i:654;i:2;i:654;i:3;i:654;i:4;i:654;i:5;i:654;i:6;i:654;}';
$str2 = '654,654,654,654,654,654,654';
for( $i=1; $i<500000; $i++ ){
	unserialize($str);                         //> 0.346 сек.
	explode(',', $str2);                       //> 0.151 сек.
	array_map('intval', explode(',', $str2) ); //> 0.424 сек.
}

isset() в 2 раза быстрее in_array()

Скорости очень быстрые, но если обрабатываются большие массивы, то есть смысл заюзать array_flip() и искать значение через isset():

$arr  = array( 5, 6, 7, 8, 9, 10, 11, 12, 13 );
$arr2 = array_flip( $arr ); //  [5] => 0 [6] => 1 [7] => 2 [8] => 3 [9] => 4 [10] => 5 [11] => 6 [12] => 7 [13] => 8

for( $i = 1; $i < 500000; $i++ ){
	in_array( 5, $arr ); //> 0.03150 сек.

	isset( $arr2[5] );   //> 0.01552 сек.
}

Точное сравнение

PHP язык без строгой типизации и потому иногда могут возникать неожиданные результаты при сравнении (проверке) разных значений...

if( 0 == 'строка' ) echo 'Неужели?';
// Условие сработает и мы увидим: 'Неужели?'

// другими словами - строка превращается в число при сравнении и становится 0:
var_dump( 0 == 'строка' );   //> bool(true)

// но
var_dump( '0' == 'строка' ); //> bool(false)

Происходит так очевидно, потому что 'строка' превращается в ноль: intval( 'строка' ) = 0, а 0 == 0 это true, разумеется...

Так например можно пропустить переменную запроса:

// $_GET['foo'] может быть любой строкой и проверка всегда будет срабатывать...
if( $_GET['foo'] == 0 ){
	echo $_GET['foo'];
}

// поэтому по возможности ставьте проверку строго по типу
if( $_GET['foo'] === '0' ){
	echo $_GET['foo'];
}

Все следующие значения одинаковы, при сравнении через == (не строгий оператор сравнения):

0 == false == "" == "0" == null == array()

Ну и так:

1 == '1нечто' == true
true == array(111)

in_array() нас обманывает

Вы мастер массивов в PHP. Вы уже знаете все о создании, редактировании и удалении массивов. Тем не менее, следующий пример может вас удивить.

Часто при работаете с массивами приходится в них что-либо искать с помощью in_array().

$array = array( false, true, 1 );

if( in_array( 'строка', $array ) ){
	echo 'Неужто нашлось';
}

Как думаете выведет этот пример надпись «Неужто нашлось»? После такого вопроса, вы наверняка решили что условие сработает, но при написании кода, скорее всего было бы наоборот - и вы бы решили что условие не сработает smile На самом деле, это условие сработает и код выведет надпись «Неужто нашлось».

Так происходит, потому что PHP язык бестиповой и in_array() в данном случае сравнивает значения, но не учитывает тип, т.е. использует оператор ==, а не ===. А 'строка' == true даст нам true. Вот и получается что in_array() лжёт!

Решение

Чтобы избежать такого «обмана», нужно указать true в третий параметр в in_array(), так все сравнения будут проходить с учетом типа значения.

$array = array( false, true, 1 );

if( in_array( 'строка', $array, true ) )
	echo 'Неужто нашлось';
else
	echo 'Не найдено'; // сработает этот вариант условия

Разница между PHP операторами OR и ||, AND и &&

PHP операторы OR, AND и ||, && соответственно, отличаются приоритетами выполнения. У последних он выше, поэтому они будут выполняться раньше.

Если сравнивать с оператором присваивания: =, то OR/AND будут выполняться ПОСЛЕ оператора присваивания, в то время как у || и && будут выполняться ДО оператора присваивания, из за более высокого приоритета. Рассмотрим эту разницу на примере:

OR и ||

$true  = true;  // присваиваем
$false = false; // присваиваем

$var = $false OR $true;
// результат: $var = false
// потому что присваивание сработает раньше чем сравнение OR
// действует как: ( ($var = $false) or $true )

$var2 = $false || $true;
// результат: $var2 = true
// так как первым произошло сравнение, а уже потом присваивание

var_dump( $var, $var2 ); // bool(false), bool(true)

AND и &&

// "&&" имеет больший приоритет, чем "and"

$var = true && false;
// результат выражения (true && false) присваивается переменной $g
// работает как: ($var = (true && false))

$var2 = true and false;
// константа true присваивается $var2, а затем значение false игнорируется
// работает как: (($var2 = true) and false)

var_dump( $var, $var2 ); //> bool(false), bool(true)

Полезная ссылка по этой теме: Приоритет оператора

Шунтирующие операторы (короткая запись)

При сравнении типа AND (&&), если первое условие вернет false/0/''/array(), то нет смысла проверять следующие условия, потому что всё условие выполнится только если сразу все вложенные условия вернут что-либо отличное от empty (не false)...

При сравнении типа OR (||), если хоть одно условие вернет true или что-то отличное от empty, то нет смысла проверять следующие вложенные условия, потому что все условие выполняется когда хоть одно под-условие возвращает не false.

// foo() никогда не буде вызвана
// так как эти операторы являются шунтирующими (short-circuit)

$a = false && foo();
$b = ( false and foo() );
$c = true || foo();
$d = ( true  or  foo() );

count() не всегда дает ожидаемый результат

var_dump( count(false) );   //> int(1)
var_dump( count(0) );       //> int(1)
var_dump( count('') );      //> int(1)
var_dump( count(array()) ); //> int(0)

// тоже самое с sizeof()
var_dump( sizeof(false) );   //> int(1)
var_dump( sizeof(0) );       //> int(1)
var_dump( sizeof('') );      //> int(1)
var_dump( sizeof(array()) ); //> int(0)

isset() и null

Все мы привыкли проверять наличие значения в массиве через isset(). Однако если элемент в массиве есть, но его значение null, то isset() вернет false, как если бы элемента в массиве не было.

Наличие элемента со значением null можно проверить функцией array_key_exists().

$array = array('first' => null, 'second' => 4);

isset( $array['first'] ); //> false

array_key_exists( 'first', $array ); //> true

Странное поведение в PHP при передаче значения foreach по ссылке

$a = array('a', 'b', 'c');

foreach( $a as & $v ){ }
foreach( $a as   $v ){ }

print_r( $a );

/*
Array
(
	[0] => a
	[1] => b
	[2] => b
)
*/

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

Чтобы не ловить такие баги, при передаче по ссылке значения в foreach, указывайте уникальное значение переменной $val или очищайте $val после foreach с помощью unset($val).

foreach( $a as & $v ){}
unset($v);

Почему так происходит отлично объясняется тут.

empty() и объекты

Проверка empty() на объектах может вести себя странно. Допустим у нас есть некий объект $obj и мы проверяем пусто ли свойство var, и получаем такое:

if( empty( $obj->var ) ){
	// условие сработает
}

if( ! $obj->var ){
	// условие не сработает
}

Парадокс! Как такое может быть? empty() говорит что свойство пустое, а ! говорит что в нем что-то есть. Как одно и тоже свойство может быть пустым и не пустым одновременно? Квантовая суперпозиция господа...

Однако если разобраться, то нет тут ничего удивительного и все логично!

Дело в том, что конструкция empty() обращается к встроенному методу объекта __isset(), а прямой запрос свойства ($obj->var) обратиться к __get().

Т.е. получается empty() и ! запрашивают разные методы, если свойство не установлено:

class FOO {

	function __get( $name ){
		if( $name == 'bar' ) return true;
	}

}

$obj = new FOO;

var_dump( empty($obj->bar) ); //> bool(true) - переменной нет

var_dump( ! $obj->bar );      //> bool(false) - переменная есть

А теперь, зададим значение свойства bar в __isset() и empty() его получит:

class FOO {

	function __isset( $name ){
		if( $name == 'bar' ) return true;        
	}

	function __get( $name ){
		if( $name == 'bar' ) return true;
	}

}

$obj = new FOO;

var_dump( empty($obj->bar) ); //> bool(false) - переменная есть

var_dump( ! $obj->bar );      //> bool(false) - переменная есть

Увеличитель числа ++

Имеет большое значение в каком положении использовать ++ (инкремент, увеличитель).

++$i - увеличивает $i на 1, сразу - при текущем вызове $i.
$i++ - увеличит $i на 1, при следующем вызове $i.

$i = 0;

echo $i++; //> 0 - число увеличится при следующем вызове
echo $i;   //> 1 - увеличилось
echo ++$i; //> 2 - число увеличивается сразу

// сейчас $i = 2

// увеличивать можно внутри условий, индексов массивов - где угодно
if( $i++ == 2 ) echo $i; //> 3
$array[ ++$i ]; //> просим элемент массива с индексом 4

// однако нужно учитывать положение множителя - до или после переменной
// в обоих случаях проверяемое число будет разное...

// сейчас $i = 4

$array = array( 5 => 'foo' );
$array[ $i++ ]; //> ошибка - индекса нет, потому что мы просим 4

-- - уменьшитель (декремент) работает точно также...

Повторим еще раз:

Пример Название Действие
++$a инкремент до Увеличивает $a на 1, затем возвращает значение $a.
$a++ инкремент после Возвращает значение $a, затем увеличивает $a на 1.
--$a декремент до Уменьшает $a на 1, затем возвращает значение $a.
$a-- декремент после Возвращает значение $a, затем уменьшает $a на 1.

Увеличение строки ++

С числами все довольно просто, но что будет если инкрементить строки?

Что выведет данный код?

$a = 'fact_2';
echo ++$a;                                   //> fact_3

$a = '2nd_fact';
echo ++$a;                                   //> 2nd_facu

$a = 'a_fact';
echo ++$a;                                   //> a_facu

$a = 'a_fact?';
echo ++$a;                                   //> a_fact?

$a = 'Привет';
echo ++$a;                                   //> Привет

При инкременте строки, PHP увеличивает последний символ на символ следующий по алфавиту. И если в конце 2, то следующий символ будет 3. После t следует u. Однако эта операция не имеет никакого смысла в случае, когда строка заканчивается на не буквенно-численный символ.

Этот момент хорошо описан в официальной документации по операциям инкремента/декремента, однако многие не читали этот материал, потому что не ожидали встретить там ничего особенного.

Неточности с плавающей точкой

Посчитайте эту арифметику и скажите результат:

echo intval( (0.1 + 0.7) * 10 );

Сколько получилось, 8? А у компьютера 7!

Так происходит, потому что компьютеры не умеют хорошо работать с неточными числами - это как выясняется большая и старая проблема, есть даже статья на эту тему: «Что каждый компьютерщик должен знать об операциях с плавающей точкой».

Что получается в итоге и где ошибка?

0.1 + 0.7 = 0.79999999999

0.79999999999 * 10 = 7.9999999999

intval( 7.9 ) = 7 а не 8. Когда значение приводится к int, PHP обрезает дробную часть.

Однако, если посчитать так, то увидим 0.8, а не 0.79999999999. Хотя этот результат является лишь округлением:

echo 0.1 + 0.7; //> 0.8

Какой вывод можно сделать из этого примера? Будьте очень осторожны, когда дело касается дробных чисел (чисел с плавающей точкой) и никогда им не доверяйте.

Для складываний и вычитаний флоат чисел в PHP есть спец. фукцнии: bcadd(), bcsub(). Например:

echo bcadd(0.1, 0.7, 1); // 0.8
echo intval( bcadd(0.1, 0.7, 1)  * 10 ); // 8

Все объекты в PHP передаются по ссылке

Это свойство объектов вам возможно известно, но даже в этом случае можно поймать баг, поэтому при работе с объектами будьте внимательны.

Для примера, давайте рассмотрим такой код, когда мы используем объект для удобства, который создается из массива:

$data = (object) array(
	'my_val' => 'bar',
);

$var = $data;
$var->my_val = 'new_bar';

$var2 = $data;
echo $var2->my_val; //> new_bar

Мы создали казалось бы общие данные, который потом планируем использовать в разных переменных, меняя данные объекта где нужно, а где не нужно оставляя исходный вариант. В этом случае изменение данных будет менять их и в исходном варианте...

Так, например, если использовать массив, то все будет работать как мы ожидаем. Тот же код но с массивом:

$data = array(
	'my_val' => 'bar',
);

$var = $data;
$var['my_val'] = 'new_bar';

$var2 = $data;
echo $var2['my_val']; //> bar

Поэтому чтобы первый вариант работал правильно, в переменную нужно передавать копию объекта, создаваемую с помощью clone:

$data = (object) array(
	'foo' => 'bar',
);

$var = clone $data;
$var->foo = 'new_bar';

$var2 = clone $data;
echo $var2->foo; //> bar

-

Использовал при написании:

5 комментов
Здравствуйте, !     Войти . Зарегистрироваться