WordPress как на ладони
Очень Удобный и Быстрый Хостинг для сайтов на WordPress. Пользуюсь сам и вам рекомендую!

30 неожиданностей в PHP

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

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

Оглавление:

Точное сравнение: 0 == 'строка'

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 = [ false, true, 1 ];

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

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

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

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

$array = [ false, true, 1 ];

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

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

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

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

OR и ||

$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)

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

Шунтирующие операторы (short-circuit)

Docs: https://www.php.net/manual/ru/language.operators.logical.php

Шунтирующие операторы - это просто техника (трюк), в которой используйются логические операторы && и ||. , для того, чтобы сократить последовательную проверку boolean выражений (значений).

Например:

if( $foo && some_check() ){
	// do staff
}

Тут && это шунтирующий оператор: делается первая проверка и если она true, то делается вторая, если false, то до второй првоерки дело не доходит и код работает в обход второй проверки (шунтируется).

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

Код выше можно записать так (но это длинно и пожалуй менее читаемо):

if( $foo ){
	if( some_check() ){
		// do staff
	}
}

Еще примеры где удобно использовать шунтирующие операторы:

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

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

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

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

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
$array = [ 'a', 'b', 'c' ];

foreach( $array as & $item ){ }
foreach( $array as   $item ){ }

print_r( $array );

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

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

Что же произошло? Собственно, ничего такого, чтобы мы сами не просили сделать PHP. В первом цикле мы объявили ссылку & $item, которая после завершения работы цикла указывает на элемент массива $array[2]. Далее мы пробегаемся ещё раз по массиву, на каждом шаге присваивая переменной $item очередное значение. Т.к. в PHP область видимости переменных не ограничивается блоком составного оператора, то переменная $item во втором цикле - это та же самая переменная из первого цикла. Поэтому, одновременно с установкой значения переменной $item, это же значение присваивается и элементу $array[2].

  • Шаг 0: $item = $array[2] = $array[0] = a
  • Шаг 1: $item = $array[2] = $array[1] = b
  • Шаг 2: $item = $array[2] = $array[2] = b

Полное объяснение смотрите тут.

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

foreach( $array as & $val ){
	// операции с $val
}
unset( $val );

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 = 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, то эта 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

А вот пример сериализации дробного значения:

$str = serialize( 0.43 ); //> d:0.429999999999999993338661852249060757458209991455078125;

echo unserialize( $str ); //> 0.43

Решить проблему с сериализацией можно так ini_set( 'serialize_precision', -1 );. Подробнее тут.

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

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

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

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

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

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

echo $data->my_val; //> new_bar

Проблема тут в том, что когда мы делаем присваивание $var = $data; в переменную $var записывается ID объекта (указатель на объект), а не сам объект. Далее, если обратить внимание мы переходим внутрь объекта с помощью -> - $var->my_val = 'new_bar'; и меняем "внутренности" объекта, т.е. мы работаем уже с самим объектом, а не с переменной где лежит указатель на этот объект. Поэтому неважно какую переменную мы используем, в итоге мы всегда работаем с самим объектом.

В этом примере у нас есть две переменные ($data и $var), в которые записано где в памяти лежит этот объект.

Для примера, сделаем тоже самое с массивом - все будет работать по-другому:

$data = [
	'my_val' => 'bar',
];

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

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

Чтобы первый пример (с объектом) работал также как пример с массивом. Нам нужно создать копию объекта (клонировать его) и записать его ID в переменную. Используетм для этого clone:

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

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

echo $data->my_val; //> bar

Обратите внимание! Все это не дает нам права думать, что объекты передаются по ссылке! Ссылки в PHP работают иначе, примеры читайте ниже.

Не редко слышу фразу "Объекты в PHP передаются по ссылке" - технически это не так - в PHP есть ссылки (&) и работают они по-другому! Впрочем, с практической точки зрения такое упрощение не приводит к проблемам в общении с другими разработчиками.

