img

PHP-код с багами: 10 самых распространенных ошибок, которые совершают разработчики PHP

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

Ошибка №1. Оставлять висячие ссылки на массивы после циклов foreach

Не знаете точно, как использовать циклы foreach в PHP? Если вы хотите работать с каждым элементом итерируемого массива в отдельности, использование ссылок в циклах foreach может оказаться полезным. 

$arr = array(1, 2, 3, 4);
foreach ($arr as &$value) {
    $value = $value * 2;
}
// $arr is now array(2, 4, 6, 8) { // $arr – это теперь array(2, 4, 6, 8) }

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

Главное запомните, что у foreach нет области видимости. А значит, $value в приведенном выше примере является ссылкой на верхнюю часть сценария. На каждой итерации foreach задает ссылку, которая указывает на следующий элемент массива $array. Так что, после того, как цикл закончит свою работу, $value так и будет указывать на последний элемент $array и останется в области видимости. 

Ниже приведен пример ошибок, к которым это может привести. Такие ошибки сбивают с толку и их довольно трудно обнаружить.

$array = [1, 2, 3];
echo implode(',', $array), "\n";

foreach ($array as &$value) {}    // by reference { // по ссылке }
echo implode(',', $array), "\n";

foreach ($array as $value) {}     // by value (i.e., copy) {  // по значению (то есть путем копирования) }
echo implode(',', $array), "\n";

Приведенный выше код выдаст следующий результат:

1,2,3
1,2,3
1,2,2

Нет, это не опечатка. Последнее значение в последней строке и правда равно 2, а не 3. 

Почему же?

После того, как мы прошли первый цикл foreach, $array остается таким же, но, как мы уже говорили выше, $value остается в качестве висячей ссылки на последний элемент $array (так как цикл foreach обращался к $value по ссылке).

В результате, когда мы проходим по второму циклу foreach, начинают происходить всякие «странные вещи». В частности, так как теперь мы осуществляем доступ к $value по значению (то есть путем копирования), foreach на каждом шаге цикла копирует каждый последующий элемент массива $array в $value. Таким образом, на каждом шаге второго цикла происходит следующее:

  • Проход 1: $array[0] (т.е. «1») копируется в $value (которое является ссылкой на $array[2]), поэтому теперь $array[2] равен 1. Так что, теперь $array содержит [1, 2, 1]. 
  • Проход 2: $array[1] (т.е. «2») копируется в $value (которое является ссылкой на $array[2]), поэтому теперь $array[2] равен 2. Так что, теперь $array содержит [1, 2, 2].
  • Проход 3: $array[2] (которое теперь равно «2») копируется в $value (которое является ссылкой на $array[2]), так что $array[2] так и остается равным 2. Поэтому теперь $array содержит [1, 2, 2].

Для того, чтобы быть иметь возможность использовать преимущества ссылок в циклах foreach и при этом быть уверенным в том, что подобных проблем возникать не будет, вызовите функцию unset() для переменной сразу после цикла foreach, чтобы удалить ссылку. Например, 

$arr = array(1, 2, 3, 4);
foreach ($arr as &$value) {
    $value = $value * 2;
}
unset($value);   // $value no longer references $arr[3] { // $value уже не является ссылкой на $arr[3] }

Ошибка №2. Непонимание того, как ведет себя функция isset()

Несмотря на свое название, функция isset() возвращает значение false не только для несуществующих элементов, но и для нулевых значений (null). 

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

Давайте посмотрим на следующий фрагмент кода:

