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

Опубликовано: 2022-03-11

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)

Проблема в том, что если вы не будете осторожны, это также может иметь некоторые нежелательные побочные эффекты и последствия. В частности, в приведенном выше примере после выполнения кода $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 (ie, 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 на каждом шаге цикла. В результате вот что происходит на каждом шаге второго цикла foreach :

  • Проход 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]

Распространенная ошибка № 2: непонимание поведения isset()

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

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

Рассмотрим следующее:

 $data = fetchRecordFromStorage($storage, $identifier); if (!isset($data['keyShouldBeSet']) { // do something here if 'keyShouldBeSet' is not set }

Автор этого кода предположительно хотел проверить, установлен ли 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 }

Более того, комбинируя array_key_exists() с get_defined_vars() , мы можем надежно проверить, установлена ​​ли переменная в текущей области видимости или нет:

 if (array_key_exists('varShouldBeSet', get_defined_vars())) { // variable $varShouldBeSet exists in current scope }

Распространенная ошибка № 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 вернуть массив по ссылке (т. е. с помощью & ), 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. $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). echo $config->getValues()['test'];

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

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

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

 class Config { private $values = []; // return a REFERENCE to the actual $values array 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 public function __construct() { $this->values = new ArrayObject(); } public function getValues() { return $this->values; } } $config = new Config(); $config->getValues()['test'] = 'test'; echo $config->getValues()['test'];

Если вы догадались, что это приведет к той же ошибке «неопределенный индекс», что и в нашем предыдущем примере с array , вы ошиблись. На самом деле, этот код будет работать просто отлично. Причина в том, что, в отличие от массивов, 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; } }

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

Распространенная ошибка № 5: фальсификации использования памяти и неэффективность

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

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

Мы загрузим таблицу базы данных следующим образом:

 // connect to mysql $connection = new mysqli('localhost', 'username', 'password', 'database'); // create table of 400 columns $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 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 $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

Прохладный. Похоже, запрос безопасно управляется внутри с точки зрения ресурсов.

Однако, чтобы быть уверенным, давайте еще раз увеличим лимит и установим его на 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() не покажет никакого увеличения использования ресурсов, поскольку мы увеличиваем лимит в нашем запросе. Это приводит к проблемам, подобным той, что была показана выше, когда нас заставляют самоуспокоиться, думая, что с управлением памятью все в порядке. Но на самом деле наше управление памятью имеет серьезные недостатки, и мы можем столкнуться с проблемами, подобными показанной выше.

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

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

 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"); }

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

Распространенная ошибка № 6: игнорирование проблем с Unicode/UTF-8

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

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

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

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

Особенно ценным ресурсом в этом отношении является сообщение UTF-8 Primer for PHP and MySQL, написанное Франсиско Кларией в этом блоге.

Распространенная ошибка № 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. Например, это значение по умолчанию для публикации в сервисе AngularJS $http .)

На стороне сервера в нашем примере мы просто делаем дамп массива $_POST :

 // php var_dump($_POST);

Удивительно, но результат будет:

 array(0) { }

Почему? Что случилось с нашей строкой JSON {a: 'a', b: 'b'} ?

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

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

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

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

Затем, когда мы выгружаем массив $_POST , мы видим, что он правильно включает полезную нагрузку POST; например:

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

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

Посмотрите на этот образец кода и попробуйте угадать, что он напечатает:

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

Если вы ответили от «а» до «z», вы можете быть удивлены, узнав, что были неправы.

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

В PHP нет типа данных char ; доступна только string . Имея это в виду, увеличение 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

В таком случае вот один из способов правильного перебора значений от «a» до «z» в PHP:

 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), состоящая из следующих пяти стандартов:

  • PSR-0: стандарт автозагрузки
  • PSR-1: базовый стандарт кодирования
  • PSR-2: Руководство по стилю кодирования
  • PSR-3: Интерфейс регистратора
  • ПСР-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: $array = []; var_dump(empty($array)); // outputs bool(true) $array = new ArrayObject(); var_dump(empty($array)); // outputs bool(false) // why don't these both produce the same output?

И что еще хуже, результаты были бы другими до PHP 5.0:

 // Prior to PHP 5.0: $array = []; var_dump(empty($array)); // outputs bool(false) $array = new ArrayObject(); var_dump(empty($array)); // outputs 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): $array = []; var_dump(count($array)); // outputs int(0) $array = new ArrayObject(); var_dump(count($array)); // outputs int(0)

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

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

Сначала давайте определим класс Regular , который включает в себя test как обычное свойство:

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

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

 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" $magic = new Magic(); var_dump($magic->test); // outputs string(4) "value"

Ладно пока.

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

 var_dump(empty($regular->test)); // outputs bool(false) var_dump(empty($magic->test)); // outputs 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 не будет работать, и возникнут проблемы, подобные описанным здесь.

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