Давайте посмотрим на примере. Чтобы понимать, что такое передача по ссылке, посмотрим как ведут себя переменные, которые передаются по ссылке:

function func( & $input ){
	$input = 'bar';
}

$var = 'foo';

func( $var );

var_dump( $var ); // string(3) "bar"

Тут переменная $var передается в функцию по ссылке и при изменении переменной $input внутри функции, она также меняет и значение $var. Так происходит потому что мы передаем в функцию ссылку на переменную $var (точнее, функция принимает параметр, который является ссылкой на переданную переменную). В этом случае можно уверенно говорить, что переменная передается по ссылке, а не по значению. Так работают ссылки в PHP.

Теперь давайте сделаем тоже самое для объекта, только не укажем & и посмотрим будет ли код вести себя также без указания ссылки, но при передаче объекта:

function func( $input ){
	$input = 'bar';
}

$var = new stdClass();
$var->foo = 'Hello';

func( $var );

print_r( $var ); // stdClass Object( [foo] => Hello )

Как мы видим изменение внутри функции не изменили переменную $var - она как содержала объект так его и содержит. Если бы объект передавался по ссылке, то при изменении $input, значение $var также должно было измениться, но этого не произошло. Поэтому, технически, фраза "Объекты в PHP передаются по ссылке" не верна!

Но с другой стороны, такой код меняет сам объект:

function func( $input ){
	$input->foo .= ' World!';
}

$var = new stdClass();
$var->foo = 'Hello';

func( $var );

print_r( $var ); // stdClass Object( [foo] => Hello World! )

В чем же дело? Упрощённо, механику можно представить так: когда мы создаём объект с помощью оператора new и присваиваем какой-то переменной, в эту переменную помещается не сам объект, а некий идентификатор объекта, id.

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

Таким образом снаружи функции и внутри мы, имея одинаковое значение идентификатора объекта, работаем с одним и тем же объектом.

Но если внутри функции мы присвоим переменной, например null – повлияет ли это на объект снаружи функции? Никак! Мы обнулили переменную содержащую id объекта внутри функции, но снаружи функции, внешняя переменная всё ещё содержит id объекта и сам объект никуда не делся из памяти.

Подробнее: https://5minphp.ru/episode83/

Сложение массивов

При сложении массивов элементы добавляемого массива не заменяют исходные, как это часто ожидается.

$arr1 = [ 'key1'=>'val_1', 'key2'=>'val_2' ];
$arr2 = [ 'key1'=>'val_3', 'key2'=>'val_4', 'key3'=>'val_5' ];

print_r( $arr1 + $arr2 );
/*
Array
	[key1] => val_1
	[key2] => val_2
	[key3] => val_5
*/
$arr1 = [ 'val_1', 'val_2' ];
$arr2 = [ 'val_3', 'val_4', 'val_5' ];

print_r( $arr1 + $arr2 );
/*
Array
	[0] => val_1
	[1] => val_2
	[2] => val_5
*/

Изменение типа данных в ключах массива

При создании индекса массива PHP автоматически преобразовывает тип данных. Это надо учитывать, при работе с ассоциативными массивами. Так например, если в индекс передать число в виде строки ('555'), то в индексе оно станет числом, или если в индекс передать true, то оно станет числом 1, а вот null превратиться в пустую строку. Пример кода см. ниже.

Выдержка из php.net — Массивы.

В массиве key может быть либо типа integer, либо типа string. value может быть любого типа.

Дополнительно с ключом key будут сделаны следующие преобразования:

  • Строки, содержащие целое число (исключая случаи, когда число предваряется знаком +) будут преобразованы к типу integer. Например, ключ со значением "8" будет в действительности сохранен со значением 8. С другой стороны, значение "08" не будет преобразовано, так как оно не является корректным десятичным целым.

  • Числа с плавающей точкой (тип float) также будут преобразованы к типу integer, то есть дробная часть будет отброшена. Например, ключ со значением 8.7 будет в действительности сохранен со значением 8.

  • Тип bool также преобразовываются к типу integer. Например, ключ со значением true будет сохранен со значением 1 и ключ со значением false будет сохранен со значением 0.

  • Тип null будет преобразован к пустой строке. Например, ключ со значением null будет в действительности сохранен со значением "".
    Массивы (тип array) и объекты (тип object) не могут использоваться в качестве ключей. При подобном использовании будет генерироваться предупреждение: Недопустимый тип смещения (Illegal offset type).

  • Если указано несколько элементов с одинаковым ключом, то только последний будет использоваться, а все другие будут перезаписаны.

