Pull to refresh

PHP: фрактал плохого дизайна

Reading time 32 min
Views 204K
Original author: Eevee

Предисловие


Я капризный. Я жалуюсь о многих вещах. Многое в мире технологий мне не нравится и это предсказуемо: программирование — шумная молодая дисциплина, и никто из нас не имеет ни малейшего представления, что он делает. Учитывая закон Старджона, у нас достаточно вещей для постижения на всю жизнь.

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

PHP — единственное исключение. Фактически каждая деталь PHP в какой-то мере поломана. Язык, структура, экосистема: всё плохо. И даже нельзя указать на одну убийственную вещь, настолько дефект систематичный. Каждый раз, когда я пытаюсь систематизировать недостатки PHP, я теряюсь в поиске в глубину обнаруживая всё больше и больше ужасных мелочей(отсюда фрактал).

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

Аналогия


Я только что выпалил это Мэл, чтобы описать своё расстройство и она настояла на том, чтобы я воспроизвёл это здесь:

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

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

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

Берёте плоскогубцы, у которых нет зазубрин; они плоские и гладкие. Не так полезно, как могло бы быть, но ими всё ещё можно выкручивать болты.

И так далее. Все инструменты чем-то странные и вывернутые, но не настолько, чтобы быть совсем бесполезными. И во всём наборе нет конкретной проблемы; в нём есть все инструменты.

Теперь представьте себе миллионы плотников, использующих такой вот набор инструментов и говорящих вам: «А что не так с этими инструментами? Я никогда не использовал ничего другого и они отлично работают!». И плотники показывают вам, построенные ими дома с пятиугольными комнатами и крышей кверху ногами. Вы стучитесь в дверь, она просто падает внутрь и они орут на вас за то, что вы сломали их дверь.

Вот что не так с PHP.


Расстановка сил


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

  • Язык должен быть предсказуем. Язык — носитель для выражения человеческих идей и выполнения их на компьютере, поэтому человеческое понимание правильности программы критично.
  • Язык должен быть целостен. Похожие вещи должны быть похожи, разные должны различаться. Знание части языка должно помогать в изучении и понимании остальной части.
  • Язык должен быть краток. Новые языки существуют, чтобы уменьшить шаблонность присущую старым языкам. (Мы все могли бы программировать на машинных кодах.) Язык должен в тоже время избегать введения своих собственных шаблонов.
  • Язык должен быть надёжен. Языки — инструменты для решения задач; проблемы, которые они представляют сами по себе должны быть минимальны. Любые непонятные моменты вызывают смущение.
  • Язык должен быть отлаживаем. Если что-то идёт не так, программист обязан это починить, и нам нужна вся помощь, которую мы можем получить.


Моя позиция такова:

  • PHP полон сюрпризов: mysql_real_escape_string, E_ACTUALLY_ALL
  • PHP не целостен: strpos, str_rot13
  • PHP требует шаблонного кода: проверка ошибок вокруг «C API»-вызовов, ===
  • PHP чудной: ==, foreach ($foo as &$bar)
  • PHP непрозрачен: без стэктрэйсов по умолчанию и фатальных ошибок, сложный error reporting.


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

Не нужно следующих комментариев


Я участвовал во многих спорах о PHP. Слышал много общих контраргументов на самом деле предназначенных, чтобы закрыть тему. Пожалуйста воздержитесь от них :(

  • Не говорите мне, что «хорошие разработчики могут писать хороший код на любом языке», или что «плохие разработчики что-то там ещё». Это ничего не значит. Хороший плотник может забить гвоздь камнем или молотком, но где вы видели плотников забивающих что-либо камнями? Один из факторов, определяющих хорошего разработчика, это способность выбрать подходящие инструменты.
  • Не говорите мне, что разработчик обязан помнить тысячи странных исключений и неожиданных поведений. Да, это обязательно в любой системе, потому что компьтеры сосут. Только это не значит, что нет верхнего предела фигни в системе. В PHP нет ничего кроме исключений, и это не круто, если борьба с языком занимает больше усилий нежели написание самой программы. Мои инструменты не должны создавать мне не позитивную работу.
  • Не говорите мне, что «так работает C API». Какой вообще тогда смысл использовать высокоуровневый язык, если он даёт только строковые хэлперы и массу дословных C обёрток? Просто пишите на C! Смотрите, для него даже есть CGI-библиотека.
  • Не говорите мне: «Это тебе за то, что делаешь странные вещи». Если две фичи существуют, кто-нибудь когда-нибудь обязательно найдёт причину использовать их вместе. И снова, это не C: спецификации нет, как нет и надобности в «неопределённом поведении».
  • Не говорите мне, что Facebook или Wikipedia написаны на PHP. Я в курсе! Они так же могли бы быть написаны на Brainfuck'е, достаточно умные люди могут пересилить это всё. Всё, что мы знаем, это то, что время разработки могло быть ополовинено или удвоенно если бы эти продукты были написаны на каком-нибудь другом языке; только этот отдельный аргумент ничего не значит.
  • В идеале вы не должны говорить мне ничего! Это мой лучший выстрел; если он не изменит мнения о PHP, тогда его не изменит ничто, так что хорош уже спорить с каким-то чуваком в Интернете и иди уже сделай крутой сайт в рекордные сроки, чтобы доказать, что я не прав :)


Кстати: я обожаю Python. И с удовольствием прожужжу тебе уши, ноя о нём, если ты на самом деле этого хочешь. Я не утверждаю, что он идеален; я просто взвесил его преимущества и его проблемы и сделал вывод, что он лучше всего подходит для того, что я делаю.

И я никогда не встречал PHP-разработчика, который может сделать тоже самое на PHP. Но я натыкался на достаточное количество тех, кто сразу начинает извинятся за что-то и всё, что делает PHP. Такое мышление ужасает.

PHP


Ядро языка


CPAN был назван «стандартной библиотекой Perl». Это не говорит много о стандартной библиотеке Perl, но указывает, что прочное ядро может построить замечательные вещи.

