Pull to refresh

Безумный PHP. Фьюри код

Reading time 7 min
Views 64K

Сборник PHP ненормальностей или что надо знать чтобы не сойти с ума и не прострелить себе что-нибудь


Прочитал статью mnv: "Приведение типов в PHP == табурет о двух ножках?" и захотелось в комментариях добавить немного дополнений, но… Но потом увидел комментарий и понял, что лучше дополню статью тем, про что мало кто пишет и мало где это имеется в централизованном виде. Вроде бы всем известная тема, а все же кому-то в новинку. Это не совсем про приведение типов, но они тоже есть. Это про особенности, зная которые можно делать меньше ошибок. Если интересно, го под кат, я создал!

PHP не плохой и не хороший. Он для своих задач и с ними он справляется. При этом этот язык программирования имеет много разных багофич. Можно на них жаловаться и негодовать. А можно просто про них знать и уметь их обходить или применять. Не не не, я не призываю так писать, я просто говорю, что если захочется, то…

Прежде чем судить, давайте договоримся!


Давайте рассматривать этот пост — как развлекательный. Т.е. это задачки не для собеседований и не для продакшена. Это просто примеры задач на олимпиаду, где можно получить звание "Я — интепретатор PHP!".

Думай как PHP… Чувствуй как PHP… Будь PHP!


Задачки взяты из нашего квеста, который мы делали на "День девелопера", отмечаемый в нашей компании. Наша коллега даже писала подробную статью про то, как в Tutu.ru чествуют труд айтишников в статье "Как отметить день программиста на работе и сделать всех довольными?". Так что повторюсь, это не для продакшена и не для собеседования. Это ради фана!

Про числа


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

Каков результат конкатенирования следующих строк? (да, так не пишут, но у нас же олимпиада):

<?php
{
    print "a" . 2;
    print "a".2;
}

Что будет?
В последней строке PHP попытается привести запись к числу с плавающей запятой. Но это приведет к ошибке.
PHP Parse error: syntax error, unexpected '.2' (T_DNUMBER) in Command line code on line 1

Поиск строки в массиве

Это вполне себе боевая ситуация. Такое может встретиться в коде некоторых CMS или реальных проектов.

<?php
 
$a = ['7.1'];
 
in_array('7.1', $a);
in_array('7.1abc', $a);
in_array('7.10', $a);
in_array('7.100000000000000009123', $a);

Вспоминаем про фишку PHP и получаем правильный ответ:
in_array('7.1', $a); // true
in_array('7.1abc', $a); // false
in_array('7.10', $a); // true
in_array('7.100000000000000009123', $a); // true

Что бы избежать ошибок, используйте 3й аргумент: указание типа сравнения.

bool in_array ( mixed $needle, array $haystack [, bool $strict = FALSE ] )

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


Как в PHP переопределить TRUE?


Вопрос на засыпку и ради академического интереса. Если очень хочется, то:
<?php
 
// Так сделать не получится в глобальной области
// PHP Notice:  Constant true already defined in ...
namespace {
   define('true', false);
}
 
// Но вот в неймспейсе - пожалуйста
namespace Hack {
    define('Hack\\true', false);
    var_dump(true === false); // true
}

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

Валидный ли скрипт?


<?php

$   = 1;
$   = 2;
$   = $  + $ ;

var_dump(   $   );

//EOF//

Да
Символ с десятичным индексом 160 входит в таблицу разрешенных символов для именования переменных в PHP. В Windows его набрать можно как Alt+0160.

Выполнить любой ценой


Это просто задачка на подумать. Опять же про приведение типов. Просто головоломка.

<?php

    if ( $x == false && $y == true && $x == $y )
    {
        echo "Yuo crazy developer!";
    }

Один из вариантов
$x = 0;
$y = 'x';

Безумная логика


А вот еще классный пример особенностей интепретатора. Давайте ответим на вопрос, что будет?

$a = 1;
var_dump( $a + $a++ );

А если усложним?

$a = 1;
var_dump( $a + $a + $a++ );