Вот пример:

$arr = [
	'555'   => 'val-1',  // int(555)     (будет удален)
	555 .'' => 'val-2',  // int(555)
	'bar'   => 'val-3',  // "bar"
	false   => 'val-4',  // int(0)       (будет удален)
	true    => 'val-5',  // int(1)       (будет удален)
	null    => 'val-6',  // string(0) ""
	0       => 'val-7',  // int(0)
	1       => 'val-8',  // int(1)
	8.7     => 'val-9',  // int(8)
	'08'    => 'val-10', // string(2) "08"
			   'val-11', // int(556)
	'→'     => 'val-12', // "→"
];

var_dump( $arr );

/*
array(9) {
  [555]=>   string(5) "val-2"
  ["bar"]=> string(5) "val-3"
  [0]=>     string(5) "val-7"
  [1]=>     string(5) "val-8"
  [""]=>    string(5) "val-6"
  [8]=>     string(5) "val-9"
  ["08"]=>  string(6) "val-10"
  [556]=>   string(6) "val-11"
  ["→"]=>   string(6) "val-12"
}
*/

Closure::call — вызов анонимной функции с указанием контекста

Это не столько неожиданность, сколько интересная особенность, о которой мало кто знает.

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

Для этого в объекте замыкания есть метод:

call( $that, ...$params )
$that(object)
Объект для привязки к замыканию на время его вызова.
...$params
Сколько угодно параметров, которые передаются в замыкание.

Пример того как это использовать

class Value {

	protected $value;

	function __construct( $value ){
		$this->value = $value;
	}

	function get_value(){
		return $this->value;
	}
}

$three = new Value( 3 );
$four  = new Value( 4 );

$closure = function( $delta ){
	echo $this->get_value() + $delta;
};

$closure->call( $three, 4 ); // 7
$closure->call( $four,  4 ); // 8

Что мы видим? При вызове одного и того же замыкания мы получаем разный результат, который зависит от контекста вызова (от того какой объект передается и используется в замыкании).

Рекурсивная анонимная (лямбда) функция

Пример ниже показывает как можно создать рекурсивное замыкание. Другими словами как можно создать анонимную функцию и использовать её рекурсивно саму в себе.

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

// считаем факториал
$factorial__fn = static function( $n ) use ( & $factorial__fn ){
	if( $n == 1 ){
		return 1;
	}

	return $n * $factorial__fn( $n-1 );
};

echo $factorial__fn( 5 ); //= 120

Возврат по ссылке для функции или метода $var = & get()

С PHP 8.1.0 возврат по ссылке из функции void устарел, поскольку такая функция противоречива. Ранее в такой ситуации выдавалась ошибка уровня E_NOTICE: Только ссылки на переменные должны возвращаться по ссылке.

function & test(): void {
}

Передача по ссылке переменных это дело понятное, а вот ссылки для вызова функции/метода — не совсем.

Такая ссылка указывает на переменную, которую функция возвращает (т.е. она связывает возвращаемую переменную с переменной куда будет получен результат функции).

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

Рассмотрим на примере:

function & test( $file = null ){
	static $one = 1;
	static $two = 2;

	echo "$one, $two\n";

	return $two;
}

$two = & test();
$two = 222;
test();

/*
1, 2
1, 222
*/

Еще пример:

function & get(){
  static $data = null;

  return $data;
}

$var = & get();

var_dump( $var ); // NULL

$var = 'Привет мир';

echo get(); // Привет мир

