10 самых распространенных ошибок при работе с PHP

php

PHP делает относительно легкой разработку систем на web платформе, что является основной причиной его популярности. Но не смотря на его простоту использования, PHP превратился в довольно сложный язык со множеством фреймворков, нюансов и тонкостей, которые могут “укусить” разработчиков, ведущих к волосо-выдергивающим часам отладки. Эта статья выделяет 10 самых распространенных ошибок, которых PHP разработчики должны остерегаться.

php

 

Распространенная ошибка #1: Оставление висячих ссылок на массивы после foreach циклов.

 

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

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

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

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

Вышеприведенный код выведет следующее:

Нет, это не опечатка. Последнее значение в последней строке действительно 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 цикла, чтобы удалить ссылку, нарпимер:

 

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

 

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

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

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

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

Вот еще один пример:

Код выше подразумевает, что если $_[‘active’] возвращает истину, то postData будет обязательно инициализирован и поэтому isset($postData) будет возвращать истину. Таким образом, напротив, код выше предполагает, что единственный способ по которому isset($postData) будет возвращать ложь, это если $_POST[‘active’] так же возвращает ложь.

Нет.

Как объяснено, isset($postData) будет всегда возвращать ложь если $postData установлен в null. И, таким образом, это возможно для isset($postData) вернуть ложь даже если $_POST[‘active’] возвращает истину. Итак, еще раз, вышеописанная логика имеет недостаток.

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

 

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

Для примера мы можем переписать первый из вышеприведенных примеров как показано:

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

 

Растространенная ошибка #3: Путанница между возвратом по ссылке и возвратом по значению

 

Рассмотрим этот пример кода:

Если вы выполните вышеприведенный код, вы получите следующее:

Что не правильно?

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

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

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

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

Будет выводиться test, как ожидалось.

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

Если вы предполагаете, что это будет выводить такую же “undefined index” ошибку, как наш ранний пример с массивом, то вы ошибаетесь. Причина в том, что в отличии от массивов, PHP всегда передает объекты по ссылке. (ArrayObject это SPL объект, который полностью повторяет функционал массива, но работает как объект).

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

Во всем сказанном стоит заметить, что практика возврата ссылки на массив или ArrayObject в общем это то, что должно избегаться, т.к. это предоставляет клиенту возможность модифицировать приватные данные экземпляра. Это “плевок в лицо” инкапсуляции. Вместо этого лучше использовать старомодные геттеры и сеттеры, например:

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

 

Растространенная ошибка #4: Выполнение запросов в цикле

 

Это не редкость натолкнуться на что-то вроде этого, если ваш PHP не работает:

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

Как результат, каждая итерация приведенного цикла будет вызывать отдельный запрос в базу данных. Так если, для примера, вы передаете массив с 1000 значений в в цикл, будет сгенерировано 1000 отдельных запросов к ресурсу! Если подобный скрипт вызыван в нескольких потоках, это может потенциально привести систему к полной остановке.

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

Один пример довольно распространненого случая где можно встретить неэффективные запросы (т.е. в цикле), это когда форма публикуется со списком значений (ИД для примера). Тогда, чтобы получить полный набор данных для каждого ИД код будет итерировать через массив и делать отдельный SQL запрос для каждого ИД. Это обычно выглядит как-то так:

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

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

 

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

 

Пока получение множества записей одновременно определенно более эффективнее, чем выполнение одного запроса для получения каждой записи, такой подход может потенциально вести к состоянию “out of memory”  в libmysqlclient когда используется PHP расширение.

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

Мы инициализируем таблицу базы данных вот так:

OK, теперь давайте проверим использование ресурсов:

Вывод:

Круто. Похоже что запрос безопасно выполняется в рамках границ ресурсов.

Все же, просто чтобы убедиться, давайте поднимем предел еще раз и установим его в 100,000. Ой-ой. Когда мы сделали это, мы получили:

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

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

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

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

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

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

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

 

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

 

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

Но это ни в коем случае не освобождает разраработчика от правильного обращения с UTF-8 и избегания ошибочного предположения, что все строки обязательно будут в “старом добром ASCII“. Код, который не может правильно обращаться с не-ASCII строками печально известен внесением грубых гейзенбагов  (heisenbugs) в ваш код. Даже простой вызов strlen($_POST[‘name’]) может вызвать проблемы если кто-то с фамилией “Schrödinger” попытается войти в вашу систему.

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

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

Особенно важный ресурс в этом отношении это UTF-8 Primer for PHP and MySQL пост от Francisco Claria в его блоге.

 

Растространенная ошибка #7: Предполагать, что $_POST будет всегда содержать ваши POST данные

 

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

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

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

Неожиданно результат будет:

Почему? Что случилось с нашей 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, как показано:

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

 

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

 

Взгляните на этот простой кусок кода и попробуйте догадаться что он выведет:

Если ответили от “а” до “z” вы может будете удевлены узнать, что вы ошиблись.

Да, он вывоедет от “а” до “z”, но так же он выведет от “аa” до “yz”. Давайте посмотрим почему.

В PHP нет символьного типа данных; только строковый доступен. Имея это в виду, приращение строки “z”  в PHP производит “aa”:

До того как конфуз станет больше, aa лексографически меньше чем z:

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

В таком случае, вот один способо правильно обойти значения от “а” до “z” в PHP:

Или по другому:

 

Растространенная ошибка #9: Игнорирование стандартов кодирования

 

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

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

К счастью для PHP программистов есть PHP Standards Recommendation () включающий следующие пять стандартов (не переводится, т.к. имена стандартов) :

  • PSR-0: Autoloading Standard
  • PSR-1: Basic Coding Standard
  • PSR-2: Coding Style Guide
  • PSR-3: Logger Interface
  • PSR-4: Autoloader

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

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

 

Растространенная ошибка #10: Не правильное использование empty()

 

 

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

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

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

Этот подход тем не менее достаточно популярен. Для примера это способ, которым  <a href="http://framework.zend.com/manual/2.3/en/modules/zend.db.table-gateway.html" target="_blank">Zend\Db\TableGateway</a> в Zend Framework 2 возвращает данные, когда вызывается  current() на результате TableGateway::select() как рекомендует документация. Разработчик может легко стать жертвой этой ошибки с похожими данными.

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

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

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

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

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

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

Очень хорошо.

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

Омг. Так, если мы пологаемся на empty(), мы можем быть введены в заблуждение веря, что свойство test в $magic пустое, когда в реальности оно равно ‘value’.

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

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

Так что главная идея это то, что метод empty() должен быть использован с осторожностью, т.к. он может привести к заблуждающим – или даже потенциально обманчивым – результатам, если кто-то не осторожен.

 

Заключение

 

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

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

 

Оригинал на английском:

http://www.toptal.com/php/10-most-common-mistakes-php-programmers-make

Это мой перевод. Если вы заметили ошибки или считаете, что какие-то части можно перевести лучше – пишите в комментарии.