Если ответили на первые 2 вопроса, то тогда вам не составит труда интерпретировать вот эту задачку:

<?php

$a = 1;
$b = 1;

var_dump( ($a + $a + $a++) === ($b + $b++) );


Ответ в следующем абзаце.

Что-то пошло не так, да?
И в 1-м, и во 2-м случае ответ 3, а значит, 3-й пример выдаст true.

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

Похожим образом обрабатывается умножение вместе со сложением: 2 + 2 * 2 = 6, а не 8, потому что умножение имеет более высокий приоритет.

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

Больше деталей и картинка
В байт-кодах всё перечисленное выглядит следующим образом:

Тут с восьмой строки начинается второй пример (правда присваивание единицы во втором примере я опустил — как видите второй операции ASSIGN нет).

Источник


Но на деле не логично. Так что не ломайте голову, все работает правильно. Но не так, как вы ожидали. В других языках, к примеру, Javascript, ответ будет другой. Проверьте. 2й вариант ведет себя одинаково с тем же JS. Но вот 1й вызывет вопросы, если смотреть на результаты в разных языках программирования.

Слабо проитерировать строку?


Есть задача проитерировать строку:

<?php
 
$s = 'Iteration'; // <- можно модифицировать
 
for ($i = 0; $i < 10; $i++) {
    // echo ???, "\n";
}
 
//EOF//

На выходе нужен следующий результат:

Iteration 0
Iteration 1
Iteration 2
Iteration 3
Iteration 4
Iteration 5
Iteration 6
Iteration 7
Iteration 8
Iteration 9


Ваши варианты?
А вот вам мой вариант
<?php
 
$s = 'Iteration 0';
 
for ($i = 0; $i < 100; $i++) {
    echo $s++, "\n";
}
 
//EOF//

Если в конце стоит 1 цифра, то итерация будет повторяться от 0 до 9 при цикле. Если хотите получить от 0 до 99, то нужно уже поставить 2 числа: «Iteration 00». А что если хочется вывести алфавит? Тогда ставим в конце букву a, если хотим вывести все по порядку:

Iteration a
Iteration b
Iteration c
Iteration d
Iteration e
Iteration f
Iteration g
Iteration h
Iteration i
Iteration j
Iteration k
Iteration l
Iteration m
Iteration n
...

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

Значения по ссылке


Ссылки в PHP, штука вещь хорошая! Но есть нюансы. У нас нигде не определен массив $foo. Это должно вызывать ошибку…

<?php

function getCount(&$a)
{
	return count($a);
}

$cnt = getCount( $foo );
var_dump($cnt);


// и если сделать так

$cnt = getCount( $foo['bar'] );
var_dump($cnt);


Но все будет ок. Ошибка будет только в случае, если убрать амперсанд, тогда получим:
PHP Notice: Undefined variable: foo in /www/sites/majorov.su/***/a.php on line 8
int(0)
PHP Notice: Undefined variable: foo in /www/sites/majorov.su/***/a.php on line 14
int(0)

Несуществующий валидный код


Давайте пофантазируем. У вас мощный проект с 10 летней историей. И у вас появилась задача рефакторинга и реинжиниринга старого кода.

И вот вы правите код и сходите с ума. Почему? Ну вот потому, что, допустим, вы не понимаете почему у вас работает то, что не должно.

<?php
 
class Bar {
    public $foo = 1;
}
 
$Obj1 = new Bar( Foo.bar() ); // Foo нигде не определен! Он должен вызвать фатал
$Obj2 = new stdClass( getFoo( $Obj1 ) ); // getFoo() не существует, почему он работает?

Если нет конструктора, то код, переданный в конструктор не вызывает ошибок. Он парсится, но не выполняется, так как оптимизатор игнорирует эти строки. Это экономит нам время на обработку. Ведь чтобы показать ошибку, нужно знать какая ошибка (код, тип, номер строки...). А тут просто идет пропуск части кода, так как нет в нем смысла. Это классно, правда. Но вы должны знать про это, иначе вам взорвет это мозг.