Как видно из примера, мы получили значение статической переменной функции. А также установили связь (ссылку) на эту переменную. Теперь меняя внешнюю переменную $var мы меняем внутреннюю переменную функции.

Аналогичный пример с объектом:

class My_Class {
	public $value = 42;

	public function & get_value() {
		return $this->value;
	}
}

$class = new My_Class;

echo $class->value; // 42

$val = & $class->get_value();
$val = 2;

echo $class->value; // 2

Рассмотрим еще один пример, где наглядно видно отличие вызова по ссылке и без:

function & func(){
	static $static = 0;
	$static++;

	return $static;
}

$var1 = & func();

echo $var1; // 1
func();
func();
echo $var1; // 3

$var2 = func(); // вызов без &

echo $var2; // 4

func();
func();

echo $var1; // 6
echo $var2; // 4

Ссылку из функции можно передавать в другую функцию, которая ожидает ссылку:

function & collector() {
  static $collection = array();

  return $collection;
}

array_push( collector(), 'myval' );

print_r( collector() );
/*
Array (
	[0] => myval
)
*/

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

elseif, а не else if

// Лучше всего писать elseif а не else if
if( $a > $b ){
	echo '$a > $b';
} elseif( $a == $b ){
	echo '$a == $b';
}

// Так тоже можно, но не рекомендуется
if( $a > $b ){
	echo '$a > $b';
} else if( $a == $b ){
	echo '$a == $b';
}

// А вот такой вариант (без фигурных скобок и раздельного else if)
// вызовет ошибку синтаксиcа
if($a > $b):
	echo '$a > $b';
else if($a == $b): // Parse Error: syntax error, unexpected 'if' (T_IF)
	echo '$a == $b';
endif;

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 сек.
}

Статические методы примеси (trait) можно использовать напрямую

trait FOO {

	static function foo(){
		echo 'bar';
	}

}

FOO::foo(); // bar

Однако помните, что создавать трейты без их использования (например как хранилище статических методов) - это плохая практика.

Объявление типов: cтрогая типизация не так строга

Все что ниже касается кода в котором не используется declare( strict_types=1 );.

С этой декларацией, все типы должны быть в точности такими которые указаны! Есть лишь одно исключение — целое число (int) можно передать в функцию, которая ожидает значение типа float.

Объявление типов преобразует тип:

function input( bool $val ) {

	var_dump( $val ); // bool(true)
}

input( 1 );         // bool(true)
input( 'string' );  // bool(true)
input( '' );        // bool(false)
input( '0' );       // bool(false)
input( [] );        // Uncaught TypeError: Argument 1 passed to input() must be of the type bool, array given
function input( int $val ) {

	var_dump( $val ); // bool(true)
}

input( '1' );       // int(1)
input( '0' );       // int(0)
input( '2.7' );     // int(2)
input( 'string' );  // Uncaught TypeError: Argument 1 passed to input() must be of the type int, string given
input( '' );        // Uncaught TypeError: Argument 1 passed to input() must be of the type int, string given
input( [] );        // Uncaught TypeError: Argument 1 passed to input() must be of the type int, array given
function output( $return ): bool {

	return $return;
}

var_dump( output( 1 ) );        // bool(true)
var_dump( output( 'string' ) ); // bool(true)
var_dump( output( '' ) );       // bool(false)
var_dump( output( '0' ) );      // bool(false)
var_dump( output( [] ) );       // Uncaught TypeError: Return value of output() must be of the type bool, array returned

empty() включает в себя isset()

Ну очень часто вижу, как пишут:

isset( $vars[1] ) && !empty( $vars[1] )
// или
isset( $vars ) && !empty( $vars[1] )

Первая проверка лишняя! Здесь можно просто использовать !empty( $vars[1] ).

  • empty() - это аналог !isset($foo) || !$foo
  • !empty() - это аналог isset($foo) && $foo.

Т.е. empty делает то же самое, что и isset, плюс дополнительно проверяет наличие значения.

