Грабли PHP

PHP — интерпретируемый язык программирования, который вплоть до сих пор является практически стандартом для создания сайтов. И, несмотря на то, что в последние годы стали весьма популярны платформы Ruby on Rails и Django, предназначенные для веб-разработки на языках Ruby и Python, PHP всё ещё остаётся наиболее распространён.

Название PHP расшифровывается как “PHP Hypertext Preprocessor”, где “PHP”, в свою очередь, означает то же самое — и так до бесконечности. Это название в целом достаточно точно определяет способ работы PHP на сервере — конструкции на языке PHP могут быть встроены в код на HTML по аналогии с тем, как макросы встраиваются в исходный код на C/C++. Однако, гораздо более эффективно полностью разделять PHP и HTML, используя так называемые механизмы шаблонизации. Одним из известных механизмов шаблонизации является XSLT, позволяющий на основе данных, представленных в формате XML, получить какое-то определённое результирующее представление. Это может быть HTML-документ или обычный текстовый файл. Такое преобразование обычно осуществляется на стороне сервера, так как не все обозреватели поддерживают технологию XSLT.

Впрочем, эта страница посвящена PHP, а не механизмам шаблонизации. Я не буду подробно описывать язык PHP шаг за шагом, это замечательно делают в университете. Однако, я расскажу о некоторых особенностях PHP, о скрытых «граблях» и дам некоторые рекомендации.

Одним из основных понятий PHP, как и большинства других языков программирования, является идентификатор — имя некоторой сущности в теле программы. Дать определение идентификатора в PHP значительно труднее, чем для других языков, потому что идентификаторы в PHP могут содержать не только латинские буквы, цифры и знак подчёркивания, но и многие другие знаки. Например, допускаются национальные языки в кодировке UTF-8 и даже, к примеру, символы кавычек («») и многие другие. Таким образом, строка “привет_всем” является корректным идентификатором, “foo_бар_«あぁ»” — тоже. В идентификатор можно включить даже те кавычки, в которые я заключил эти примеры. А вот, к примеру, 1func не является корректным идентификатором, так как начинается со знака цифры. Хорошо это или плохо? На этот вопрос сложно дать однозначный ответ. Для многих это может быть хорошо тем, что можно использовать русские имена переменных и функций и легче разбираться в своём коде. С другой стороны, можно наоборот потерпеть неудачу, написав код с русскоязычными идентификаторами и передав его программисту, незнакомому с русским языком.

Переменные в языке PHP обозначаются идентификатором с предшествующим ему знаком доллара ($). При объявлении переменных нет необходимости явно указывать их тип, так как он определяется автоматически при присвоении переменной начального значения. Рассмотрим несколько примеров.

<?php
$foo = 'Hello, world!';
$bar = 0;
$baz = 4.26;
echo $foo;
?>

Как вы могли заметить, в этом примере мы объявили 3 переменных и вывели одну из них на экран (или в обозреватель — зависит от условий работы сценария). Операторы разделяются символом точки с запятой. Кстати, нужно отметить ещё одну интересную особенность PHP: грань между выражением и оператором в этом языке довольно размытая (в отличие от, например, Pascal). Например, строка вида “$bar + $baz”, очевидно, является выражением, однако, кроме здравого смысла абсолютно ничто не мешает использовать в коде такие конструкции в качестве операторов, добавив в конце символ точки с запятой. С другой стороны, и оператор присваивания является «не совсем» оператором: саму по себе операцию присваивания можно использовать в каком-либо другом операторе в качестве выражения. Рассмотрим пример.

$foo = false;
if($foo = true) {
 echo 'Foo is true!';
} else {
 echo 'Foo is false!';
}

Как вы думаете, что будет выведено в результате выполнения этого кода? Отвечу сразу: будет выведено “Foo is true”. А всё из-за того, что в этой конструкции встречаются очередные грабли: интуитивно кажется, что здесь имеет место операция сравнения переменной $foo с логическим значением true, что само по себе может вызвать желание просто написать “if($foo) …”. Но нет — в действительности здесь имеет место не сравнение неопределённой переменной $foo с константой true, а определение переменной $foo путём присваивания ей начального значения true. Это как раз тот случай, где операция присваивания выступает в роли выражения. Возникает логичный вопрос — а что же является значением такого выражения? Ответ на этот вопрос столь же очевиден — значением является результат, помещаемый в переменную в левой части равенства. Эта особенность даёт возможность разом инициализировать несколько переменных.

$foo = $bar = $baz = true;

Правда, к сожалению, в PHP не совсем удобно реализовано множественное присваивание. Например, в Python для обмена значениями 2 переменных достаточно выполнить оператор: “foo, bar = bar, foo”. В PHP же, если хочется обойтись без промежуточной переменной, придётся сделать так:

list($foo, $bar) = array($bar, $foo);