Философия


  • PHP изначально создавался для непрограммистов(и если читать между строк не для программ); он не смог уйти от своих корней. Вот из документации по PHP 2.0 цитата о том, как делался выбор относительно приведения типов для + и прочих:

    Отдельные операторы для каждого типа сильно усложняют язык, например, вы больше не сможете использовать '==' для строк(что?), вы теперь будете использовать 'eq'. Я не вижу в этом никакого смысла, особенно в таком языке как PHP, где большинство скриптов будут достаточно простыми и в большинстве случаев, написаны непрограммистами, которым нужен язык с простейшим логическим синтаксисом и низким порогом вхождения.
  • PHP построен, чтобы продолжать фурычить при любых обстоятельствах. Если есть выбор между тем, чтобы сделать непонятно что и упасть с ошибкой, он сделает непонятно что. Что-нибудь лучше, чем совсем ничего.
  • Дизайн не имеет определённой философии. Ранний PHP был вдохновлён Perl'ом; огромная std-библиотека с «out»-параметрами из C; ОО-часть сделана как в C++ и Java.
  • PHP обширно черпает вдохновение из других языков, при этом ему удаётся быть непонятным для тех, кто эти языки знает. (int) выглядит как C, но int не существует. Нэймспэйсы используют \. Новый синтаксис массивов получился уникальный среди всех языков с хэш-литералами: [key=>value].
  • Слабая типизация(всмысле, тихая автоматическая конверсия между строками/числами/всем остальным) настолько сложная, что она не стоит того, сколько бы усилий начинающего программиста она не сохраняла.
  • Новая функциональность реализуется как новый синтаксис, даже если она мала; большинство её делается с помощью функции или чего похожего на функции. Кроме поддержки классов, которая заслуживает множества новых операторов и ключевых слов.
  • Некоторые из проблем, описанных в этой статье на самом деле имеют первоклассное решение — если конечно вы хотите платить Zend'у за фиксы к их языку с открытыми исходниками.
  • Целая куча событий происходит за сценой. Взять хотя бы вот этот код, откуда-то из PHP-документации:
    @fopen('http://example.com/not-existing-file', 'r');
    

    Что он будет делать?

    • Если PHP скомпилирован с --disable-url-fopen-wrapper, он не будет работать. (Документация не говорит, что означает «не будет работать»; вернёт null, бросит исключение?) Заметьте, что этот флаг убрали в PHP 5.2.5.
    • Если allow_url_fopen выключен в php.ini, он тоже не будет работать. (Как не будет? Нет идей.)
    • Из-за @, предупреждение о несуществующем файле не будет выведено.
    • Но будет выведено, если scream.enabled установлен в php.ini.
    • Или если scream.enabled вручную установлен через ini_set.
    • Но не в том случае, если не установлен корректный error_reporting.
    • Если оно будет выведено, куда оно будет выведено зависит от display_errors, снова в php.ini. Или ini_set.


    Я не могу сказать как такой безобидный вызов функции будет себя вести без проверки флагов времени компиляции, глобальной конфигурации сервера и конфигурации в моей программе. И это всё встроенное поведение.
  • Этот язык полон глобального и неявного поведения. mbstring использует глобальную кодировку. func_get_arg и прочие вроде бы обычная функция, но оперирует над выполняемой в данный момент функцией. Обработка ошибок и исключений имеет глобальные умолчания. register_tick_function устанавливает глобальную функцию, которая выполняется каждый тик — чего?!
  • До сих пор нет поддержки потоков.(Неудивительно, учитывая вышеуказанное.) С отсутствием встроенного fork(упомянуто ниже), это очень усложняет парралельное программирование.
  • Некоторые части PHP практически созданы для производства глючного кода:

    • json_decode возвращает null для невалидного ввода, при том, что null — абсолютно верный объект для декодируемого JSON'а. Эта функция абсолютно ненадёжна, если вы конечно не вызываете json_last_errorкаждый раз при её использовании.
    • array_search, strpos и другие похожие функции возвращают 0, если находят вхождение на нулевой позиции, но false если не находят его вообще.


    Дайте-ка мне чуть расширить последний пункт.

    В C, такие функции как strpos возвращают -1, если элемент не был найден. Если вы не проверите этот вариант и попытаетесь использовать результат в качестве индекса, вы попадёте в мусорную память и ваша программа упадёт. (Скорее всего. Это же C. Хрен его знает. Я уверен, что для этого как минимум есть инструменты.)

    В Python'е например эквивалентные методы .index бросят исключение, если элемент не найден. Если вы не проверите этот случай, ваша программа упадёт.

    В PHP эти функции возвращают false. Если вы используете FALSE как индекс, или сделаете с ним почти всё что угодно кроме сравнения с помощью ===, PHP спокойно сконвертирует его в 0 за вас. Ваша программа не упадёт; она вместо этого будет работать неправильно без предупреждения, если конечно вы не забудете вставить нужный шаблонный код вокруг каждого места где вы используете strpos и некоторые другие функции.

    Это плохо! Языки программирования — это инструменты; я предполагаю, что они будут работать вместе со мной. Здесь же, PHP поставил для меня хитрую ловушку, и должен быть осторожен даже с такими повседневными вещами как операции над строками и сравнения на равенство. PHP — минное поле.


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

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

Окей, вернёмся к фактам.

Операторы


  • == бесполезен
    • Он не транзитивен. "foo" == TRUE, и "foo" == 0… но, конечно же TRUE != 0.
    • == конвертирует в число, если возможно. Далее конвертирует в float'ы, если возможно. Получается, что большие шестнадцатиричные строки(например, хеши паролей) могут неожиданно быть равными, когда они не равны.
    • По тем же причинам, "6" == " 6", "4.2" == "4.20" и "133" == "0133". Но прошу заметьте, что 133 != 0133, потому что 0133 восьмеричное.
    • === сравнивает значения и тип… но не для объектов, где === истинно если оба операнда один и тот же объект! Для объектов, == сравнивает оба значения(для каждого аттрибута) и типы, что === делает для всех остальных типов. Чего.
  • Не лучше и сравнение:
    • Оно даже не согласовано: NULL < -1, и NULL == 0. Сортировка не детерменирована; она зависит от порядка в котором, алгоритм будет сравнивать элементы.
    • Операторы сравнения пытаются сортировать массивы, двумя разными способами: сначала по длине, затем по элементам. Вторая сортировка происходит, если у массивов одинаковое количество элементов, но разный набор ключей. По хорошему, такие массивы вообще нельзя сравнивать.
    • Объекты при сравнении всегда больше всего остального… кроме других объектов, которые не больше и не меньше.
    • Для более типобезопасного ==, у нас есть ===. Для типобезопасного < у нас… нет ничего. "123" < "0124", всегда, что бы вы не делали.
  • Вопреки вышеописанном безумию и чёткому отрицанию Perl'овых пар строковых и числовых операторов, PHP не перегружает +, + всегда сложение, а . всегда конкатенация.
  • [] оператор индекса может также быть записан как {}.
  • [] может быть применён к любой переменной, не только к строкам и массивам. Он возвращает null и не выдаёт предупреждение.
  • [] не может слайсить; он только возвращает отдельные элементы.
  • foo()[0] — синтаксическая ошибка. (Пофикшено в PHP 5.4.)
  • В отличие от (почти) всех остальных языков с похожим оператором, ?: левоассоциативен. Поэтому следующий код:
    $arg = 'T';
    $vehicle = ( ( $arg == 'B' ) ? 'bus' :
                 ( $arg == 'A' ) ? 'airplane' :
                 ( $arg == 'T' ) ? 'train' :
                 ( $arg == 'C' ) ? 'car' :
                 ( $arg == 'H' ) ? 'horse' :
                 'feet' );
    echo $vehicle;
    

    выведет horse.


