30 неожиданностей в PHP
Казалось бы, все знаешь, пишешь функции, используешь операторы и получаются у тебя крутые, быстрые и понятные конструкции кода. Но в какой-то момент случается нечто, что не поддается объяснению, когда код работает так, как нужно ему, а не как ожидаешь ты и все это, как-будто, вопреки здравому смыслу, как будто, происходят чудеса и какая-то магия. Но код ошибаться не может - ошибаешься ты, потому что либо что-то не учёл, либо чего-то не знаешь!
Ниже поговорим про особенности PHP: неожиданные, необычные, нестандартные, не очевидные, странные или особенные ситуации/случаи в PHP.
Читайте также: PHP Синтаксис версий с 5.3 до 7.3
Точное сравнение: 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
Странное поведение в PHP при передаче значения foreach по ссылке
$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
Что мы видим? При вызове одного и того же замыкания мы получаем разный результат, который зависит от контекста вызова (от того какой объект передается и используется в замыкании).
Рекурсивная анонимная (лямбда) функция
Build In PostПример ниже показывает как можно создать рекурсивное замыкание. Другими словами как можно создать анонимную функцию и использовать её рекурсивно саму в себе.
Все это может показаться бредом, но как ни странно такие функции мне иногда нужны из-за удобства. Поэтому хорошо бы знать, что такое можно делать в 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()
Передача по ссылке переменных это дело понятное, а вот ссылки для вызова функции/метода — не совсем.
Такая ссылка указывает на переменную, которую функция возвращает (т.е. она связывает возвращаемую переменную с переменной куда будет получен результат функции).
Это имеет смысл если функция имеет статические переменные, которые не будут удалены после того как функция отработает, и к которым нам нужно иметь доступ извне.
Рассмотрим на примере:
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 */
При использовании ссылку (&) обязательно нужно указывать, иначе функция будет работать как обычно, без ссылки на внутреннюю переменную.
$two = & test(); // ссылка работает $two = test(); // ссылка не работает
С PHP 8.1.0 возврат по ссылке из функции void устарел, поскольку такая функция противоречива. Ранее в такой ситуации выдавалась ошибка уровня E_NOTICE: Только ссылки на переменные должны возвращаться по ссылке.
function & test(): void { }
Еще пример:
function & get(){ static $data = null; return $data; } $var = & get(); var_dump( $var ); // NULL $var = 'Привет мир'; echo get(); // Привет мир
Как видно из примера, мы получили значение статической переменной функции. А также установили связь (ссылку) на эту статическую переменную. Теперь меняя внешнюю переменную $var мы меняем внутреннюю переменную функции.
ЗАМЕТКА: если убрать ключевое слово static, то ссылка перестанет иметь смысл, потому что после первого вызова функции, её внутренняя переменная будет очищена:
function & get(){ $data = null; return $data; } $var = & get(); var_dump( $var ); // NULL $var = 'Привет мир'; echo get(); // пустая строка (NULL)
Аналогичный пример с объектом:
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
Библиотеки из видео:
Цикл, который будет выполняться N раз - for( $i = 2;; )
Итересный подход чтобы выполнить цикл конкретное кол-во раз.
for( $i = 2;; ){ var_dump( $i ); if( --$i === 0 ){ break; } }
Выведет:
int(2) int(1)
Конструкция for( $i = 2;; )
, используемая в сочетании с --$i
внутри цикла, представляет собой способ ограничить количество итераций, которые совершит цикл. Несмотря на свою непривычность, это является валидным использованием конструкции цикла "for" в PHP.
В этом примере мы начинаем со значением $i = 2. Затем в цикле кадый раз уменьшаем значение на 1 --$i
.
Итак, при первом проходе $i = 2. На второй итерации $i = 1. Затем $i = 0 - мф попадаем в условие break.
Более понятно и читаемо код выше можно записать так:
$i = 2; while( $i > 0 ){ var_dump( $i ); $i--; }
NOTE: Честно говоря не знаю зачем нужно писать непонятно, когда можно написать понятнее, но прием мне показался интересным, поэтому он тут.
Интересные видео
--
Использовал при написании:
- Личные наблюдения
- http://php.net
- https://habrahabr.ru/post/226861/