Т.е. empty - это то же самое, что !$foo, но не выдает warning, если переменная не существует.

В этом и заключается основной смысл этой функции: выполнять сравнение, не беспокоясь о том, что переменой может не быть.

Более того на isset проверяются все вложенности, например следующий код не выведет никаких warning'ов:

$foo = null;
var_dump( empty( $foo->bar->baz ) ); // true

$foo2 = 777;
var_dump( empty( $foo2->bar->baz ) ); // true

Вместо null тут может быть что угодно: массив, число, строка, объект. Т.е. empty просто подавляет любые варнинги в рантайме.

Но разумеется это не работает с ошибками парсинга (синтаксиса), потому что при ошибке синтаксиса до запуска скрипта дело вобще не доходит. Например:

empty( null->bar->baz ); // Parse error

В хорошем коде empty() сигнализирует, что переменной может не быть и это допустимо. А вот если переменная обязана быть, то проверка такой переменной с помощью empty (как это очень часто делают) просто подавит логическую ошибку кода если вдруг переменная будет переименована или удалена в процессе рефакторинга. И вы не увидите предупреждение заранее, а поймаете баг потом и будете искать причину вручную! Статический анализатор на такое ругаться скорее всего тоже не будет, потому что empty подразумевает полноте отсутствие переменной.

Тоже самое касается и isset()

$foo = null;
var_dump( isset( $foo->foo->bar ) ); // false

Не раздувайте код, пишите коротко и читаемо!

static для Closure

Почему важно указывать static при объявлении анонимных функций. Зачем делать callback’и в функции сортировки (usort), статическими?

Из документации:

При объявлении в контексте класса, текущий класс будет автоматически связан с Closure, делая $this доступным внутри Closure. Если вам НЕ нужно такое связывание с текущим классом, используйте статические анонимные функции.

Получается, что когда Сlosure объявляется в контексте класса, класс автоматически привязывается к замыканию и $this доступен внутри анонимной функции.

Пример, который показывает эту проблему:

class LargeObject {

	protected $array;

	public function __construct() {
		$this->array = array_fill( 0, 2000, 15 );
	}

	public function getItemProcessor(): Closure {
		return function () {
			return 1 + 2;
		};
	}
}

$processors = [];
for( $i = 0; $i < 2000; $i++ ){
	$processors[] = ( new LargeObject() )->getItemProcessor();
}

echo sprintf( '%.2f MiB', memory_get_peak_usage() / 1024 / 1024 ); // 138.86 MiB

Как мы видим процесс сожрал 138.86 MB памяти, по сути на ровном месте.

Так происходит, потому что в $processors[] мы продолжаем накапливать массив, внутри которого находятся связанные с объектом Closure, а значит, объект не может быть удален сборщиком мусора, пока не будет удалена переменная $processors.

Теперь давайте добавим static:

    return static function () {
		return 1 + 2;
	};

И теперь по памяти мы получим адекватные 5.86 MB, а не 138.86 MB.

Вывод

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

Как получить значения приватных свойств или методов

Иногда бывает нужно получить значение свойства класса видимость которого приватная (private/protected).

Обычно это делается через рефлексию new ReflectionClass().

Однако это также можно сделать и через замыкание в которое можно передать контекст:

class Test {

	protected $protect = 'Protected variable.';

	private $priv = 'Private variable.';

	protected function protected_func(){
		return 'protected function.';
	}

	private function private_func(){
		return 'Private function.';
	}
}

$Test = new Test();

print_r( [
	'protect'        => ( fn() => $this->protect )->call( $Test ),
	'priv'           => ( fn() => $this->priv )->call( $Test ),
	'protected_func' => ( fn() => $this->protected_func() )->call( $Test ),
	'private_func'   => ( fn() => $this->private_func() )->call( $Test ),
] );

Чем отличается print и echo

echo — Не возвращает значения после выполнения.
print — Возвращает 1.

var_dump( echo 'foo' ); // Parse Error: syntax error, unexpected 'echo'
var_dump( print 'bar' ); // int(1)