ACL, MD5 и… Коллизия?


Представим что у вас есть CMS. И вот там есть что-то вроде такого (поверьте, много где такого кода (не именно такого, но работающего по такому принципу, можно найти в рунете).

<?php

// Допустим брутфорсим пользователя admin

// Получили пользовательский пароль QNKCDZO
$_POST = ['pass' => 'QNKCDZO'];
$userPass = md5($_POST['pass']);

// Есть пароль в базе такого вида 240610708
$actualPassInDb = md5('240610708');

$autorizied = false;

// сделали проверку
if ( $userPass == $actualPassInDb ) {
	// Авторизировали пользователя
	$autorizied = true;
}
else {
	/*header*/var_dump("location: /error/");
	die;
}

А что не так? Вроде все ок. Пароли же разные. Разве нет?
Давайте взглянем на md5() хеши паролей:
  • QNKCDZO, в MD5 = string(32) «0e830400451993494058024219903391»
  • 240610708 = string(32) «0e462097431906509019562988736854»

Поняли? PHP видит 0 и думает что это число. Точнее PHP видит 0e[0-9]+ и думает что это float число, которое приводится к 0. Подробнее описано по ссылке: https://blog.whitehatsec.com/magic-hashes/

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

Foreach и ссылки на ключи


На последок напомню, что, с ссылками надо быть аккуратнее.
Вот пример, как получить не то, что ожидали:

<?php

$array = ['foo', 'bar'];

foreach ($array as $k => &$foo){
	$foo .= $k;
}

var_dump($array);

foreach ($array as $foo) {
	var_dump($foo);
}


Что ожидаем? А что получаем?

А получаем вот что:

array(2) {
  [0]=>
  string(4) "foo0"
  [1]=>
  &string(4) "bar1" // важно, здесь указатель !
}
string(4) "foo0"
string(4) "foo0"

Из-за спрятавшегося указателя при следующей итерации мы получаем доступ к другому значению.

Собственно это весь мой комментарий к той статье, который я хотел добавить. Учите особенности языка. Учите их не ради того, чтобы валить на собеседованиях. Учите их, чтобы самому в такую яму не попасть. Не брезгуйте. Вы так не пишите, так другие так могут написать. А кто будет объяснять им почему не так и что не так? А как объяснить то, чего не знаешь?

Мир вам, девелоперы. Я писал на PHP 12 лет, и сейчас уже 3 года как во фронтенд разработке, схожу с ума с JavaScript, но это уже совсем другая история. Но иногда хочется потрогать это самый PHP.

Так же советую к прочтению статью от AlexLeonov «Готовимся к собеседованию по PHP: ключевое слово «static»», там есть интересные моменты, которые я не стал описывать.

P.S.: На картинке PHP MV 9. Пистолет PHP (Prvi Hrvatski Pistolj — первый хорватский пистолет) был в спешном порядке разработан в отделившейся от союзной Югославии Хорватии в начале девяностых годов 20 века, когда страна отчаянно нуждалась в оружии из-за возникшей на руинах СФРЮ войны. Пистолет, вобравший в себя черты таких известных и достаточно удачных образцов как Beretta 92 и Walther P38, вышел гораздо менее удачным и имел проблемы с надежностью. Выпуск его был довольно непродолжительным и позже он был заменен на вооружении Хорватской армии гораздо более удачным пистолетом HS 2000.

Пистолет PHP использует автоматику с коротким ходом ствола, запирание осуществляется при помощи расположенной ниже ствола качающейся личинки. Возвратная пружина расположена под стволом. Ударно-спусковой механизм курковый, двойного действия (самовзводный). Слева на рукоятке расположен рычаг безопасного спуска курка с боевого взвода. Магазин двухрядный, емкостью 15 патронов.

Будьте аккуратны, не прострелите себе чего-нибудь!

UPD: в комментариях юзер smart дал хорошие линки на документацию, для тех, кто хочет разобраться в деталях
Tags:
Hubs:
+73
Comments 74
Comments Comments 74

Articles