Переменные


  • Нет никакого способа объявить переменную. Переменные, которые не существуют создаются со значением null при первом использовании.
  • Глобальные переменные должны быть объявлены с ключевым словом global перед использованием. Это естественное последствие предыдущего пункта, кроме того, что глобальная переменная даже не может быть прочитана без явного объявления — вместо этого PHP просто создаёт локальную переменную с таким же именем. Я не знаю другого языка с такими же проблемами с контекстом.
  • Нет ссылок. То, что в PHP называется ссылками, это на самом деле псевдонимы; это шаг назад, прям как ссылки в Perl, не существует передачи через объект, как в Python.
  • «Ссылочность» поражает переменные больше всего остального в языке. PHP — динамически типизированный, поэтому у переменных в общем случае нет типа… кроме ссылок, которые украшают определения функций, синтаксис переменных и присваивания. Как только переменная становится ссылкой(что может случится где угодно), она связана с этой ссылкой. Невозможно определить, случилось это или нет и для разыменования ссылки нужно полностью уничтожить переменную.
  • Окей, я соврал. Есть "SPL-типы", которые тоже поражают переменные:

    Выполнение $x = new SplBool(true); $x = "foo"; завершится неудачей. Смотрите-ка, выглядит как статическая типизация.
  • Можно получить ссылку на несуществующий ключ внутри неопределённой переменной(которая становится массивом). Использование несуществующего массива обычно выводит notice, но не в этом случае.
  • Константы определяются с помощью вызова функции; до этого они не существуют. (Возможно, это копия Perl-поведения use constant.)
  • Имена переменных чувствительны к регистру. Имена функций и классов нет. Включая имена методов, что делает camelCase не лучшим выбором именования.


Конструкции


  • array() и пару дюжин других конструкций — не функции. Сама по себе конструкция array не означает ничего, $func = "array"; $func(); не работает.
  • Распаковка массивов может быть выполнена операцией list($a, $b) = .... list() — синтаксис похожий на функцию так же как array. Я не знаю почему для этого был выделен отдельный синтаксис или почему было выбрано такое смутное имя.
  • (int) очевидно создан, чтобы выглядеть как C, но это отдельный токен; в языке нет никакого int. Попробуйте: var_dump(int) не только не работает, но и выбрасывает parse error, потому что аргументы выглядит как оператор приведения типа.
  • (integer) — синоним (int). Ещё есть (bool)/(boolean) и (float)/(double)/(real).
  • Есть оператор (array) для приведения к массиву и оператор (object) для приведения к объекту. Звучит безумно, но у них есть применение: вы можете использовать (array) для создания функции с аргументом, который может списком либо одним его элементом, чтобы работать с ним одинаково. Кроме того, что вы не можете сделать это надёжно, потому что если кто-то передаст отдельный объект, приведение его к массиву выдаст массив состоящий из аттрибутов объекта. (Приведение к объекту выполняет обратную операцию.)
  • include() и прочие это просто C-шный #include: она дампит другой исходный файл в ваш. Нет системы модулей, даже для PHP-кода.
  • Вложенных классов и функций не существует. Только глобальные. include() файла дампит переменные из этого файла в текущий контекст функции (и даёт файлу доступ к вашим переменным), но классы и функции дампятся в глобальный контекст.
  • Добавление в массив выполняется $foo[] = $bar.
  • echo — выражение, а не функция.
  • empty($var) настолько не функция, что что угодно кроме переменной, как например empty($var || $var2) приводит к parse error'у. Почему вообще парсеру нужно что-то знать о empty?
  • Существует избыточный синтаксис для блоков: if (...): ... endif; и др.