То есть print возвращает статус состояния произведенной операции, echo просто выводит текст.

Так print можно использовать в тернарных операторах:

( $var > 1 ) ? print 'yes' : print 'no';

goto конструкция

В PHP иногда удобно использовать goto конструкцию.

Например, нам нужно подключиться к БД, но с первого раза это может не получится, нужно пробовать несколько раз с задержкой. Это можно написать через while цикл или можно записать так:

CONNECTION_RETRY: {
	$max_retries = 5;
	$delay = 1;

	$retries = 0;

	try {
		$client->connect( ...$arguments );
	}
	catch ( Exception $exception ) {
		if ( ++$retries >= $max_retries ) {
			throw $exception;
		}

		usleep( $delay * 1000 );

		goto CONNECTION_RETRY;
	}
}

Сравнение DateTime и DateTimeImmutable

Работая с объектами DateTime или DateTimeImmutable их время иногда нужно сравнивать (больше/меньше/равно).

Обычно их сравнивают через временную метку UNIX, получая её с помощью метода getTimestamp(). Или используют метод diff(), который считает разницу между датами.

Однако, мало кто знает что, что объекты дат можно сравнивать используя обычные операторы сравнения >, < и ==:

$date1 = new DateTime("now");
$date2 = new DateTime("tomorrow");

var_dump( $date1 == $date2 );
var_dump( $date1 < $date2 );
var_dump( $date1 > $date2 );

Пример об этом есть в документации к методу DateTimeInterface::diff().

Такое сравнение возможно потому что PHP Под капотом использует библиотеку timelib Дерика Ретанса (автор Xdebug).

fn() => $var - стрелочные функции и внешние переменные

При написании юнит-тестов мне как-то понадобилось замокать работу кэша. Я решил положить значение кеша в локальную переменную функции теста.

Вот пример который демонстрирует как это работает:

$cache = '';

$get = function() use ( & $cache ){
	return $cache;
};

$set = function( $val ) use ( & $cache ){
	$cache = $val;
};

$set( 'hello' );

echo $get(); // hello

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

$cache = '';

$get = fn() => $cache;

$set = fn( $val ) => $cache = $val;

$set( 'hello' );

var_dump( $get() ); // string(0) ""

Почему так происходит? Обратимся к документации:

И анонимные, и стрелочные функции реализованы через класс Closure.

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

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

Стрелочные функции используют привязку переменных по значению. Это примерно эквивалентно выполнению use($x) для каждой переменной $x, используемой внутри стрелочной функции. Привязка по значению означает, что невозможно изменить какие-либо значения из внешней области.

По ссылкам можно только передавать и возвращать переменные:

fn( & $x ) => $x;
fn &( $x ) => $x;

Переназначение private свойства класса

Допустим, у нас есть абстрактный класс с приватным свойством и наследник с таким же приватным свойством $age:

abstract class Father {
	private int $age = 56;

	public function __construct(
		protected string $name
	) {
	}
}

final class Child extends Father {
	private int $age = 22;

	public function __construct(
		protected string $name,
	) {
		parent::__construct( $name );
	}
}

Приватные свойства обоих объектов будут отдельными, потому что они приватные! Так например, если теперь сделать var_dump(), мы увидми 3 свойства, а не 2:

print_r( new Child( 'Jhon' ) );
/*
Child Object (
	[age:Father:private] => 56
	[name:protected] => Alex
	[age:Child:private] => 22
)
*/

Чтобы получить все эти данные в PHP, например, чтобы потом закэшировать объект можно привести объект к массиву:

print_r( (array) new Child( 'Jhon' ) );
/*
Array
(
	[\0Father\0age] => 56
	[\0*\0name] => Jhon
	[\0Child\0age] => 22
)
*/

Где это может пригодится, см это видео https://www.youtube.com/watch?v=YvJXq9aJwpQ
Библиотеки из видео:

Интересные видео

--

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

8 комментариев
    Войти