Можно также пользоваться тем свойством интерпретатора, что для логических операций конъюнкции (&&) и дизъюнкции (||) не производится вычисление правых частей, в случае, если вычисление левых даёт однозначный ответ.

($foo = @ fopen($filename = '/path/to/some/file', 'r')) || die('Failed to open file: ' . $filename);
($bar != 5) && $bar++;

В первом примере мы пытаемся открыть при помощи функции fopen некоторый файл, одновременно записывая имя этого файла в переменную $filename для какого-то последующего использования и, в случае, если функция fopen возвращает false (открытие не удалось), однозначное определение результата дизъюнкции невозможно и начинается вычисление правой части. В правой части стоит вызов функции die, которая завершает работу сценария, выводя сообщение об ошибке. Отметим, что символ “@” предназначен для скрытия генерируемого функцией сообщения об ошибке и может быть опущен. Во второй строке мы инкрементируем значение переменной $bar только если оно не равно 5. Эти примеры — хорошая альтернатива условному оператору и при достаточной привычке такой подход может ускорить написание кода.

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

$foo ? function1() : function2();

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

return ($_SERVER['REQUEST_URI'] == '/') ?
$this->action_default() : (preg_match('#^/([a-z_]+)(/(.+))?$#iU', $_SERVER['REQUEST_URI'], $m) ?
(method_exists($this, $a = 'action_' . $m[1]) ?
(isset($m[3]) ? call_user_func(array($this, $a), $m[3]) : call_user_func(array($this, $a))) :
header('Location: ' . ATS_WEBPATH)) : header('Location: ' . ATS_WEBPATH));

Конечно, это довольно сложный пример, но тем любопытнее будет проанализировать его и попытаться разбить на условные операторы :)

Но давайте вернёмся к граблям, ведь PHP помимо всего прочего известен именно как язык, в котором очень легко наступить на грабли. Очередные грабли поджидают нас на пути к использованию значений небулевских типов в качестве условий в различных операторах (например, в операторе цикла). PHP — язык с динамической типизацией, и многие типы данных имеют логические значения. Например, пустая строка или строка ’0′ воспринимаются как false, а остальные строки — как true. Аналогичная ситуация с числами — здесь 0 воспринимается как false, другие величины — как true. Впрочем, ноль тоже может неожиданно превратиться в true, если получен в ходе операций над числами с плавающей точкой.

В программировании на PHP очень часто используется такой подход, при котором условием цикла или условного оператора является выражение нелогического типа. Один из примеров, который наверняка будет рассмотрен в лекционном курсе, — взаимодействие с СУБД MySQL.

while($foo = mysql_fetch_assoc($bar)) {
	// что-то делаем
}

Функция mysql_fetch_assoc возвращает следующий ряд данных из переменной типа resource $bar, полученной в результате SQL-запроса при помощи функции mysql_query. Тип resource примечателен тем, что никакое его значение с логической точки зрения не рассматривается как false. Когда, наконец, рядов данных больше нет, функция возвращает обычное булевское значение false (так как нет никаких ограничений на тип возвращаемых значений и одна и та же функция может возвращать значения разных типов).

mysql_fetch_assoc — «хорошая» функция и с ней проблем не возникает. Но есть, к сожалению, и «плохие» функции. Рассмотрим, например, функцию strpos, возвращающую позицию подстроки needle в строке haystack или false в случае, если подстрока не найдена. Она имеет следующий вид.

mixed strpos(string $haystack, string $needle [, int $offset])

Очень часто возникает необходимость отвечать не на вопрос о позиции подстроки в некоторой строке, а лишь о её присутствии там. В силу малоизвестности функции strstr (а также иногда в силу того, что она медленна) для этой цели обычно используют strpos. И вот тут-то можно наступить на грабли.

$foo = 'Hello, world!';
echo strpos($foo, 'world') ? "SUCCESS!\n" : "FAILURE!\n";
echo strpos($foo, 'Hello') ? "SUCCESS!\n" : "FAILURE!\n";

Несмотря на то, что обе искомые подстроки присутствуют в строке $foo, второй блок echo выведет “FAILURE!”, потому что подстрока встретилась в позиции 0 и это значение было расценено как false. Не поможет и операция сравнения (==, !=), потому что выражение (0 == false) равно true (!). Единственным выходом из ситуации является операция сравнения со сравнением типа (===, !==), которая помимо сравнения значений сравнивает также их типы. Таким образом, корректный код с использованием strpos выглядит так:

$foo = 'Hello, world!';
echo (strpos($foo, 'world') !== false) ? "SUCCESS!\n" : "FAILURE!\n";
echo (strpos($foo, 'Hello') !== false) ? "SUCCESS!\n" : "FAILURE!\n";

«Нехорошие» функции встречаются не только среди возвращающих числа, но и среди возвращающих строки. Например, таковой является функция readdir, которая может вернуть строку вида ’0′ в случае, если файл с таким именем присутствует в рассматриваемом каталоге.

Комментарии запрещены.