Обработка ошибок


  • Ещё один уникальный оператор PHP @(на самом деле взятый из DOS) заглушает ошибки.
  • У ошибок PHP нет стэктрэйсов. Вы должны установить обработчик, чтобы их генерировать. (Но вы не можете для fatal error'ов — см. ниже.)
  • Parse error'ы PHP часто просто выплёвывают состояние парсера и больше ничего, жутко усложняя отладку, если вы где-то забыли кавычку.
  • Парсер PHP внутри ссылается на :: как на T_PAAMAYIM_NEKUDOTAYIM, и на << как на T_SL. Я сказал «внутри», но как указано выше это то, что показывается программисту, когда :: или << встречается в неверном месте.
  • Большая часть обработки ошибок состоит в выводе строчки в лог сервера, который никто не читает и не интересуется.
  • Уровень E_STRICT как раз то, что нужно, но похоже он на самом деле предотвращает немногое и нет документации, что он на самом деле делает.
  • E_ALL включает все категории ошибок — кроме E_STRICT.
  • Жутко много противоречий о том, что разрешено, а что нет. Я не знаю, как E_STRICT к этому применяется, но следующее делать можно:
    • пытаться получить доступ к несуществующему свойству объекта, типа $foo->x.(warning)
    • использовать переменной как имени функции, или имени переменной, или имени класса.(без сообщений)
    • пытаться использовать неопределённую константу.(notice)
    • пытаться получить доступ свойство чего-нибудь, не являющегося объектом.(notice)
    • пытаться использовать имя переменной, которая не существует.(notice)
    • 2 < "foo" (без сообщений)
    • foreach (2 as $foo); (warning)

    А это нельзя:
    • пытаться получить доступ к несуществующей константе класса, типа $foo::x. (fatal error)
    • использовать константной строки как имени функции, имени переменной или имени класса. (parse error)
    • пытаться вызвать неопределённую функцию. (fatal error)
    • оставлять точку с запятой в конце блока или файла. (parse error)
    • использовать list и разные другие квазиконструкции как названия методов. (parse error)
    • индексировать возвращаемое функцией значение, типа foo()[0]. (parse error, пофикшено в 5.4, см. выше)


    Во всём этом списке есть ещё несколько хороших примеров странных parse error'ов.
  • Метод __toString не может бросать исключения. Если вы попробуете PHP… эм, бросит исключение. (На самом деле fatal error, который снова в отличие от всех остальных можно передавать.)
  • PHP-ошибки и PHP-исключения абсолютно разные существа. Они, похоже, не могут взаимодействовать никак.
    • PHP-ошибки (внутренние и вызванные через trigger_error) не могут быть словлены блоком try/catch.
    • Точно так же, исключения не приводят к вызову обработчиков установленных через set_error_handler.
    • Вместо этого, есть отдельная функция set_exception_handler которая обрабатывает не пойманные исключения, потому что обернуть входную точку вашей программы в блок try невозможно в модели mod_php.
    • Fatal error'ы (типа new ClassDoesntExist()) не могут быть пойманы ничем. Многие вполне невинные вещи бросают fatal error'ы, принудительно завершая вашу программу по сомнительным причинам. Shutdown-функции всё ещё вызываются, но они не могу получить стэктрэйс (потому что выполняются на верхнем уровне), и в них не так просто определить завершилась ли ваша программа с ошибкой или выполнилась до конца.
  • Нет конструкции finally, создание wrapper-кода(установил обработчик, выполнил код, убрал обработчик; манкипатч, проверил, разманкипатчил) утомительно и сложно для написания. Несмотря на то, что объектная модель и исключения в многом скопированы из Java, это было сделано намеренно, потому что finally «не имеет большого смысла в контексте PHP». Да ну?


Функции


  • Вызовы функций явно достаточно дорогие.
  • Некоторые встроенные функции взаимодействуют с функциями возвращающими ссылки, скажем так, странно.
  • Как я замечал ранее, некоторые вещи, похожие на функции или похожие на то, что должно быть функцией на самом деле конструкции языка, поэтому всё, что работает с функциями не будет работать с ними.
  • Аргументы функций могут иметь «type hint'ы», это попросту статическая типизация. Но вы не можете требовать от аргумента быть int или string или object или другим примитивным типом, хотя каждая встроенная функция использует эти типы, наверное потому что int в PHP не существует.(про (int) см. выше) Вы также не можете использовать специальные псевдо-типы используемые повсеместно во встроенных функциях mixed, number или callback.
    • В результате, следующий код:
      function foo(string $s) {}          
      foo("hello world");
      

      Выводит ошибку:
      PHP Catchable fatal error:  Argument 1 passed to foo() must be an instance of string, string given, called in...
      
    • Вы можете заметить, что указанного «type hint'а» не должно здесь быть. В этой программе нет класса string. Если вы воспользуетесь ReflectionParameter::getClass(), чтобы исследовать type hint динамически, он упрётся в несуществующий класс, делая невозможным получение имени класса.
    • Type hint не может быть применён к возвращаемому функцией значению.
  • Передача аргументов функции в другую функцию(dispatch, не такая уж и редкость) делается через call_user_func_array('другая функция', func_get_args()). Но func_get_args выбрасывает fatal error в рантайме, жалуясь, чтом func_get_args не может быть параметром функции. Как и почему такая ошибка вообще существует? (Пофикшено в PHP 5.3.)
  • Замыкания требует явного указания всех замыкаемых переменных. Почему интерпретатор сам не может это определить? Калечит всю фичу. (Окей, потому что любое использование переменной переменной, абсолютно любое, создаёт её, если обратное явно не указано.)
  • Замыкаемые переменные «передаются» с той же семантикой как остальные аргументы функции. Вот так, массивы, строки и пр. будут переданы по значению. Если не используете &.
  • Т.к. замыкаемые переменные фактически автоматически передаваемые аргументы и нет вложенных контекстов, замыкание не может использовать private-методы, даже если оно определено внутри класса. (Возможно пофикшено в 5.4? Не уверен.)
  • Нет именованных параметров функций. На самом деле явно отклонено разработчиками, потому что «ведёт к беспорядочному коду».
  • Аргументы со значениями по умолчанию могут быть перед аргументами без них, не смотря на то, что в документации указано, что это странно и бесполезно. (Зачем тогда это позволять?)
  • Лишние аргументы игнорируются(кроме лишних аргументов встроенных функций, они вызывают ошибку). Недостающие аргументы предполагаются равными null.
  • Функции с переменным числом аргументов требуют возни с func_num_args, func_get_arg и func_get_args. Для этого нет синтаксиса.


ООП


  • Функциональные части PHP сделаны как в C, а объектные как в Java. Даже не могу сказать, как меня это раздражает. Я не нашёл ни одной глобальной функции с заглавной буквой в имени, в то время как важные встроенные классы используют camelCase для имён методов и аксессоры getFoo в Java-стиле. Это динамический язык, верно? Perl, Python и Ruby: у каждого из этих языков есть какуя-либо концепция доступа к «свойству» из кода; у PHP же только неуклюжий __get и прочие. Система классов создана вокруг более низкоуровневого языка Java, который естественно и намеренно более ограничен, чем языки-современники PHP, я сбит с толку.
  • Классы не объекты. Любой метакод вынужден ссылаться на них по имени, как в случае с функциями.
  • Встроенные типы не объекты и (не как в Perl) никоим образом не могут быть представлены ввиде объектов.
  • instanceof оператор, несмотря на то, что классы были добавлены достаточно поздно и что большая часть языка построена на функциях и функционном синтаксисе. Влияние Java? Классы не first-class объекты?
    • Но существует функция is_a. С необязательным аргументом указывающим разрешать ли объекту быть строкой, содержащей имя класса.
    • get_class тоже функция; нет оператора typeof. Также как и оператора is_subclass_of.
    • Тем не менее, instanceof не работает для встроенных типов (снова, int не существует). Для этого случая вам нужен is_int и пр.
    • И ещё правая часть должна быть переменной или литеральной строкой; не может быть выражением. Это приводит к… parse error'у.
  • clone — оператор?!
  • ООП-дизайн представляет из себя жуткую мешанину Java и Perl.
  • Синтаксис аттрибутов объектов $obj->foo, а аттрибутов классов $obj::foo. Я не в курсе есть ли другой язык в котором сделано так же или какую пользу это приносит.
  • Также метод экземпляра может быть вызван статически (Class::method). Такой вызов сделать в другом методе(пер. или даже другом классе) трактуется как обычный вызов метода в текущем $this.
  • new, private, public, protected, static и пр. Хотели привлечь Java-разработчиков? Я в курсе, что это дело вкуса, но я не понимаю зачем это обязательно в динамическом языке — это нужно в C++ в основном для компиляции и разрешения имён во время компиляции.
  • Подклассы не могут перегружать private-методы. Перегруженные публичные методы подкласса даже не видят private-методы, кроме того, что не могут вызывать. Создаёт проблемы, например, при написании тест-моков.
  • Методы не могут называться, например «list», потому что list() — специальный синтаксис(не функция) и парсер начинает путаться. Для такой неоднозначности нет никакой причины, и всё работает при динамической модификации(пер. monkeypatching).($foo->list() не приводит к ошибке синтаксиса.)
  • Если при вычислении аргументов конструктора бросается исключение(например new Foo(bar()) и bar() бросает исключение), конструктор не будет вызван, а деструктор будет.(Пофикшено в PHP 5.3.)
  • Исключения в __autoload и деструкторах вызывают fatal error.
  • Нет никаких конструкторов и деструкторов. __construct — это инициализатор, как __init__ в Python. Нет метода при вызове, которого выделится память и будет создан объект.
  • Нет инициализатора по умолчанию. Если суперкласс не определяет собственный __construct, Вызов parent::__construct() приводит к fatal error'у.
  • ООП также вводит интерфейс итераторов, который учитывают некоторые части языка(типа for...as), но ни одна встроенная сущность(такая как массивы) этот интерфейс на самом деле не реализует. Если вам нужен итератор массива, вам приходится оборачивать его в ArrayIterator. Нет встроенной поддержки сцепления, slice'инга и чего-либо ещё для работы с итераторами, как «first class»-объектами.
  • Классы могут перегрузить то, как они конвертируются в строки и как они ведут себя будучи вызванными, но не как они конвертируется в числа и другие встроенные типы.
  • Есть конверсия в строки для Строк, чисел и массивов; язык на это очень полагается. Функции и классы тоже строки. Тем не менее конверсия встроенного или определённого пользователем объекта(и даже замыкания) в строку вызывает ошибку, если объект не определяет __toString. Даже echo становится потенциально склонен к ошибке.
  • Нет перегрузки сравнения и последовательности(пер. ordering).
  • Статические переменные внутри методов экземпляра глобальны; одно значение на все экземпляры класса.


Стандартная библиотека


Философия стандартной библиотеки Perl «some assembly required»(пер. возможный перевод «понадобится некоторая сборка»), Python — «батарейки в комплекте», PHP — «раковина, но канадская с подписью C на обоих кранах».

Общие замечания


  • Нет системы модулей. Вы можете компилировать расширения PHP, но какие из них загружать указывается в php.ini, и у вас только две опции: расширение существует(и вносит своё содержимое в глобальное пространство имён) или нет.
  • Стандартная библиотека не разбита на namespace'ы, поскольку они были добавлены недавно. В глобальном пространстве имён тысячи функций.
  • Части библиотеки дико противоречат друг другу:
    • Подчеркивание против его отсутствия: strpos/str_rot13, php_uname/phpversion, base64_encode/urlencode, gettype/get_class
    • «to» против 2: ascii2ebcdic, bin2hex, deg2rad, strtolower, strtotime
    • объект+действие против действие+объект: base64_decode, str_shuffle, var_dump versus create_function, recode_string
    • Порядок аргументов: array_filter($input, $callback) против array_map($callback, $input), strpos($haystack, $needle) против array_search($needle, $haystack)
    • Путаница с префиксами: usleep против microtime
    • Варьируется положение i в именах функций, нечуствительных к регистру
    • Около половины имен функций работы с массивами начинаются с array_. Другая половина нет.
  • Раковина. Библиотека включает:
    • Байндинги для ImageMagick, байндинги для GraphicsMagick(это форк ImageMagick), и пригорошню функций для чтения EXIF-данных(что и так умеет делать ImageMagick).
    • Функции для парсинга bbcode'а, специального маркапа используемого горсткой некоторых форумных пакетов.
    • Слишком много разных XML-пакетов. DOM(объектно-ориентированный), DOM XML(не объектно-ориентированный), libxml, SimpleXML, «XML Parser», XMLReader/XMLWriter и ещё полдюжины акронимов, которые я не могу разобрать. Между ними точно есть какая-то разница, идите и узнайте её сами.
    • Байндинги для двух отдельных процессоров кредитных карт: SPPLUS и MCVE. Зачем?
    • Три способа доступа к базе MySQL: mysql, mysqli и абстракция PDO.


Влияние C


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

  • «Выходные» параметры, не смотря на то, что PHP вполне способен возвращать специальные хэши или несколько аргументов без особых усилий.
  • Как минимумум дюжина функций для получения последней ошибки определённой подсистемы(см. ниже), хотя исключения существуют в PHP уже восемь лет.
  • Бородавки типа mysql_real_escape_string, несмотря на то, что у неё такие же аргументы как и у сломанной mysql_escape_string, просто потому что это часть MySQL C API.
  • Глобальное поведение для неглобального функционала (например MySQL). Использование нескольких подключений MySQL требует передачи дескриптора подключения в каждый вызов функции.
  • Врапперы очень, очень и очень тонкие. Например вызов dba_nextkey без вызова dba_firstkey упадёт с segfault'ом.
  • Набор функций ctype_*(типа ctype_alnum) называется в соответствии C-функциям определения класса символа с похожими именами, вместо того, чтобы называться, например isupper.


Обобщения


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

Как сортировать в обратном порядке? В Perl, вы можете сделать sort {$b <=> $a}. В Python .sort(reverse=True). В PHP, это отдельная функция rsort().

  • Функции получения C-ошибки: curl_error, json_last_error, openssl_error_string, imap_errors, mysql_error, xml_get_error_code, bzerror, date_get_last_errors и другие.
  • Функции сортировки: array_multisort, arsort, ksort, krsort, natsort, natcasesort, sort, rsort, uasort, uksort, usort
  • Функции для текстового поиска: ereg, eregi, mb_ereg, mb_eregi, preg_match, strstr, strchr, stristr, strrchr, srcpos, stripos, strrpos, strripos, mb_strpos, mb_strrpos плюс вариации, выполняющие подстановки.
  • К тому же куча бесполезных псевдонимов: strstr/strchr, is_int/is_integer/is_long, is_float/is_double, pos/current, sizeof/count, chop/rtrim, implode/join, die/exit, trigger_error/user_error...
  • scandir возврщает список файлов в текущей директории. Вместо того, чтобы возвращать сначала директории(что могло бы быть полезно), функция возращает их в алфавитном порядке. Необязательный аргумент позволяет получить их в обратном алфавитном порядке. Очевидно, функций сортировки было недостаточно.
  • str_split разбивает строку на равные по длине части. chunk_split разбивает строку на части одинаковой длины и объединяет их через разделитель.
  • Для каждого формата архивов используется отдельный набор функций. Всего шесть груп таких функций, с разным API, для bzip2, LZF, phar, rar, zip и gzip/zlib.
  • Т.к. вызов функции с массивом аргументов настолько неудобен(call_user_func_array), есть несколько пар функций типа printf/vprintf и sprintf/vsprintf. Они делают одно и то же только одна принимает аргументы, а другая массив аргументов.


Текст


  • preg_replace с флагом /e(eval) выполняет подстановку соответствий на строку подстановки, затем eval'ит эту строку.
  • strtok очевидно создана по образу C-функции, которая считается неудачной по разным причинам. Неважно, что PHP мог бы легко возвращать массив(в C это не так просто), и что хак, используемый strtok(3)(модификация строки на месте) в PHP не используется.
  • parse_str парсит строку GET-запроса, не указывая этого в имени. Также она ведёт себя как register_globals и дампит запрос в виде переменных в локальный контекст, если вы не передадите ей массив для наполнения. (Она, конечно, ничего не возвращает.)
  • explode отказывается разбивать с пустым разделителем. Любая другая реализация разбиения строки где угодно воспринимает это как разбиение посимвольно; в PHP для этого отдельная функция, непонятно названная str_split и описанная как «конвертирующая строку в массив».
  • Для форматирования дат, используется strftime, которая ведётся себя как C API и учитывает локаль. Ещё есть date с абсолютно другим синтаксисом и работающая только с английским.
  • "gzgetss — Получить строку из указателя на gz-файл и вырезать HTML-тэги." До смерти хочу узнать какие обстоятельства привели к концепции этой функции.
  • mbstring
    • Всё дело о «много-байтовости», в то время как проблема в кодировках.
    • Работает с обычными строками. Использует одну глобальную кодировку «по умолчанию». Некоторые функции позволяют указание кодировки, но она применяется ко всем аргументам и возвращаемому значению.
    • Предоставляет функции ereg_*, но они устарели. Функциям preg_* не повезло, тем не менее они могут понимать UTF-8, если скормить им кое-какие специфические флаги PCRE.


Система и reflection


  • Вообще целая куча функций стирает грань между текстом и переменными. compact и extract — только вершина айсберга.
  • Существует несколько способов для динамики в PHP, и с первого взгляда нет никакой заметной разницы или опредёлнных приемуществ. classkit позволяет модифицировать ползовательские классы, runkit заменяет classkit и позволяет модифицироет что угодно пользовательское; Reflection*-классы позволяет инспектировать большинство частей языка; очень много функций для работы со свойствами функций и классов. Эти подсистемы независимы, связаны, избыточны?
  • get_class($obj) возвращает имя класса объекта. get_class() возвращает имя класса, в котором вызвана функция. Принимая это во внимание, функция делает две абсолютно разные вещи: get_class(null)… ведёт себя так же, как get_class(). Поэтому вы не можете доверять ей при передаче произвольного объекта. Сюрприз!
  • Классы stream_* позволяют реализовывать пользовательские потоковые объекты и прочие встроенные файловые сущности. «tell» не может быть реализован по внутренним причинам. (К тому же в эту систему вовлечена цела ГОРА функций.)
  • register_tick_function принимает объект замыкания. unregister_tick_function нет; вместо этого она бросает ошибку, жалуясь, что замыкание не может быть сконвертировано в строку.
  • php_uname сообщает о текущей OC. Не в том случае, если PHP не может сказать, где он выполняется; тогда он сообщает на какой ОС он был собран. Произошло ли это не сообщается.
  • fork и exec не встроены. Они идут с расширением pcntl, но оно не включено по умолчанию. popen не предоствляет pid.
  • session_decode читает произвольную строку сессии, но работает только если уже есть активная сессия. И дампит результат в $_SESSION, вместо того, чтобы его возвращать.


Разное


  • curl_multi_exec не изменяет curl_errno в случае ошибки, но изменяет curl_error.
  • Аргументы mktime идут в следующем порядке: час, минута, секунда, месяц, день, год.


Манипуляция данными


Программы ничто иное кроме как большие машины поглощающие данные и выплёвывающие больше данных. Очень много языков созданы вокруг типов данных, которыми они манипулируют, от awk до Prolog и C. Если язык не может обрабатывать данные, он не может ничего.

Числа


  • Целые — знаковые и 32-битные на 32-битных платформах. В отличие от современников PHP, в нём нет автоматической конверсии в большое целое. Так что ваша математика может работать по разному в зависимости от архитектуры процессора. Единственная альтернатива использовать функции обёртки GMP или BC. (Разработчики предложили добавить новый отдельный 64-битный тип. С ума сошли.)
  • PHP поддерживает восьмеричный синтаксис с ведущим 0, так что 012 будет числом десять. Однако, 08 будет числом ноль. 8(или 9) и остальные следующие цифры исчезают. 01c ошибка синтаксиса.
  • pi — функция. А ещё есть константа, M_PI.
  • Нет оператора возведения в степень, только функция pow.


Текст


  • Нет поддержики юникода. Надёжно работает только ASCII, честно. Есть вышеупомянутое расширение mbstring, но оно как бы не работает.
  • Это означает, что использование встроенных строковых функций с UTF-8-текстом создаёт риск их порчи.
  • Точно также нет концепции типа сравнения регистра вне ASCII. Несмотря на распространённость нечуствительных к регистру версий функций, ни одна из них не считает é равным É.
  • Ключи нельзя квотировать при интерполяции переменных, например "$foo['key']" — ошибка синтаксиса. Вы может не квотировать ключи(будет сгенерировано предупреждение) или использовать ${...}/{$...}.
  • "${foo[0]}" работает. "${foo[0][0]}" — синтаксическая ошибка. Работает, если внести $ внутрь фигурных скобок. Плохо скопированный синтаксис Perl(с абсолютно другой семантикой)?


Массивы


Ё моё.
  • Этот тип данных ведёт себя как список, упорядоченный хэш, упорядоченный набор, разреженный список и время от времени как их странные комбинации. Какая его эффективность? Каков будет расход памяти?(пер. анализ расхода памяти для массивов) Кто знает? У меня всё равно нет других вариантов.
  • => — не оператор. Это специальная конструкция, существующая только внутри конструкций array(...) и foreach.
  • Отрицательные индексы не работают, т.к. -1 точно такой же валидный ключ как и 0.
  • Несмотря на то, что это единственная структура данных языка, для неё нет короткого синтаксиса; array(...)это короткий синтаксис. (PHP 5.4 вводит «литералы», [...].)
  • Конструкция => базируется на Perl, который позвляет foo => 1 без квотирования(вот почему конструкция существует Perl; иначе вы можете просто использовать запятую.) В PHP вы не можете так сделать не получив предупреждение; PHP — единственный язык в своей нише, в котором нет проверенного способа создать хэш без квотирования строковых ключей.
  • Функции работы с массивами часто ведут себя смутно и противоречиво, потому что вынуждены оперировать списками, хэшами или возможно их комбинацией. Взять хотя бы array_diff, «вычисляющий разность массивов».
    $first  = array("foo" => 123, "bar" => 456);
    $second = array("foo" => 456, "bar" => 123);
    echo var_dump(array_diff($first, $second));
    

    Что будет делать этот код? Если array_diff воспринимает аргументы, как хэши тогда очевидно они разные; одинаковые ключи с разные значения. Если аргументы воспринимаются как списки, тогда они всё равно разные; значения идут в разном порядке.

    Фактически array_diff признаёт массивы равными, потому что он воспринимает их как наборы; она сравнивает только значения и игнорирует порядок.
  • В той же мере array_rand странно ведёт себя выбирая случайные ключи, что не так полезно в общем случае выбора из списка вариантов.
  • Несмотря на то, как сильно PHP-код полагается на сохранение порядка ключей:
    array("foo", "bar") != array("bar", "foo")
    array("foo" => 1, "bar" => 2) == array("bar" => 2, "foo" => 1)
    

    Оставляю читателю узнать, что случится если массивы смешанные. (Я сам не знаю.)
  • array_fill не может создавать массивы нулевой длины, вместо этого она выдаст предупреждение и вернёт false.
  • Все из (многих...) функций сортировки оперируют массивом на месте и ничего не возвращают. Нет способа создать отсортированную копию; вы вынуждены копировать массив сами, затем сортировать его и использовать.
  • Но array_reverse возвращает новый массив.
  • Список отсортированных сущностей и ключи поставленные в соответстивие значениям звучат как отличный способ обрабатывать аргументы функций, но это не так.


Немассивы


  • Стандартная библиотека включает «Quickhash», ООП-реализацию «специфических строго-типизированных классов» для создание хэшей. И на самом деле предоставляет четыре класса, каждый для работы с различными комбинациями типов ключей и значений. Непонятно, почему встроенная реализация массивов не может быть оптимизирована для этих очень частых случаев, и какова относительная эффективность «Quickhash».
  • Класс ArrayObject(реализующий пять различных интерфейсов) может оборачивать массив и позволяет ему вести себя как объект. Пользовательские классы могут реализовывать те же интерфейсы. Беда в том, что у класса жалкая горстка методов, половина которых не похожа на встроенные функции, к тому же встроенные функции не умеют оперировать ArrayObject'ом или другим похожим на массив классом.


Функции


  • Функции — не данные. Замыкания всё-таки объекты, но обычные функции нет. Вы даже не можете ссылаться на них по их прямым именам; var_dump(strstr) вызывает предупреждение и предполагает, что вы имели ввиду строковый литерал, "strstr". Нельзя отличить произвольную строку от «ссылки» на функцию.
  • create_function просто обёртка вокруг eval. Она создаёт функцию с обычным именем и устанавливает её глобально(поэтому эта функция никогда будет собрана сборщиком мусора — не используйте в цикле!). Она на самом деле ничего не знает а текущем контексте, поэтому это не замыкание. Имя содержит NUL-байт, поэтому такая функция никогда не конфликтует с обычными функциями(потому что PHP-парсер отказывает, если где угодно в файле есть NUL).
  • Если определить функцию __lambda_func, create_function сломается — на самом деле реализация создаёт через eval функцию с именем __lambda_func, затем внутренними методами переименовывает её. Если __lambda_func уже существует, первая часть процесса бросит fatal error.


Прочее


  • Инкремент (++) NULL'а выдаёт 1. Декремент (--) NULL'а выдаёт NULL. Более того декремент строки оставляет её неизменной.
  • Нет генераторов.


Web-фрэймворк


Выполнение


  • Один общий файл, php.ini контролирует огромную часть функционала PHP и вводит сложные правила относительно того, что и когда перегружается. PHP-приложение которое предполагает внедрение на произвольных машинах вынужден в любом случае заменять настройки, чтобы нормализовать окружение, в любом случая уничтожая полезность такой механики как php.ini.
  • По существу PHP выполняется как CGI. При каждой загрузке PHP-страницы всё приложение перекомпилируется и выполняется. Даже дев серверы для игрушечных Python-фрэймворков так себя не ведут.

    Это создало целый рынок «PHP-акселераторов», выполняющий компиляцию единожды, ускоряя PHP до уровня любого другого языка. Zend, компания, стоящая за PHP, сделала это частью своей бизнес-модели.
  • Достаточно долгое время PHP-ошибки по умолчанию шли на клиент — думаю, чтобы для помощи при разработке. Не думаю, что это до сих пор верно, но я всё ещё время от времени вижу mysql-ошибки, выпадающие наверху страницы.
  • PHP всё ещё полон странных «пасхальных яиц» типа выдачи логотипа PHP при передаче соответствующего аргумента в запросе.
    Кроме того, что это никак не связано с построением вашего приложения, это ещё и позволяет определить используете ли вы PHP(и возможно грубо определить какую версию), в не зависимости от того как много конфигурации в ваших mod_rewrite, FastCGI, обратном проксировании или Server:.
  • Пробелы вне тэгов <?php ... ?>, даже в библиотеках, считаются текстом и включаются в ответ(или приводят к ошибкам «headers already sent»). Популярный фикс — не указывать закрывающий ?>; PHP не жалуется и у вас нет завершающей новой строки в конце файла.


Внедрение


Внедрение часто упоминается как наибольшее преимущество PHP; сбросьте несколько файлов и всё. В самом деле это проще, чем выполнение целого процесса, как вы делали бы в Python, Ruby или Perl. Но PHP не даёт много другое.

Со своей стороны я за то, чтобы Web-приложения выполнялись как сервера приложений и запросы реверс-проксировались к ним. Это требует минимальных усилий и даёт множество выгод: вы можете управлять Web-сервером и приложением отдельно, вы может выполнять сколь угодно много или мало процессов приложения на сколь угодно большом количестве машин без дополнительных Web-серверов, вы можете выполнять приложение под собственным пользователем без усилий, вы можете использовать любой Web-сервер, вы можете остановить приложение, не прикасаясь к Web-серверу, вы можете выполнять бесшовное внедрение просто изменив точку проксировния и пр. Спаивать ваше приложение с Web-сервером абсурдно и больше нет причин так делать.

  • PHP естественно связан с Apache. Выполнение его по отдельности или с другим Web-сервером требует точно такой же(если не больше) возни как и в других языках.
  • php.ini применяется ко всем PHP-приложениям, выполняющимся на машине. Есть только один файл php.ini и он применяется глобально; если вы на shared-сервере и вам нужно его изменить, или вам нужно выполнять два приложения с различными настройками, тогда вам не повезло; вы должны применять набор всех нужных настроек из самого приложения через ini_set, конфигурационный файл Apache или .htaccess. Если можете. Вау, нужно проверить много мест, чтобы определить как же настройка получает своё значение.
  • Подобным образом нет простого способа «отделить» PHP-приложение и его зависимости от остальной системы. Выполняете два приложения, требующие разных версий библиотеки или даже самого PHP? Начните со сборки второй копии Apache.
  • Подход «сбрось несколько файлов» между прочим делает routing жуткой болью в заднице, также это значит что вы должны осторожно разрешать и запрещать доступ, потому что ваша иерархия URL'ов, также весь ваш код. Конфигурационные файлы и другие «partial'ы» требуют защитных проверок как в C, чтобы избежать их прямой загрузки. Шум контроля версий (типа .svn) должен быть также защищён. С mod_php всё в вашей файловой системе потенциальная входная точка; с сервером приложений, у вас одна входная точка, и только URL контролирует вызывается ли она.
  • Вы не можете бесшовно обновить несколько файлов выполняемых как CGI, если вы не хотите падений и неопределенных поведений, когда пользователи пинают ваш наполовину обновлённый сайт.
  • Не смотря на то, как просто настроить Apache для выполнения PHP, даже здесь вас поджидает несколько коварных ловушек. В то время, как документация PHP советует использовать SetHandler для запуска .php-файлов как PHP, AddHandler работает так же хорошо, и на самом деле Google выдаёт мне в два раза больше результатов для AddHandler. Собственно проблема.

    Когда вы используете AddHandler, вы указываете Apache, что «выполнение следующего как php» — один из возможных способов обработки .php-файлов. Но! Apache не одного и того же мнения о расширениях файлов, как каждый человек на планете. В нём есть поддержка, например, index.html.en, распознаваемого как HTML-файла на английском. Для Apache файл может иметь сколько угодно расширений одновременно.

    Представьте, что у вас есть форма загрузки файлов, которая выгружает файлы в публичную директорию. Чтобы быть уверенными, что никто не загрузит PHP-файл, вы просто проверяете, что расширение файлов не .php. Всё, что атакующий должен сделать это загрузить файл с именем foo.php.txt; загрузчик не увидит никаких проблем, но Apache будет распознавать файл как PHP, и он выполнится.

    Проблема не в том, что «используется исходное имя файла» или «надо было лучше валидировать»; проблема в том, что ваш Web-сервер, настроен выполнять любой старый код, на который он может наткнутся — именно это свойство делает PHP «простым для внедрения». CGI требовал +x, это хотя бы что-то, но PHP не требует даже этого. И это не теоретическая проблема; я нашёл несколько сайтов с этой проблемой.


Чего не хватает


Я предполагаю следующее с различными уровнями критичности для построения Web-приложения. Имело бы смысл в PHP, как языке, продаваемом как «Web-язык», реализовать что-нибудь из нижеуказанного.

  • Нет системы шаблонов. Есть сам PHP, не нет ничего что бы работало как большой интерполятор, вместо того, чтобы работать как программа.
  • Нет XSS-фильтра. Нет, «используй htmlspecialchars» — не XSS-фильтр. Вот это XSS-фильтр.
  • Нет CSRF-защиты. Вы должны делать её сами.
  • Нет обобщённого стандартного API для баз данных. Штуки, типа PDO, вынуждены оборачивать отдельные API для абстракции.
  • Нет routing'а. Ваш сайт выглядит так же как ваша файловая система. Многие разработчики обмануты, думая, что mod_rewrite (и весь остальной .htaccess) подходящая замена.
  • Нет аутентификации или авторизации.
  • Нет дев сервера.
  • Нет интерактивной отладки.
  • Нет явного механизма внедрения; только «скопируйте вот эти файлы на сервер».


Безопасность


В рамках языка


Плохая репутация безопасности PHP в основном связана с тем, что он принимает произвольные данные из одного языка и выдаёт их в другой. Это плохая мысль. "<script>" ничего не значит в SQL, но точно значит в HTML.

Ещё хуже становится от крика об «очистке входных данных». Это полностью неверно; нет в природе волшебной палочки, взмахнув которой вы делаете кусочек данных «чистым». Что нужно делать так это говорить на нужном языке: использовать placeholder'ы в SQL, использовать списки аргументов при создании процессов и пр.

  • PHP открыто поощряет «очистку»(пер. sanitizing): для этого есть целое расширение для фильтрации данных.
  • Все эти addslashes, stripslashes и прочая слэш-фигня — отвлекающий манёвр, который ничего не даёт.
  • Как я знаю, нет способа безопасно создать процесс. Можно ТОЛЬКО выполнить строку через шэл. Вы можете экранировать как сумасшедший и надеяться, что шэл по умолчанию использует верное экранирование; либо вручную делать pcntl_fork и pcntl_exec.
  • Две функции escapeshellcmd и escapeshellarg имеют почти одинаковые описания. Заметьте, что для Windows, escapeshellarg не работает (т.к. предполагает семантику Bourne shell), а escapeshellcmd просто заменяет пачку пунктуаций на пробелы, потому что никто не может понять экранирование Windows cmd (который может тихо упасть вне зависимости от того, что вы пытаетесь сделать).
  • Оригинальные встроенный MySQL-байндинги, до сих пор широко используемые, не могут создавать prepared statement'ы.


По сей день PHP-документация по SQL-инъекциям рекомендует сумасшедшие практики типа проверки типов, используя sprintf и is_numeric, везде вручную используя mysql_real_escape_string, или везде вручную используя addslashes (которая «может быть полезна»!). PDO или параметризация даже не упоминаются, кроме как в пользовательских комментариях. Я пожаловался на это конкретное место разработчикам PHP минимум два года назад. Да, разработчики были встревожены… страница не обновлена до сих пор.

Небезопасны по умолчанию


  • register_globals. Его выключили по умолчанию достаточно давно, и он пропал в PHP 5.4. Мне пофигу. Это помеха.
  • include разрешает HTTP URL'ы. Туда же.
  • Magic quotes. Так близки к безопасности по умолчанию, и всё же слишком далеки от правильного понимания самой концепции.


Ядро


Интерпретатор PHP сам по себе содержал просто очаровательные проблемы безопасности.

  • В 2007 в интерпретаторе была уязвимость переполнения целого. Фикс начался с if (size > INT_MAX) return NULL; и покатился по наклонной. (Для тех, кто не в курсе C: INT_MAX самое большое целое, которое может уместится в переменную, вообще. Я думаю, дальше вы поняли.)
  • Чуть позже, в PHP 5.3.7 умудрились включить функцию crypt(), которая фактически позволяла зайти кому угодно с каким угодно паролем.
  • PHP 5.4 уязвим к отказу в обслуживании, т.к. он берёт заголовок Content-Length (который кто угодно может установить в любое значение) и пытается создать массив переданного размера. Это плохая мысль.


Я мог бы раскопать ещё что-нибудь, но дело не в том, что есть X эксплойтов — в софте бывают баги, это случается, так или иначе. Их природа шокирует. Я ведь даже не ищу их; это то, что упало мне на порог в последние пару месяцев.

Заключение


Некоторые комментаторы справедливо указали, что у меня нет заключения. И да, у меня нет заключения. Если вы до сюда дочитали, я предполагаю, вы были согласны со мной с самого начала :)

Если вы знаете только PHP и вам любопытно научиться чему-то ещё, гляньте учебник по Python и попробуйте Flask для Web'а. (Я не большой фанат их языка шаблонов, но он делает своё дело.) Он разделяет части вашего приложения, но это всё ещё те же самые исходные части и они должны быть похожи на то, что вы видели до этого. Я, возможно, напишу настоящий пост об нём; ураганное введение в целый язык и web-стек — тема для другой статьи.

Позже и для больших проектов вы можете попробовать средне-уровневый Pyramid или Django, сложный монстр, хорошо подходящий для построения сайтов, похожих на сайт самого Django.

Если вы не разработчик, но всё равно читаете это по какой-либо причине, я не успокоюсь, пока все не планете не прочитают Learn Python The Hard Way.

Я не пробовал Ruby on Rails и его соперников, у Perl с его Catalyst'ом вроде есть ещё порох в пороховницах. Читайте, учитесь, создавайте, жгите.

Ссылки


Спасибо за вдохновение:

Tags:
Hubs:
+334
Comments 538
Comments Comments 538

Articles