$data = fetchRecordFromStorage($storage, $identifier);
if (!isset($data['keyShouldBeSet']) {
    // do something here if 'keyShouldBeSet' is not set
    { // если 'keyShouldBeSet' не существует, выполняются действия }
}

Тот, кто писал этот код, судя по всему, хотел проверить, существует ли keyShouldBeSet в $data. Но, как мы уже говорили ранее, функция isset($data['keyShouldBeSet']) вернет false, даже если $data['keyShouldBeSet'] существует, но имеет значение null. Так что, логика, приведенная выше, неверна.

А вот другой пример:

if ($_POST['active']) {
    $postData = extractSomething($_POST);
}

// ...

if (!isset($postData)) {
    echo 'post not active';
}

Код выше предполагает, что, если $_POST['active'] возвращает true, то postData обязательно существует, а, значит, isset($postData) вернет true. И наоборот, если isset($postData) возвращает false, то и $_POST['active'] возвращает false. 

Но это не так. 

Как мы уже говорили, isset($postData) может также вернуть false, если $postData имеет значение null. Таким образом, функция isset($postData) может вернуть false, даже если $_POST['active'] вернул true. И опять получается, что логика, приведенная выше, неверна.

К слову, если цель приведенного выше кода на самом деле заключалась в том, чтобы еще раз проверить, возвращает ли $_POST['active'] значение true, прибегнуть к помощи функции isset() было не самым хорошим решением. Вместо этого было бы гораздо лучше просто перепроверить $_POST['active'], то есть:

if ($_POST['active']) {
    $postData = extractSomething($_POST);
}

// ...

if ($_POST['active']) {
    echo 'post not active';
}

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

Например, вы могли бы переписать первый пример следующим образом:

$data = fetchRecordFromStorage($storage, $identifier);
if (! array_key_exists('keyShouldBeSet', $data)) {
    // do this if 'keyShouldBeSet' isn't set
    { // если ‘keyShouldBeSet’ не существует, то выполняются действия, указанные здесь }
}
Более того, вы можете проверить, существует ли переменная в текущей области видимости. Для этого вам нужно объединить методы array_key_exists() и get_defined_vars():
if (array_key_exists('varShouldBeSet', get_defined_vars())) {
    // variable $varShouldBeSet exists in current scope
    { // переменная $varShouldBeSet существует в текущей области видимости }

}

Ошибка №3. Путаница с возвратом по ссылке и по значению

Давайте посмотрим на следующий фрагмент кода:

class Config
{
    private $values = [];

    public function getValues() {
        return $this->values;
    }
}

$config = new Config();

$config->getValues()['test'] = 'test';
echo $config->getValues()['test'];

Если вы запустите этот код, то получите следующий результат:

PHP Notice:  Undefined index: test in /path/to/my/script.php on line 21

Что же не так?

Проблема в том, что в коде, который мы привели выше, произошла путаница с возвратом массива по ссылке и по значению. Если вы явно не укажите PHP, чтобы он вернул массив по ссылке (то есть с помощью &), то он по умолчанию вернет массив «по значению». Это значит, что будет возвращена копия массива, и, как следствие, вызываемая функция и вызывающая сторона будут обращаться к разным экземплярам массива. 

Так что, вызываемая выше функция getValues() возвращает копию массива $values, а не ссылку на него. Помня об этом, вернемся к двум ключевым строкам из примера выше:

// getValues() returns a COPY of the $values array, so this adds a 'test' element
// to a COPY of the $values array, but not to the $values array itself.
{ // функция getValues() возвращает КОПИЮ массива $values, а значит, что элемент test добавляется в КОПИЮ массива $values, в не в сам массив. }
$config->getValues()['test'] = 'test';

// getValues() again returns ANOTHER COPY of the $values array, and THIS copy doesn't
// contain a 'test' element (which is why we get the "undefined index" message).
{ // функция getValues() снова возвращает ДРУГУЮ КОПИЮ массива $values, и в ЭТОЙ копии нет элемента test (собственно говоря, почему мы и получаем сообщение о неопределенном индексе) }
echo $config->getValues()['test'];

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

$vals = $config->getValues();
$vals['test'] = 'test';
echo $vals['test'];

Этот код сработает отлично, то есть он выведет элемент test, не выдавая сообщения о неопределенном индексе. Однако такой подход может оказаться непригодным; это зависит от ваших целей. В частности, с помощью этого кода вы не сможете изменить исходный массив $values. Так что, если вы хотите, чтобы изменения (например, добавление элемента test) касались также исходного массива, вам нужно изменить функцию getValues() так, чтобы она возвращала ссылку на сам массив $values. Вы можете это сделать, добавив & перед именем функции, показывая таким образом, что она должна вернуть ссылку:

class Config
{
    private $values = [];

    // return a REFERENCE to the actual $values array
    { // возвращает ССЫЛКУ на фактический массив $values }
    public function &getValues() {
        return $this->values;
    }
}

$config = new Config();

$config->getValues()['test'] = 'test';
echo $config->getValues()['test'];

Как мы и хотели, результатом будет test. 

Но давайте запутаем ситуацию еще больше. Рассмотрим следующий код:

class Config
{
    private $values;

    // using ArrayObject rather than array
    { // используем ArrayObject вместо массива }
    public function __construct() {
        $this->values = new ArrayObject();
    }

    public function getValues() {
        return $this->values;
    }
}

$config = new Config();

$config->getValues()['test'] = 'test';
echo $config->getValues()['test'];

Если вы считаете, что здесь мы получим ту же ошибку с неопределенным индексом (что и в предыдущем примере), то вы ошибаетесь. На самом деле, этот код будет отлично работать. Это связано с тем, что PHP всегда передает объекты по ссылке (в отличие от массивов). (ArrayObject – это объект SPL, который полностью имитирует использование массивов, но при этом работает как объект.) 

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

При этом стоит отметить, что возвращение ссылок на массив или ArrayObject, как правило, считается не самой лучшей практикой, так как вызывающая сторона может изменить частные данные экземпляра. Это противоречит понятию инкапсуляции. Вместо этого лучше использовать старомодные геттеры и сеттеры. Например,

class Config
{
    private $values = [];
    
    public function setValue($key, $value) {
        $this->values[$key] = $value;
    }
    
    public function getValue($key) {
        return $this->values[$key];
    }
}

$config = new Config();

$config->setValue('testKey', 'testValue');
echo $config->getValue('testKey');    // echos 'testValue'

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

Ошибка №4. Выполнение запросов внутри цикла

Нередко можно столкнуться с чем-то подобным, что приводит к тому, что ваш PHP-код не работает:

$models = [];

foreach ($inputValues as $inputValue) {
    $models[] = $valueRepository->findByValue($inputValue);
}

По сути, здесь все верно, но если вы проследите за логикой кода, то можете обнаружить, что безвредный на первый взгляд вызов $valueRepository->findByValue() в итоге приводит к появлению некоторого запроса:

$result = $connection->query("SELECT `x`,`y` FROM `values` WHERE `value`=" . $inputValue);

В результате, с каждой итерацией будет выполняться запрос к базе данных. Поэтому, если вы передадите в цикл массив из 1000 элементов, то он создаст 1000 отдельных запросов к базе данных! А если этот сценарий будет вызываться в нескольких потоках, то в теории это может привести к полной остановке системы.  

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

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

$data = [];
foreach ($ids as $id) {
    $result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` = " . $id);
    $data[] = $result->fetch_row();
}

Но то же самое можно выполнить гораздо более эффективным способом. Вам понадобиться всего один SQL-запрос:

$data = [];
if (count($ids)) {
    $result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` IN (" . implode(',', $ids));
    while ($row = $result->fetch_row()) {
        $data[] = $row;
    }
}

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

Ошибка №5. Ложное и нерациональное использование памяти

Несмотря на то, что извлечение множества записей за раз куда как более эффективно, чем выполнение запросов для каждой строки, которую вы хотите извлечь, если вы используете расширение PHP mysql и библиотеку libmysqlclient, то такой подход в теории может привести к ситуации под названием «недостаточно памяти». 

Чтобы понять, о чем я говорю, давайте посмотрим на пример с ограниченными ресурсами (512 Мб оперативной памяти), MySQL и php-cli.

Давайте загрузим таблицу базы данных:

// connect to mysql { // подключаемся к MySQL }
$connection = new mysqli('localhost', 'username', 'password', 'database');

// create table of 400 columns { // создаем таблицу из 400 столбцов }
$query = 'CREATE TABLE `test`(`id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT';
for ($col = 0; $col < 400; $col++) {
    $query .= ", `col$col` CHAR(10) NOT NULL";
}
$query .= ');';
$connection->query($query);

// write 2 million rows { // добавляем 2 миллиона строк }
for ($row = 0; $row < 2000000; $row++) {
    $query = "INSERT INTO `test` VALUES ($row";
    for ($col = 0; $col < 400; $col++) {
        $query .= ', ' . mt_rand(1000000000, 9999999999);
    }
    $query .= ')';
    $connection->query($query);
}

Окей, а теперь давайте проверим, как используются ресурсы:

// connect to mysql { // подключаемся к MySQL }
$connection = new mysqli('localhost', 'username', 'password', 'database');
echo "Before: " . memory_get_peak_usage() . "\n";

$res = $connection->query('SELECT `x`,`y` FROM `test` LIMIT 1');
echo "Limit 1: " . memory_get_peak_usage() . "\n";

$res = $connection->query('SELECT `x`,`y` FROM `test` LIMIT 10000');
echo "Limit 10000: " . memory_get_peak_usage() . "\n";

Вот так выглядит результат:

Before: 224704
Limit 1: 224704
Limit 10000: 224704

Отлично. Выглядит так, как будто с точки зрения ресурсов запрос благополучно управляется внутри. 

Но, чтобы убедиться наверняка, давайте увеличим значение LIMIT до 100 000. Опа, и вот что мы получим:

PHP Warning:  mysqli::query(): (HY000/2013):
              Lost connection to MySQL server during query in /root/test.php on line 11

Что же произошло?

Здесь проблема заключается в том, как работает модуль PHP mysql. По сути это просто прокси для libmysqlclient, который выполняет всю грязную работу. Когда часть данных уже выбрана, они попадаю непосредственно в память. А так как эта память никак не управляется менеджером PHP, функция memory_get_peak_usage() не сообщит нам о том, что количество использованных ресурсов возросло, так как мы увеличиваем LIMIT в нашем запросе. Это приводит к проблемам, о которых мы говорили выше. Получается, что нас обманным путем пытаются уверить, что с нашим управлением памятью все в порядке. Но по сути наше управление памятью далеко не идеально, и мы можем столкнуться с проблемами (такими как мы описывали выше). 

Во всяком случае, вы можете избежать ситуаций с ложным использованием памяти, описанных выше. Для этого вам нужно использовать модуль mysqlnd. Хотя сам по себе он вам никак не поможет оптимизировать использование вашей памяти. Этот модуль скомпилирован как собственное расширение PHP, и он использует диспетчер памяти PHP.

Таким образом, если мы запустим тестовый код, приведенный выше, и вместо mysql воспользуемся mysqlnd, то получим гораздо более реалистичную картину использования нашей памяти:

Before: 232048
Limit 1: 324952
Limit 10000: 32572912

И кстати, она будет куда хуже. Если обратиться к документации PHP, то там можно найти, что mysql использует в два раза больше ресурсов для хранения данных, чем mysqlnd. Именно поэтому исходный сценарий, в котором мы использовали mysql, на самом деле использовал даже больше памяти, чем продемонстрировано здесь (примерно в два раза больше). 

Дабы избежать такого рода проблем, попробуйте ограничить размер ваших запросов или использовать цикл с меньшим количеством итераций. Например,

$totalNumberToFetch = 10000;
$portionSize = 100;

for ($i = 0; $i <= ceil($totalNumberToFetch / $portionSize); $i++) {
    $limitFrom = $portionSize * $i;
    $res = $connection->query(
                         "SELECT `x`,`y` FROM `test` LIMIT $limitFrom, $portionSize");
}

Если мы рассуждаем об этой ошибке и ошибке №4, то должны понимать, что необходимо найти золотую середину между слишком детализированными и повторяющимися запросами и огромными штучными запросами. Как и почти во всем в этой жизни, необходим баланс; любая крайность может навредить корректной работе PHP.  

Ошибка №6. Игнорирование проблем с Unicode/UTF-8

В некотором смысле это скорее проблема самого PHP, нежели что-то, с чем вы столкнетесь при отладке кода. Однако эту проблему так толком и не решили. Ядро PHP 6 должно было поддерживать Unicode, но, так как разработка PHP 6 была приостановлена в 2010 году, с этим пришлось повременить. 

Но это вовсе не значит, что разработчики могут игнорировать правильную обработку UTF-8 и думать, что все строки обязательно будут «старым добрым ASCII». Код, который не может правильно обработать не-ASCII строки, печально известен внедрением опасных гейзенбагов. Даже простые вызовы strlen($_POST['name']) могут вызвать проблемы, если некто с фамилией Schr?dinger захочет зарегистрироваться в вашей системе. 

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

  • Если вы мало что знаете о Unicode и UTF-8, то изучите, по крайней мере, основы. 
  • Вместо старых строковых функций используйте функции mb_* (убедитесь, что в вашей сборке PHP есть расширение multibyte).
  • Убедитесь, что ваша база данных и таблицы настроены таким образом, что могут использовать Unicode (многие сборки MySQL все еще используют по умолчанию latin1).
  • Помните, что json_encode() преобразует не-ASCII символы (например, Schr?dinger преобразуется в Schr\u00f6dinger), а serialize() - нет. 
  • Убедитесь, что ваши файлы с PHP-кодом имеют кодировку UTF-8. Это необходимо, чтобы избежать конфликтов при объединении строк с жестко запрограммированными или сконфигурированными строковыми постоянными.

Ошибка №7. Предполагать, что $_POST всегда будет содержать ваши данные POST-запроса

Несмотря на свое название, массив $_POST не всегда будет содержать ваши данные POST-запроса. Он и вовсе может оказаться пустым. Чтобы разобраться в этом, давайте рассмотрим пример. Допустим, что мы делаем запрос к серверу, вызвав jQuery.ajax():

// js
$.ajax({
    url: 'http://my.site/some/path',
    method: 'post',
    data: JSON.stringify({a: 'a', b: 'b'}),
    contentType: 'application/json'
});

(К слову, обратите внимание, что здесь есть contentType: 'application/json'. Мы отправляем данные в формате JSON, что довольно популярно для API. Это стандартная практика, например, при добавлении службы $http в AngularJS.)

Мы просто распечатываем массив $_POST на серверной стороне:

// php
var_dump($_POST);

Сюрприз! А вот и результат:

array(0) { }

Но почему? Что случилось со строкой JSON {a: 'a', b: 'b'}?

Ответ таков: PHP анализирует полезную нагрузку POST-запроса автоматически только в том случае, если она имеет тип содержимого application/x-www-form-urlencoded или multipart/form-data. Причины такого поведения исторически сложившиеся – много лег назад, когда $_POST был только реализован в PHP, это были единственные типы содержимого. Таким образом, PHP не загружает полезную нагрузку POST-запроса автоматически ни для каких других типов содержимого (даже для тех, которые считаются довольно популярными на сегодняшний день, например, application/json).

Так как $_POST - это суперглобальная переменная, то переопределив ее один раз (желательно в самом начале нашего сценария), мы сможем пользоваться этим измененным значением (т.е. в том числе и полезной нагрузкой POST-запроса) во всем нашем коде. Это очень важно, так как фреймворки PHP и почти все пользовательские сценарии используют $_POST для того, чтобы извлекать и преобразовывать данные запроса. 

Поэтому, если мы, например, обрабатываем полезную нагрузку POST-запроса с типом содержимого application/json, то мы должны вручную проанализировать содержимое запроса (т.е. декодировать данные JSON) и переопределить переменную $_POST:

// php
$_POST = json_decode(file_get_contents('php://input'), true);

После чего, когда мы распечатаем массив, мы увидим, что он должным образом содержит полезную нагрузку POST-запроса:

array(2) { ["a"]=> string(1) "a" ["b"]=> string(1) "b" }

Ошибка №8. Думать о том, что PHP поддерживает символьный тип данных

Посмотрите на следующий фрагмент кода и попробуйте догадаться, что он выведет:

for ($c = 'a'; $c <= 'z'; $c++) {
    echo $c . "\n";
}

Если вы ответили, что он выведет буквы от «a» до «z», то, вы будете удивлены – это не так.

Да, он выведет буквы от «а» до «z», но потом он также выведет сочетания букв от «аа» до «yz». Давайте разберемся, почему так происходит.

В PHP нет такого типа данных как char; есть только тип string. Принимая это в расчет, мы должны понимать, что, увеличив строку z, PHP выдаст aa:

php> $c = 'z'; echo ++$c . "\n";
aa

Впрочем, запутаю вас еще больше: с лексикографической точки зрения aa меньше, чем z:

php> var_export((boolean)('aa' < 'z')) . "\n";
true

Именно поэтому пример кода, который мы привели в начале, сначала напечатает буквы от a до z, а потом также напечатает сочетания букв от aa до yz. Он остановится, когда достигнет значения za – первого значения, которое «больше», чем z:

php> var_export((boolean)('za' < 'z')) . "\n";
false

А раз так, то вот один из способов, как можно правильно перебрать в PHP значения от «а» до «z»:

for ($i = ord('a'); $i <= ord('z'); $i++) {
    echo chr($i) . "\n";
}
Или вот так:
$letters = range('a', 'z');

for ($i = 0; $i < count($letters); $i++) {
    echo $letters[$i] . "\n";
}

Ошибка №9. Игнорирование стандартов написания кода

И хотя игнорирование стандартов написания кода не приводит сразу к необходимости отлаживать PHP-код, это все же, пожалуй, одна из самых важных тем, которую стоит здесь обсудить. 

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

К счастью для PHP-разработчиков, существуют Рекомендации по стандартам PHP (PSR - PHP Standards Recommendation). В ней есть пять стандартов:

  • PSR-0: Стандарт автозагрузки
  • PSR-1: Базовый стандарт оформления кода
  • PSR-2: Рекомендации по оформлению кода
  • PSR-3: Интерфейс протоколирования
  • PSR-4: Улучшенная автозагрузка

Изначально PSR был создан на базе материалов, полученных от лиц, сопровождающих наиболее узнаваемые на рынке платформы. Свой вклад внесли Zend, Drupal, Symfony, Joomla и прочие, и теперь они придерживаются этих стандартов. И даже PEAR, который много лет назад делал попытку стать стандартом, теперь является частью PSR.

В некоторым смысле практически неважно, какой стандарт вы используете, если вы принимаете его «условия игры» и придерживаетесь его. Однако, лучше всего придерживаться именно PSR, если у вас нет каких-то веских причин этого не делать. Все больше и больше команд и проектов соответствуют требованиям PSR. На данный момент большая часть разработчиков PHP абсолютно точно признала его Стандартом, поэтому, используя его, вы можете быть уверены, что новым разработчикам, которые будут присоединяться к вашей команде, этот стандарт будет знаком, и они будут чувствовать себя комфортно. 

Ошибка №10. Неправильное использование метода empty()

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

Для начала давайте вернемся к массивам и экземплярам ArrayObject (которые имитируют массивы). Учитывая их сходство, можно предположить, что массивы и экземпляры ArrayObject будут вести себя одинаково. Но это далеко не так. Например, вот что происходит в PHP 5.0:

// PHP 5.0 or later: { // PHP 5.0 и более поздние версии: }
$array = [];
var_dump(empty($array));        // outputs bool(true) { // выводит bool(true) }
$array = new ArrayObject();
var_dump(empty($array));        // outputs bool(false) { // выводит bool(false) }
// why don't these both produce the same output? 
{ // почему мы получаем разный результат? }

А знаете, что еще хуже? Для более ранних версий PHP (до 5.0) результаты были бы другими:

// Prior to PHP 5.0: { // версии, предшествующие PHP 5.0: }
$array = [];
var_dump(empty($array));        // outputs bool(false) { // выводит bool(false) }
$array = new ArrayObject();
var_dump(empty($array));        // outputs bool(false) { // выводит bool(false) }

Как ни печально, но такой подход довольно популярен. Например, как предполагает документация, Zend\Db\TableGateway из Zend Framework 2 именно таким образом возвращает данные при вызове current() для результата TableGateway::select(). Разработчик может легко допустить ошибку, используя такие данные. 

Для того, чтобы избежать таких проблем, лучше всего для проверки пустых массивов использовать функцию count():

// Note that this work in ALL versions of PHP (both pre and post 5.0):
{ // Обратите внимание, что это работает во ВСЕХ версиях PHP (и тех, что были до 5.0, и тех, что были после 5.0) }
$array = [];
var_dump(count($array));        // outputs int(0) { // выводит int(0) }
$array = new ArrayObject();
var_dump(count($array));        // outputs int(0) { // выводит int(0) }

И к слову, так как PHP преобразует 0 в false, функцию count() можно использовать для проверки пустых массивов в условной конструкции if (). Также стоит отметить, что эта функция имеет постоянную сложность (то есть O(1)), что еще больше доказывает тот факт, что это правильный выбор. 

Есть еще один пример, когда метод empty() может быть опасной. Речь идет о ее совместном использовании с функцией класса Magic – функцией __get(). Давайте определим два класса и свойство test в каждом из них. 

Для начала давайте определим класс Regular, у которого свойство test - это обычное свойство:

class Regular
{
public $test = 'value';
}

А теперь давайте определим класс Magic, который для доступа к свойству test использует оператор __get():

class Magic
{
private $values = ['test' => 'value'];

public function __get($key)
{
if (isset($this->values[$key])) {
return $this->values[$key];
}
}
}

Окей, а теперь давайте посмотрим, что будет происходить, когда мы попытаемся получить доступ к свойству test каждого из этих классов:

$regular = new Regular();
var_dump($regular->test);    // outputs string(4) "value" { // выводит string(4) “value” }
$magic = new Magic();
var_dump($magic->test);      // outputs string(4) "value" { // выводит string(4) “value” }

Пока все хорошо.

А теперь давайте посмотрим, что будет происходить, когда вы вызовем метод empty() для каждого из эти классов:

var_dump(empty($regular->test));    // outputs bool(false) { // выводит bool(false) }
var_dump(empty($magic->test));      // outputs bool(true) { // выводит bool(true) }

Ах ты! Так что, если мы прибегаем к помощи метода empty(), то мы можем прийти в неверным выводам, полагая, что свойство test ($magic) пусто, хотя на самом деле у него есть значение - 'value'.

Увы, но если класс для получения значения свойства использует функцию __get(), то нет какого-то беспроигрышного способа проверить, является ли свойство пустым или нет. Если вы находитесь вне области видимости класса, то вы сможете лишь проверить, вернется ли нулевое значение (null), и это не обязательно значит, что соответствующее свойство не существует, ему просто могли присвоить значение null.

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

Notice: Undefined property: Regular::$nonExistantTest in /path/to/test.php on line 10

Call Stack:
    0.0012     234704   1. {main}() /path/to/test.php:0

Так что, главное, что нужно запомнить, что метод empty() стоит использовать аккуратно, так как в противном случае вы можете получить довольно запутанные (или даже неправильные) результаты. 

Заключение

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

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

Ссылка
скопирована
Программирование
Скидка 25%
Python Advanced. Продвинутый курс
Освойте асинхронное и метапрограммирование, изучите аннотацию типов и напишите собственное приложение на FastAPI. Улучшите свои навыки Python, чтобы совершить быстрый рост вашего грейда до уровня middle.
Получи бесплатный
вводный урок!
Пожалуйста, укажите корректный e-mail
отправили вводный урок на твой e-mail!
Получи все материалы в telegram и ускорь обучение!
img
Еще по теме:
img
С помощью PHP можно относительно легко создать веб-приложение, и это является одной из причин, по которой он так популярен. Одна
img
  Хотите работать с объектно-ориентированным проектированием в Python? Начните уже сегодня, изучив метод Python под названием  _
img
  Будучи программистом, при разработке программного обеспечения вы неизбежно столкнетесь с ошибками. Это может быть что угодно –
img
Что такое браузерные события? Событие обозначает действие или явление, которое происходит в программируемой системе. Система уве
img
Когда я только познакомился с GitHub, я даже не представлял, что это такое README-файл и какую функцию он выполняет. Между нами
img
«Чистые функции» и «нечистые функции» - эти два термина программирования, которые вы можете довольно часто встречать в рамках фу
Комментарии
ЛЕТНИЕ СКИДКИ
40%
50%
60%
До конца акции: 30 дней 24 : 59 : 59