PHP: фрактал плохого проектирования
Перевод статьи PHP: a fractal of bad design
От переводчика
Я неоднократно натыкался на эту статью и на её убогий перевод на Хабре. Да, она действительно из тех, которые tl; dr, так что я не осилил её с первого раза. И со второго. И с третьего. Но в итоге я её прочитал и понял, что она очень сильная. И теперь она есть на не-ломанном русском. Порой это сумбур, некоторые утверждения вперемешку. Тем не менее, многие примеры удивительного кода проверены мной. Подтвеждаю: PHP неадекватен :)
Предисловие
Я зла. Я жалуюсь на кучу всего. Неудивительно, что в мире технологий полно того, что мне не нравится. Программирование — очень молодая дисциплина, и никто из нас толком не знает, что делает. Если при этом вспомнить ещё и Закон Старджона, то будьте уверены: я найду на что жаловаться в течение ближайшей жизни.
Здесь речь о другом. PHP не просто неудобен в использовании, не подходит для моих целей, неоптимален или противоречит моей религии. Я могу рассказать обо всём хорошем в языках, которыми я стараюсь не пользоваться, и обо всём плохом в языках, которые мне нравятся. Давайте, спрашивайте! Будет интересный разговор.
PHP — единственное исключение. Практически каждая возможность PHP работает через задницу. Язык, каркас, экосистема — всё плохо. Это настолько систематично, что я не могу показать какую-то одну убийственную вещь. Каждый раз, пытаясь составить список моих нареканий, я застревала в этом глубинном поиске, обнаруживая всё более ужасающие детали. (Следовательно, это фрактал.)
PHP — это позорище, камень в мой огород. Он настолько неадекватен, но так хвалим каждым дилетантом, до сих пор не изучившим ничего другого, что это просто бесит. У него есть несколько искупающих качеств, но я бы предпочла забыть об их существовании.
И всё же я должна поделиться этим. Итак, вот она, моя последняя попытка.
Аналогия
Я просто жаловалась Мэл, чтобы объяснить, как я расстроена, и она настояла на том, чтобы я выложила это сюда.
Я не могу сказать, что не так с PHP, потому что... ладно. Допустим, у тебя есть, ну, ящик с инструментами. Нормально выглядит, внутри стандартный набор.
Ты достаёшь отвёртку, и понимаешь, что у неё нет ручки, зато три насадки. Ну, ладно, сейчас её применить некуда, но ты надеешься, что когда-нибудь она пригодится.
Ты достаёшь молоток, но, к твоему ужасу, обе его стороны предназначены для выдёргивания гвоздей. Не смотря на это, он всё ещё пригоден. Я имею в виду, что всё ещё можно забивать гвозди ударной частью, только держать его придётся боком.
Ты вытаскиваешь плоскогубцы, но у них нет зубчатых поверхностей; они ровные и гладкие. Это не очень удобно, но всё ещё позволяет закручивать болты.
И так далее. Всё в ящике странное и причудливое, но, возможно, не настолько, чтобы быть совсем бесполезным. И с этим набором нет какой-то конкретной проблемы; в нём есть все инструменты.
Теперь представь, что ты знаком с миллионом плотников, которые говорят тебе: «Эй, что не так с этими инструментами? Я всегда пользуюсь только ими, и всё нормально получается!» И плотники показывают тебе дом, который они построили, где все комнаты пятиугольные, а крыша вверх ногами. Ты стучишь в парадную дверь, а она вваливается внутрь, и на тебя кричат за то, что ты сломал дверь.
Вот что не так с PHP.
Основа
Я считаю, что следующие качества важны для создания продуктивного и полезного языка, и PHP дико кладёт на них. Если ты не согласишься, что они ключевые, то я представить не могу, что мы на чём-нибудь сойдёмся.
-
Язык должен быть предсказуемым. Это посредник, позволяющий выразить идеи человека и заставить компьютер выполнить их, поэтому критично понимание человеком правильности программы.
-
Язык должен быть согласованным. Похожие вещи должны похоже выглядеть, разные — различаться. Знание части языка должно помогать в изучении и понимании остального.
-
Язык должен быть кратким. Новые языки нужны, чтобы уменьшить шаблонность, присущую старым языкам. (Мы все могли бы писать машинный код.) Таким образом, язык должен бороться с появлением собственных шаблонов.
Не значит, что шаблонов вообще не должно быть. Например, try...catch — шаблон, пришедший на смену errno/errstr. И, т. к. ошибки всё-таки надо обрабатывать, приходится выбирать, какой шаблон предпочтительнее — прим. переводчика -
Язык должен быть надёжным. Языки — инструменты для решения проблем; они должны бороться со всеми проблемами, которые сами создают. «Нежданчики» же, напротив, очень отвлекают.
-
Язык должен быть отлаживаемым. Когда что-то не работает, программист должен это исправить, и для этого могут потребоваться все средства, которые язык может предоставить.
Следовательно, вот что я думаю:
-
PHP полон сюрпризов: mysql_real_escape_string, E_ALL
-
PHP не согласован: strpos, str_rot13
-
PHP необходимы шаблоны: проверка на ошибки при вызовах к API Си, ===
-
PHP чудной: ==, foreach ($foo as &$bar)
-
PHP непрозрачный: нет трассировки стека по умолчанию для фатальных ошибок, неудобно докладывать об ошибках
Я не могу предоставить по абзацу комментариев на каждую проблему, объясняя, почему она попала в эти категории: это будет бесконечно. Пусть читатель сам подумает.
Что лучше не писать в комментариях
Я знакома со многими аргументами в пользу PHP. Я слышала кучу очень общих контраргументов, придуманных только для того, чтобы немедленно прервать разговор. Не пишите их сюда, пожалуйста. :(
-
Не говорите мне, что хорошие разработчики могут писать код на любом языке или плохие разработчики бла-бла-бла. Это ни о чём. Хороший плотник может забить гвоздь и камнем, и молотком, но часто ли можно увидеть плотника, забивающего гвоздь камнем? Выбирать инструменты, которые работают лучше других, — это одна из способностей хорошего разработчика.
-
Не говорите мне, что разработчик должен запомнить тысячи странных исключений и нежданчиков. Да, это неизбежно в любой системе потому что компьютеры несовершенны. Но это не значит, что нет верхнего предела глупостей в системе. PHP — сплошные исключения, и если борьба с языком требует больше усилий, чем написание программы, это не норма. Мои инструменты не должны добавлять мне работы.
-
Не говорите мне «так работает API Си». Зачем, в самом деле, использовать язык высокого уровня, если всё, что он предоставляет, это реализация строк и непосредственные обёртки Си-функций? Просто пиши на Си! Вот, для этого даже есть CGI-библиотека.
-
Не говорите мне «вот что получается, когда делаешь странные вещи». Если две возможности существуют, кто-нибудь кода-нибудь найдёт причину использовать их вместе. И, опять же, это не Си; здесь нет спецификации; нет необходимости в «неопределённом поведении».
-
Не говорите мне, что Фейсбук и Википедия написаны на PHP. Я в курсе! Их также можно было бы написать на Brainfuck'e; умные люди, которые сделали эти проекты, могут преодолеть проблемы платформы. Для обычных разработчиков, которых мы знаем, время разработки может быть разделено пополам или удвоено, если писать эти продукты на другом языке; сам по себе факт, что эти продукты написаны на PHP, ничего не значит.
-
В идеале, не говорите мне ничего! Это писалось на одном дыхании; если этот список не сможет задеть твоё мнение о PHP, ничто не сможет, так что прекрати спорить с людьми в интернете и сделай крутой сайт за рекордное время, чтобы доказать, что я не права :)
Взгляд со стороны: я обожаааю Питон. И я с радостью прожужжу тебе все уши, жалуясь на него, если ты действительно хочешь этого. Я не утверждаю, что он идеален; я просто взвесила все достоинства и недостатки и решила, что он лучше всего подходит для того, что я хочу делать.
И я не встречала PHP-разработчика который может сделать то же самое. Но я видела многих, кто быстро извинялся за всё, что делает PHP. Это ужасающий подход.
PHP
Базовый язык
CPAN — «стандартная библиотека Perl'а». Это не много говорит о стандартной библиотеке Perl'а, но показывает, что монолитное ядро способно на великие вещи.
Философия
-
PHP был изначально разработан исключительно для не-программистов (и, читая между строк, не-программ); он не в полной мере отошёл от корней. Вот цитата из документации PHP 2.0 касательно преобразования типов при использовании оператора + и аналогичных:
Как только у вас появляются отдельные операторы для каждого типа, язык становится намного сложнее. Представим, что нельзя использовать «==» для строк, нужно использовать eq. Я не вижу причины делать так, особенно для чего-то вроде PHP, где большая часть скриптов должна быть простой и в большинстве случаев написана не‑программистами, которым нужен язык с базовым логичным синтаксисом и небольшим порогом вхождения.
-
PHP разработан так, чтобы продолжать работать любой ценой. Если можно делать что-нибудь бессмысленное или прервать выполнение ошибкой, он будет делать бессмысленное. Что-нибудь лучше чем ничего.
-
Нет чёткой философии проектирования. Ранний PHP вдохновлён Perl'ом; большая стандартная библиотека с «выходными» параметрами — из Си; ОО части напоминают Си++ и Java.
-
PHP взял многое от других языков, но до сих пор остаётся непонятным для тех, кто знает эти языки. (int) выглядит в стиле Си, но int не существует. Области видимости обозначаются знаком \. Новый синтаксис массивов выглядит как [key => value], чего нет ни в одном языке в качестве объявления хэш-литералов.
-
Слабая типизация (т.е. автоматическое приведение к строкам / числам и др.) настолько сложна, что любые жалкие попытки программиста сохраняются, даже если они того не стоят.
-
Маленькая новая плюшка реализуется как новый синтаксис; и наоборот, многое делается функциями или тем, что выглядит как функции. Исключение — поддержка классов, ради которой была создана прорва операторов и ключевых слов.
-
Для части проблем, перечисленных на этой странице, есть решения — для тех, кто готов платить Zend'у за исправления в их открытом языке.
-
Есть посмотреть поближе, можно найти много странностей. Рассмотрим этот код, взятый откуда-то из документации.
@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.
Я не могу сказать, как этот безобидный вызов вёл бы себя, не консультируясь с флагами, с которыми собран PHP, конфигурацией сервера и конфигурацией программы. И всё это — встроенное поведение.
-
-
Язык полон глобальных и неявных состояний. mbstring использует глобальную кодировку. func_get_arg и компания выглядят как обычные функции, но работают с аргументами функции, выполняющейся в данный момент. Для обработки ошибок/исключений есть глобальные функции по умолчанию. register_tick_function задаёт глобальную функцию, которая будет выполняться при каждом тике — ШТА?!
-
Нет никакой поддержки потоков. (Неудивительно, учитывая вышесказанное.) Вместе с отсутствием встроенной функции fork (о ней — ниже), параллельное выполнение становится удивительно сложным.
-
Некоторые части PHP спроектированы для создания багов в коде.
-
json_decode возвращает null при ошибке декодирования, но также null может быть корректным JSON-объектом — эта функция абсолютно ненадёжна, если не звать json_last_error каждый раз, когда пользуешься ей.
-
array_search, strpos, и похожие функции возвращают 0, если они нашли «иголку» в нулевой позиции, но false если совсем не нашли.
Позвольте мне немного распространиться о последнем.
В Си функции наподобие strpos возвращают -1, если искомая подстрока не найдена. Если не проверить и попробовать использовать это значение в качестве индекса, попадёшь в нежелательную область памяти и программе снесёт башню. (Возможно. Это же Си. Кто, блин, знает. Я уверена, что для этого, по крайней мере, есть инструменты.)
Скажем, в Питоне, эквивалентные .index-методы вызовут исключение, если искомое не найдено. Если не поймать его, программа вывалится.
В PHP эти функции возвращают false. Если использовать false в качестве индекса, или сделать что-нибудь другое, кроме сравнения оператором «===», PHP любезно приведёт false к 0. Выполнение программы не прервётся; вместо этого будут без предупреждения выполнены неправильные действия, кроме случаев, когда все вызовы функций наподобие strpos обёрнуты в специальный шаблонный код.
Это плохо! Языки программирования — это инструменты; предполагается, что они работают вместе со мной. Но в PHP есть незаметная ловушка, в которую он меня тащит, и приходится неусыпно следить даже над такими земными вещами, как операции со строками и сравнение. PHP — это минное поле.
-
Во многих местах я слышала кучу замечательных историй об интерпретаторе PHP и его разработчиках. В частности от людей, которые работали над ядром PHP, отлаживали ядро PHP, общались с разработчиками ядра. Ни одной истории, которая говорила бы в их пользу.
Повторюсь: PHP — это сообщество любителей. Очень немногие из его разработчиков знали, что делают. (О, дорогой читатель, ты, конечно же, — редкое исключение!) Те, кто дорос до перехода в другой проект, снизили компетентность проекта в целом. Вот в чём самая большая проблема PHP: это слепой, ведущий слепого.
Ладно, назад, к фактам.
Операторы
-
== бесполезен.
-
Он не транзитивен. "foo" == TRUE, "foo" == 0… но, конечно же, TRUE != 0.
-
== приводит строки к числам, когда это возможно (123 == "123foo"… хотя "123" != "123foo"), это значит, что, когда возможно, также выполняется приведение к float. Значит, большие шестнадцатеричные строки (как, скажем, хэши паролей) могут случайно вернуть при сравнении true, когда они не равны. Даже JavaScript так не делает.
-
По той же причине "6" == " 6", "4.2" == "4.20" и "133" == "0133". Стоит заметить, что 133 != 0133, т. к. число 0133 восьмеричное. но "0x10" == "16" и "1e3" == "1000"!
-
=== сравнивает значения и тип… за исключением объектов, где === будет true, только если оба операнда — один и тот же объект! Для объектов == сравнивает и значения всех полей, и типы, т. е. делает то, что === делает для любого другого типа. Так-то.
-
-
Со сравнением дело обстоит не лучше.
-
Это никак не согласуется: NULL < -1, и NULL == 0. Таким образом, сортировка не детерминирована: результат зависит от порядка, в котором алгоритм сравнивает сортируемые элементы.
-
Операторы сравнения пытаются сравнивать массивы двумя способами: сначала по размеру, потом по элементам. Если в них одинаковое количество элементов, но разные наборы ключей, сравнение невозможно.
-
При сравнении объекты оказываются больше чего-либо другого... кроме других объектов, которые не больше и не меньше друг друга.
-
Более безопасный с точки зрения типов вариант «==» — это «===». Более безопасный вариант «<»… не существует. "123" < "0124" всегда, что ни делай. Приведение типов также не поможет.
-
-
Несмотря на сумасшествие выше и явное отторжение Перловых пар строковых и числовых операторов, PHP не перегружает «+». «+» — это всегда сложение, «.» — это всегда конкатенация.
-
Индексный оператор [] также можно записать как {}.
никогда не встречал. Если бы встретил, сбило бы с толку. — прим. переводчика -
[] можно использовать не только на строках и массивах, но и на любых других переменных. Вернёт null без предупреждения.
-
[] не может выбрать срез массива, только один элемент.
для этого есть функция array_slice(), хотя внутри квадратных скобок это выглядело бы лаконичнее — прим. переводчика -
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.
Сначала я впал в ступор. Как выяснилось, в качестве третьего операнда (т. е. отрицательной ветви тернарного оператора ?:) выступает не всё последующее выражение, а результат сравнения. Для первого оператора это ( $arg == 'B' ), что эквивалентно false. А ещё в буковках, обозначающих виды транспорта, я вижу слово batch — прим. переводчика
Переменные
-
Нет способа объявить переменную. Когда происходит первое обращение к несуществующей переменной, она создаётся со значением null.
с переменными в PHP вообще плохо. Чтобы узнать, существует ли глобальная переменная, нужно вызвать array_key_exists('var_name', $GLOBALS); для поиска локальной переменной придётся звать ту же функцию, но вторым аргументом будет get_defined_vars() — прим. переводчика -
Для обращения к глобальным переменным нужно сначала перечислить их после ключевого слова global. Это следствие предыдущего пункта. И это вполне логично, если не учитывать, что глобальные переменные нельзя даже читать без явного объявления — вместо этого PHP тихо создаст одноимённую локальную переменную. Я не знаю другого языка с такими нюансами видимости.
-
Нет ссылок. То, что в PHP называется ссылкой, на самом деле псевдоним; нет того, что было бы шагом назад, как ссылки в Perl’е, и это не подобие передачи по объекту в Python’е.
-
«Ссылочность» заражает переменную как ничто другое в языке. PHP динамически типизирован, так что, в общем, у переменной нет типа… за исключением ссылок, которые «украшают» определения функций, синтаксис переменных и присваивание. Как только переменная стала ссылкой (а это может произойти где угодно), она остаётся ссылкой. Нет явного способа определить ссылку, для отвязывания придётся удалить переменную.
Ссылки в PHP напоминают жёсткие ссылки в файловой системе: и оригинал, и ссылка на него на самом деле указывают на одну и ту же область памяти. Так что взятие адреса в PHP ведёт к созданию идентичной переменной, и ни одна из этих переменных не является более ссылкой, чем другая — прим. переводчика -
Ладно, я вру. Есть библиотека «SPL types», которая также заражает переменные: $x = new SplBool(true); $x = "foo"; вызовет ошибку. Всё как при статической типизации.
Не входит в стандартный набор инструментов, считается экспериментальной, последнее обновление датируется 2012 годом. Даже такой простой вещи как статическая типизация в PHP скорее нет, чем есть — прим. переводчика. -
Можно взять ссылку на несуществующий ключ в пределах неопределённой переменной (которая станет массивом с единственным элементом-ссылкой на null). Обычно использование несуществующего массива вызывает предупреждение, но здесь — нет.
Код $a = &$undef['lol']; var_dump($undef); выводит array(1) { ["lol"]=> &NULL }. Кажется, это одна из немногих ситуаций, когда по ссылке видно, что это ссылка. Какая-нибудь там ссылка на строку отобразится var_dump'ом просто как строка. Об этом говорили парой пунктов раньше — прим. переводчика -
Константа объявляется вызовом функции, которая принимает строку; до этого она не существует. (Скорее всего, это копия use constant из Perl'а.)
Тут есть нюанс: просто_текст будет расценен PHP как несуществующая константа. Это вызовет предупреждение, а в качестве значения константы будет использовано её имя — "просто_текст" в нашем случае. «Программисты», у которых отключены ошибки уровня E_STRICT, этого не увидят, поэтому спокойно смогут вместо define('DB_LOGIN', 'root'); писать define(DB_LOGIN, 'root'); (и даже define(DB_LOGIN, root);), а вместо $arr['key'] — $arr[key]. Эта тема стоит отдельного benchmark'a — прим. переводчика -
Имена переменных чувствительны к регистру. Имена функций и классов — нет. Это относится и к именам методов, так что довольно странно использовать верблюжийРегистр.
Концепция
-
array() и некоторые другие языковые конструкции — не функции. array сам по себе ничего не значит, $func = "array"; $func(); не сработает.
-
Можно разложить массив по переменным с помощью конструкции list($a, $b) = …. list() — синтаксис, напоминающий функцию, как и array. Я не знаю, почему для этого нет отдельного синтаксиса и почему название настолько неочевидно.
-
(int) — это синтаксис, разработанный по образу Си; в языке нет понятия int. Проверьте: var_dump(int) не только не работает, возникает ещё и ошибка разбора, потому что это выглядит как приведение типа.
-
(integer) — это синоним (int). Ещё есть (bool)/(boolean) и (float)/(double)/(real).
Особенно нехорошо получилось с double и long: это никак не число двойной точности / длинное целое. И float/real, и double — это числа обычной точности длиной в одно слово, размер которого определяется платформой — прим. переводчика -
Есть оператор (array) для приведения к массиву и (object) для приведения к объекту. Звучит глупо, но в этом почти есть смысл: можно использовать (array), чтобы привести аргумент функции к массиву с одним элементом и одинаково обращаться с массивами и переменными. Правда, это ненадёжно, потому что, если передать объект, получится массив, содержащий поля объекта. (Приведение массива к объекту действует противоположным образом)
-
include() и подобные функции напоминают #include в Си: они вываливают код другого файла в твой код. Нет модульной системы, даже для PHP-кода.
-
Нет вложенных или локально видимых функций и классов. Только глобальные. Подключение файла приводит к попаданию переменных в текущую область видимости (и даёт файлу доступ к твоим переменным), но функции и классы оказываются в глобальной области видимости.
Для людей, у который PHP — первый язык, это вообще очень плохо. У них потом глаза на лоб вылазят от анонимных классов, они начинают бояться нормальных языков — прим. переводчика -
Добавление к массиву производится с помощью $foo[] = $bar.
-
echo — оператор, а не функция.
print и четыре разных include также не функции. По крайней мере, is_callable возвращает для них false, а скобки необязательны — прим. переводчика -
empty($var) — это настолько не функция, что любое значение, кроме переменной, например, empty($var || $var2), — это ошибка разбора. Почему, в самом деле, это так необходимо для проверки на пустоту? (Исправлено в 5.5.)
Из жизни переводчика:var_dump($obj->viewId); var_dump(empty($obj->viewId)); string '3' (length=1) boolean true
-
Избыточный (читай — альтернативный) синтаксис для блоков: if (...): ... endif;, и т. п..
Обработка ошибок
-
В PHP есть уникальный оператор @ (позаимствованный из DOS), который глушит ошибки.
-
К ошибкам PHP не создаётся трассировка стека. Нужно ставить обработчик, чтобы трассировать стек вызова. (Но этого нельзя сделать для фатальных ошибок — см. далее)
-
При ошибках разбора PHP просто выплёвывает код и ничего более, что делает отладку забытой кавычки ужасной.
-
Анализатор PHP изнутри обрабатывает :: как T_PAAMAYIM_NEKUDOTAYIM, оператор << — как T_SL. И когда в неверном месте оказывается :: или <<, интерпретатор показывает пользователю внутреннее представление.
-
Значительная часть обработки ошибок заключается в выводе строки в лог сервера, который никто не читает.
-
Есть E_STRICT, но он, кажется, предотвращает немногое, и нет документации, объясняющей, что же он, всё-таки, делает.
-
E_ALL включает все категории ошибок... кроме E_STRICT. (Исправлено в 5.4.)
Много лет по привычке пишу error_reporting(E_ALL|E_STRICT);. А что, если старый говнокод, написанный при старом E_ALL, запустить на свежем интерпретаторе? — прим. переводчика -
Очень странно и бессистемно осознание интерпретатором того, что позволено, а что нет. Не знаю, как с этим себя ведёт E_STRICT, но здесь это в порядке вещей:
-
Попытка доступа к несуществующему полю объекта, например, $foo->x. (warning)
-
Использование переменной в качестве имени переменной, функции или класса. (без предупреждений)
Мне кажется, что это бывает полезно: когда в PHP ещё не было рефлексии, $controller->$action() творило чудеса — прим. переводчика -
Попытка использовать неопределённую константу. (notice)
-
Попытка доступа к полю не-объекта. (notice)
-
Попытка использовать несуществующую переменную. (notice)
-
2 < "foo" (silent)
-
foreach (2 as $foo); (warning)
А это «ненормально»:
-
Попытка доступа к несуществующей константе класса, например, $foo::x. (fatal error)
-
Попытка использовать строчный литерал в качестве имени переменной, функции или класса. (parse error)
Есть конструкции, которые выглядят непривычно, но должны были бы работать. "var_dump"($var) кажется адекватным выражением, но не работает. А вот в Java, например, выражение "string".equals("other") абсолютно нормально — прим. переводчика -
Попытка вызова неопределённой функции. (fatal error)
-
Непоставленная точка с запятой в последней строке блока кода или файла. (parse error)
-
Использование list и прочих других встроенных квази-функций в качестве имён методов. (parse error)
-
Обращение к элементу возвращаемого функцией массива, например, foo()[0]. (parse error; исправлено в 5.4, см. ниже)
Где-то здесь есть ещё несколько примеров странных ошибок разбора.
-
-
Метод __toString не может бросать исключений. Если попробовать, то PHP, эм... бросит исключение. (На самом деле, фатальную ошибку, которую можно обойти, если...)
-
Исключения и ошибки в PHP — это абсолютно разные звери. Кажется, они вообще не взаимодействуют.
-
Ошибки (внутренние и вызовы к trigger_error) не могут быть пойманы с помощью try/catch.
О PHP 7 пишут, что большая часть ошибок теперь стали исключениями — прим. переводчика -
Исключения обходят стороной обработчик ошибок, установленный функцией set_error_handler.
-
Наоборот, есть отдельная функция set_exception_handler, которая принимает непойманные исключения, потому что оборачивание всего кода в try/catch невозможно в модели mod_php.
-
Фатальные ошибки (например, new ClassDoesntExist()) вообще нельзя обработать. Многие невинные вещи вызывают фатальные ошибки, принудительно прерывая выполнение по сомнительным причинам. Shutdown-функции всё ещё работают, но не могут получить трассировку стека (они работают на верхнем уровне) и толком не знают, завершилась программа по ошибке или нормально отработала.
-
Попытка бросить объект, не являющийся исключением, приводит к... фатальной ошибке, а не исключению.
-
-
Нет конструкции finally, из-за чего обёрточный код (установить обработчик, выполнить код, убрать обработчик; monkeypatch, запустить тест, unmonkeypatch) писать скучно и тяжело. Вопреки тому, что объектные исключения были скопизжены из Java, это умышленно, потому что finally «не имеет смысла в контексте PHP». Че-е-его? (Исправлено в 5.5.)
Функции
-
Вызовы функций, по всей видимости, —дорогостоящие операции.
-
Некоторые встроенные функции взаимодействуют с функциями, возвращающими ссылку, эм, странным образом.
-
Как говорилось ранее, многие вещи, которые выглядят как функции, или выглядят так, как будто они должны быть функциями, на самом деле являются языковыми конструкциями, поэтому то, что работает с функциями, не работает с ними.
-
Аргументы функций могут иметь «подсказки типа», которые должны приводить к статической типизации. Но нельзя требовать от аргумента соответствия типу int, string, object или любому другому встроенному типу, даже при том, что любая встроенная функция использует такую типизацию, видимо, потому что int в PHP — ничто. (Выше есть пара слов об (int).) Также нельзя использоевать псевдотипы, которые вовсю используются встроенными функциями: mixed, number, callback. (callable разрешено начиная с PHP 5.4.)
-
Как следствие, этот код:
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... -
Можно заметить, что класс, используемый для «подсказки типа» не обязан существовать; в этой программе нет класса string. Если попробовать использовать ReflectionParameter::getClass(), чтобы сделать подсказку типа динамической, тогда PHP скажет, что класс не существует, делая невозможным определить имя класса.
-
Нельзя указать тип возвращаемого значения функции.
Было забавно, когда я несколько лет назад обнаружил, что вместо ключевого слова function в плюсах нужно указывать тип возвращаемого значения — прим. переводчика -
Передача аргументов, переданных в текущую функцию, другой функции делается так: call_user_func_array('other_function', func_get_args()). Но func_get_args во время выполнения бросает фатальную ошибку, сообщая, что не может быть использована в качестве аргумента функции. Как и почему это вообще вызывает ошибку? (Исправлено в PHP 5.3.)
-
Замыкания требуют явного перечисления переменных из внешней области видимости, которые должны попасть в замыкание. Почему интерпретатор сам не разберётся с этим? Своего рода искалечивание всей языковой конструкции. (Ладно, это всё потому что использование переменной вызывает создание последней, если явно не указано другое.)
-
Переменные, попадающие в замыкание, «передаются» тем же способом, что и в другие функции. Ну да, массивы, строки, всё это будет передано по значению. Если не использовать &.
Этот вовсе не значит, что огромный массив сразу обретёт копию в памяти. PHP использует довольно странный механизм copy-on-write. Это значит, что массив или строка будут скопированы только при попытке записи в них, а до этого чтение будет осуществляться из той же области памяти — прим. переводчика -
Т. к. переменные замыкания — это автоматически передаваемые аргументы, а вложенных областей видимости не существует, замыкание не может обращаться к приватным методам, даже будучи объявленным внутри класса. (Возможно, исправлено в 5.4? Неясно.)
-
Нет именованных аргументов функций. Явно отвергнуто разработчиками, т. к. «это придумано для внесения беспорядка в код».
В JavaScript уже давно принято передавать в функции хэш в качестве единственного аргумента. В PHP объявление «массивов» слишком многословно для этого — прим. переводчика -
Аргументы функций со значениями по умолчанию могут перечисляться раньше элементов без значений по умолчанию, даже при том, что документация указывает на странность и бесполезность такой записи. (Так зачем это разрешать?)
-
Лишние аргументы функций игнорируются (за исключением встроенных функций, где произойдёт ошибка). Недостающим аргументам присваивается null.
-
Функции с переменным числом аргументов требуют танцев с бубном, func_num_args, func_get_arg, и func_get_args. Для этой вещи нет отдельного синтаксиса.
-
ОО
-
Процедурные части PHP напоминают Си, но объектные похожи на Java. Я не могу не подчеркнуть, как же это раздражает. Система классов разработана на основе более низкоуровневого языка, который по своей сути обдуманно более ограничен, чем современники PHP, так что я сбита с толку.
-
Я ещё не не нашла ни одной заглавной буквы в именах глобальных функций, но ВажныеВстроенныеКлассы используют верблюжийРегистр в именах полей и именование методов вСтилеJava.
-
В Perl, Python и Ruby есть концепция доступа к «полям» с помощью кода; в PHP — только неуклюжий __get и ему подобные. (Документация необъяснимо ссылается на такие особые методы как «перегрузка».)
-
В классах есть объявления переменных и констант (var и const), в процедурной части языка — нет.
-
Не смотря на влияние C++/Java, где объекты довольно непрозрачны, PHP часто обращается с ними как с хэшами, например, типичное поведение foreach ($obj as $key => $value) — прошарить по всем доступным атрибутам объекта.
-
-
Классы — не объекты. Метапрограммирование требует обращения к ним по имени, как к функциям.
-
Встроенные типы — не объекты и (в отличие от Perl) не могут рассматриваться как объекты.
В Java есть классы-обёртки для примитивов. Это неуклюже выглядит и вызывает заметные накладные расходы, но есть случаи, когда это самый простой и быстрый способ упаковать значение нужным образом. А PHP библиотека SPLTypes могла бы сыграть ту же роль, но «из коробки» её нет — прим. переводчика -
instanceof — это оператор, несмотря на то, что классы появились поздно и всё что есть в языке построено на функциях или похоже на них. Влияние Java? Классы не первичны? (Я не знаю.)
-
Но is_a — это функция. С необязательным аргументом, указывающим, можно ли вместо объекта передать строку с именем класса.
-
get_class — это функция, нет оператора typeof. Как и is_subclass_of.
-
Конечно же, это не работает со встроенными типами (опять же, int — ничто). Для этого нужно использовать is_int и т. д..
Или gettype, который вернёт строку — прим. переводчика -
Правая часть выражения должна быть переменной или строчным литералом, она не может быть выражением. Вызывает ошибку разбора.
-
-
Clone — это оператор?!
-
Поля объектов — это $obj->foo, поля классов — Class::$foo. ($obj::$foo попытается привести $obj к строке и использовать в качестве имени класса.) К полям класса не обратиться из объекта; области видимости абсолютно разные, что делает поля класса абсолютно бесполезными для полиморфизма. Методы классов, конечно же, этому правилу не подчиняются и могут быть вызваны как любой другой метод. (Я говорила, что C++ тоже так делает. Но Плюсы — неудачный пример хорошего ОО.)
-
Также, метод объекта можно вызвать статично (Class::method()). Если это сделать из другого метода, будет расценено как обычный метод, вызванный на данном объекте — $this. Ну, я так думаю.
-
new, private, public, protected, static и т. д.. Хотите переплюнуть разработчиков Java? Я знаю, что эта модель — дело вкуса, но понять не могу, зачем всё это нужно в динамическом языке. В C++ большая часть этого нужна для компиляции и разрешения имён в процессе последней.
Забудьте про private и protected в PHP. var_dump всё это вскроет. Эти модификаторы стоило назвать словом readonly — прим. переводчика -
В PHP есть поддержка абстрактных классов, т. е. таких, экземпляры которых невозможно создать. В похожих языках этого добиваются, бросая исключение в конструкторе.
-
Нельзя переопределить приватный метод родительского класса. Из перегруженных методов нельзя даже увидеть, не то что вызвать, приватные методы родительского класса. Создаёт проблемы, например, при тестировании.
-
Метод не может называться, например, «list», т. к. list() — это специальный синтаксис (не функция), и анализатор сбивается с толку. Но здесь нет неоднозначности, т. к. $foo->list() не вызывает синтаксической ошибки.
Особенно есть учесть, что метод текущего класса приходится многословно вызывать как $this->list(), а list() не может быть расценен как метод класса / объекта — прим. переводчика -
Если во время вычисления аргументов конструктора будет выброшено исключение (например, в коде new Foo(bar()) функция bar() бросит исключение), конструктор не будет вызван, но деструктор будет. (Исправлено в PHP 5.3.)
-
Исключения в __autoload и деструкторах приводят к фатальным ошибкам. (Исправлено PHP 5.3.6. Теперь деструктор может выбросить исключение где угодно, в тот момент, когда количество ссылок достигнет нуля. Хмм...)
-
Нет конструкторов и деструкторов. __construct это инициализатор, как __init__ в Python. В классе нет метода, который можно вызвать, чтобы выделить память и создать объект.
-
Нет инициализатора по умолчанию. Вызов parent::__construct(), когда __construct родительского класса не определён явно, вызовет фатальную ошибку.
-
OO принесло интерфейс Iterator, с которым части языка работают (e.g., for...as), но никакие встроенные типы (как массивы) не реализуют этот интерфейс. Если нужен итератор для массивов, придётся использовать обёртку ArrayIterator. Нет встроенных путей связывать, нарезать массив или как-то иначе пользоваться итераторами как встроенными конструкциями.
-
Интерфейсы наподобие Iterator занимают несколько хороших имён методов. Если хочется реализовать Iterable (без поведения по умолчанию с итерированием всех полей), но хочется использовать такие имена методов, как key, next или current, то всё очень плохо.
-
Можно перегрузить методы, которые определяют, как объект данного класса себя ведёт, будучи преобразованным к строке или будучи вызванным, как функция, но не как он преобразовывается к числу (или другому встроенному типу).
-
Строки, числа и массивы приводятся к строкам; язык очень завязан на этом. Функции и классы — это и есть строки. Но попытка привести встроенный или пользовательский объект (даже замыкание) к строке вызовет ошибку, если не перегружен метод __toString. Даже echo становится потенциальным источником ошибок.
-
Нельзя перегрузить операторы сравнения и сортировки.
-
Внутри методов объекта статические переменные глобальны; они имеют одно и то же значение для всех экземпляров класса.
Эй, а как должно быть? Они же статические! — прим. переводчика
Стандартная библиотека
Perl «требует сборки». Python «с батарейками в комплекте». PHP — это «кухонная раковина, но она из Канады, а на обоих кранах написано Х».
Общее
-
Нет модульной системы. Можно собрать расширение для PHP, но загружать ли — указано в php.ini, а ты можешь только узнать, загружено ли оно (и вносит своё содержимое в глобальное пространство имён) или нет.
-
С тем, что пространства имён — это новинка, стандартная библиотека не так уж и сломана. В глобальном пространстве имён находятся тысячи функций.
-
Части одной библиотеки дико непохожи друг на друга:
-
Подчёркивание против слитной записи: 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 против create_function, recode_string
-
Порядок аргументов: array_filter($input, $callback) против array_map($callback, $input); strpos($haystack, $needle) против array_search($needle, $haystack)
-
Путаница с приставками: usleep против microtime
-
Нечувствительные к регистру функции отличаются наличием i в имени.
-
Около половины функций для работы с массивами начинаются на array_. Остальные — нет.
-
htmlentities и html_entity_decode это противоположности с абсолютно разным именованием.
-
-
Кухонная раковина. Библиотека включает:
-
Связи с ImageMagick, с GraphicsMagick (это форк ImageMagick), горстку функций для работы с EXIF (что ImageMagick тоже умеет делать).
-
Функции для разбора bbcode, очень специфичной разметки, используемые небольшой кучкой форумов.
-
Слишком много пакетов для XML. DOM (OO), DOM XML (функциональная), libxml, SimpleXML, «XML Parser», XMLReader/XMLWriter и ещё полдюжины акронимов, которые я не могу разобрать. Конечно, между ними есть определённая разница, и ты можешь взять и разобраться в этом.
-
Два разных обработчика кредитных карт — SPPLUS и MCVE. Шта?
-
Три способа обратиться к СУБД MySQL: mysql, mysqli и слой абстракции PDO.
-
Влияние Си
Этот пункт списка говорит сам за себя. Это абсурд, проникший в глубины языка. PHP — это высокоуровневый динамически типизированный язык программирования. Но значительная часть стандартной библиотеки — это довольно тонкие обёртки для API Си, из чего следует:
-
«Выходные» аргументы, хотя функция в PHP, немного поднатужившись, может вернуть несколько значений или хэш-карту.
-
Как минимум дюжина функций для получения последней ошибки от определённой подсистемы, при том что исключения в PHP присутствуют уже восемь лет.
-
Бородавки наподобие mysql_real_escape_string, даже при том, что она принимает те же аргументы, что и сломанная mysql_escape_string, существуют только потому что это часть MySQL C API.
-
Глобальное поведение для неглобальной функциональности (как MySQL). Использование нескольких MySQL-соединений требует явной передачи идентификатора соединения.
-
Обёртки очень, очень тонкие. Например, вызов dba_nextkey без вызова dba_firstkey приведёт к ошибке сегментации.
-
Обёртки зачастую платформозависимы: fopen(directory, "r") работает под Linux, но возвращает false и генерирует предупреждение под Windows.
-
Есть набор функций ctype_* (например ctype_alnum) которые ведут к функциям Си с подобными именами для определения класса символов, вместо, скажем, isupper.
Обобщение
Нет его. Если функция должна уметь делать две немного разные вещи, в PHP для этого найдётся две функции.
Как отсортировать в обратном порядке? В Perl это будет sort { $b <=> $a }. В Python — .sort(reverse=True). В PHP есть отдельная функция — rsort().
-
Функции, которые ищут Сишную ошибку: curl_error, json_last_error, openssl_error_string, imap_errors, mysql_error, xml_get_error_code, bzerror, date_get_last_errors, другие?
-
Функции сортировки: array_multisort, arsort, asort, ksort, krsort, natsort, natcasesort, sort, rsort, uasort, uksort, usort
-
Функции поиска текста: ereg, eregi, mb_ereg, mb_eregi, preg_match, strstr, strchr, stristr, strrchr, strpos, 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, diskfreespace/disk_free_space…
-
scandir отдаёт список файлов в заданном каталоге. Вместо того чтобы (потенциально удобнее) отдавать их в естественном порядке, функция возвращает их отсортированными. И есть аргумент, который говорит функции отдавать файлы в обратном порядке. Видимо, функций сортировки недостаточно. (В PHP 5.4 появляется третье значение для аргумента, определяющего порядок сортировки, который её отключает)
-
str_split разбивает строку на части одинаковой длины. chunk_split разбивает строку на части одинаковой длины, после чего соединяет их через заданный разделитель.
-
Чтение архивов требует использования разных наборов функций, в зависимости от формата. Есть шесть наборов таких функций, все с разными API, для bzip2, LZF, phar, rar, zip, и gzip/zlib.
-
Так как вызывать функцию, передавая ей массив аргументов, неудобно (call_user_func_array), есть такие пары, как printf/vprintf и sprintf/vsprintf. Они совершают одни и те же действия, но одни принимают переменное число аргументов, другие — массив.
Текст
-
preg_replace с флагом /e (eval) совершит замены в строке по регулярному выражению, затем выполнит строку.
-
strtok явно разработана как эквивалент такой функции в Си, но это плохая идея по нескольким причинам. Без разницы, что в PHP можно легко вернуть массив (в Си это неудобно), или что хак strtok(3) (изменение строки на месте) здесь не используется.
-
parse_str разбирает строку запроса, что никак не указано в имени. А ещё она работает как register_globals и вываливает переменные запроса прямо в локальную область видимости, если не передать массив, который надо заполнить (конечно, она ничего не возвращает).
-
explode не делит строку, если разделитель пуст или отсутствует. Любая другая функция, разделяющая строку, в этом случае делает довольно удобную штуку; в PHP вместо этого есть отдельная функция, которая называется str_split и описана так: «преобразует строку в массив».
Грань между строкой и массивом в PHP крайне тонка. Строку также можно расценивать как массив байтов — прим. переводчика -
Для форматирования даты есть strftime, которая работает как API Си и учитывает локаль. Ещё есть date, у которой абсолютно другой синтаксис, и работает она только с английским.
-
«gzgetss — получить строку из gz-файла и убрать HTML-теги.» Мне жутко интересно, какие обстоятельства привели к концепции этой функции.
-
mbstring
-
Это всё, связанное с многобайтными строками, но проблема в кодировке.
-
Также работает с обычными строками. Использует одну «глобальную» кодировку. Некоторые функции позволяют задавать кодировку, но это влияет на все аргументы и возвращаемое значение.
-
Предоставляет функции ereg_* но они считаются устаревшими. Функциям preg_* не повезло, хоть они и могут работать с UTF-8, если передать им флаг, специфичный для Perl-совместимых регулярок.
-
Система и рефлексия
-
В общем, есть куча функций, которые стирают границу между переменными и строковыми литералами. compact и extract — это лишь верхушка айсберга.
-
Есть несколько путей организовать динамику в PHP и на первый взгляд нет разницы между следующими подходами: classkit может изменять пользовательские классы; runkit вытесняет его и может изменять что-либо, объявленное пользователем; Reflection*-классы могут работать с большей частью языка; есть ещё множество функций для работы со свойствами функций и классов. Эти подсистемы независимы, связаны, избыточны?
-
get_class($obj) возвращает имя класса объекта. get_class() возвращает имя класса, в котором вызвана функция. Со стороны, эта функция делает две разные вещи, но: get_class(null)… работает как последняя. Так что нельзя доверять этой функции от произвольного значения. Сюрприз!
-
Классы stream_* позволяют реализовывать пользовательские потоковые объекты для использования с fopen и другими встроенными файловыми функциями. «tell» нельзя реализовать по внутренним причинам. (Также в этой системе есть КУЧА функций.)
-
register_tick_function примет замыкание. unregister_tick_function — нет, вместо этого будет брошена ошибка, объясняющая, что замыкание нельзя перевести в строку.
-
php_uname возвращает информацию о текущей ОС. Кроме ситуации, когда PHP не знает, на какой платформе работает; тогда оно говорит, на какой платформе было собрано. Оно никак не сообщит, какой из этих вариантов произошёл в этот раз.
-
fork и exec не встроенные. Они находятся в составе расширения pcntl, которое не включено в состав PHP по умолчанию. popen не возвращает идентификатор процесса.
-
Значение, возвращаемое функцией stat, кэшируется.
-
session_decode считывает обычную строку сессии, но работает только если сессия запущена. А результаты кладёт в $_SESSION, вместо того чтобы вернуть их.
Разное
-
curl_multi_exec при ошибке не затрагивает curl_errno, только curl_error.
-
Аргументы mktime, по порядку: час, минута, секунда, месяц, день, год.
Обработка данных
Программы — это ничто иное, как машины, которые жуют информацию и выплёвывают больше информации. Очень многие языки разработаны на основании типов данных, которые они обрабатывают, от awk до Prolog и Си. Если язык не может обрабатывать данные, он ничего не может.
Числа
-
Целые числа (хочется сделать паузу) знаковые и 32-битные на 32-битных платформах. В отличие от современников PHP, нет продвижения BigInteger. Так что можно получить сюрприз, например, отрицательный размер файла, а математика может по-разному работать на разных архитектурах процессоров. Единственный вариант для больших чисел — использовать обёртку GMP или BC. (Разработчики предлагают добавить отдельный, новый, 64-битный тип. Это сумасшествие.)
-
PHP поддерживает восьмеричный синтаксис с ведущим 0, то есть, например, 012 — это число десять. Однако 08 становится нулём. После цифры 8 (или 9) последующие цифры исчезают. 01c это синтаксическая ошибка.
-
0x0+2 даёт 4. Анализатор решает, что 2 — это и часть шестнадцатеричного литерала, и отдельный десятичный литерал, принимая это за 0x002 + 2. 0x0+0x2 показывает ту же проблему. Странно, но 0x0 +2 это всё ещё 4, но 0x0+ 2 — это правильное 2. (Исправлено в PHP 5.4, снова сломано PHP 5.4 для новой литеральной приставки 0b: 0b0+1 даёт 2.)
-
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 появились «литералы» [...].)
-
Точно не понятно, массивы приводятся к строке "Array" с предупреждением.
-
Конструкция => взята из Perl, где foo => 1 позволяет записать значение в массив, не оборачивая ключ в кавычки. (Вот, кстати, зачем она существует в Perl; в противном случае ставится запятая.) В 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», объектную реализацию «особых строго типизированных классов». И действительно, там четыре класса, каждый работает с разной комбинацией типов ключа и значения. Неясно, почему встроенная реализация не оптимизируется для этих крайне общих случаев, и каков выигрыш в производительности.
-
Есть класс ArrayObject (реализующий пять различных интерфейсов) в который можно завернуть массив и заставить его вести себя как объект. Пользовательские классы могут реализовать эти же интерфейсы. Но только у этого класса есть горстка методов, половина которых не походит на встроенные функции для работы с массивами, и встроенные функции не знают, как работать с ArrayObject и другими классами, подобными массивам.
Функции
-
Функции — не данные. Замыкания — это объекты, но обычные функции — нет. Само по себе имя функции у интерпретатора ни с чем не ассоциируется; var_dump(strstr) вызывает предупреждение, которое намекает, что имелось в виду "strstr". Нет способа разглядеть, где просто строка, а где «указатель» на функцию.
Есть функция is_callable, которая помогает это разглядеть, но и "strstr", "hello_world" имеют абсолютно идентичное внутреннее представление, и чтобы узнать, что из них callable, придётся перекопать тысячи названий функций из глобальной области видимости — прим. переводчика -
create_function это обёртка вокруг eval. Она создаёт функцию с обычным именем и устанавливает её глобально (она никогда не попадёт в мусор — нельзя использовать в цикле!). Она ничего не знает о текущей области видимости, так что это не замыкание. Название содержит NUL-байт, так что конфликта с обычной функцией не будет (потому что анализатор PHP падает, если где-нибудь в файле встречает NUL).
-
Если объявить функцию с именем __lambda_func, create_function упадёт — текущая реальзация заключается в eval-создании функции по имени __lambda_func и внутреннем переименовании. Если __lambda_func уже существует, первая часть выбросит фатальную ошибку.
Другое
-
Инкремент (++) переменной со значением NULL даёт 1. Декремент (--) NULL даёт NULL. Декремент строки также ничего не делает.
А инкремент строки воздействует на ASCII-значение последнего символа — "qwe" становится "qwf" — прим. переводчика -
Нет генераторов. (Исправлено в 5.5. Ух ты, они ещё и скопировали API генераторов в Python. Впечатляет. Однако, почему-то, $foo = yield $bar; — это синтаксическая ошибка; должно быть $foo = (yield $bar). Ох.)
Уеб-Веб-каркас
Исполнение
-
Один общий файл, php.ini, управляет значительной частью функционала PHP и вводит сложные правила того, что перекрывает что и когда. PHP-скрипты, которые предназначены для запуска на обычных машинах, всё равно должны переопределить некоторые переменные для нормализации окружения, что делает механизмы наподобие php.ini довольно бесполезными.
-
PHP ищет php.ini в нескольких местах, так что, вероятно (или нет), можно переопределить настройки сервера своим файлом. Однако, только один файл будет найден и проанализирован, поэтому нельзя переопределить несколько директив когда возникнет необходимость.
-
-
PHP обычно работает как CGI. Каждый раз, когда запрашивается страница, PHP перекомпилирует весь код перед выполнением. Даже серверы для разработки игрушечных каркасов на Python так не делают.
Это привело к целому рынку «PHP-ускорителей», которые компилируют единожды, ускоряя PHP всеми способами и приближая к другим языкам. Zend, компания, стоящая за PHP, сделала это частью своей бизнес-модели.
-
В течение довольно долгого времени ошибки PHP шли к клиенту по умолчанию — я думаю, чтобы помочь при разработке. Наверное, сейчас уже не так, но я всё ещё встречаю ошибки MySQL, выплюнутые наверху страницы.
-
PHP полон странных «пасхалок», которые отдают логитип PHP при определённом запросе. Это не только не относится к делу при построении своего приложения, но также позволяет определить, что на сервере установлен PHP (и, возможно, примерно прикинуть версию), вне зависимости от использования mod_rewrite, FastCGI, обратных прокси или конфигурации Server:.
Исправлено в PHP 5.5 — прим. переводчика -
Пустые строки перед и после <?php ... ?> даже в библиотеках считаются за литералы и уходят в ответ (или вызывают ошибку «заголовки уже отправлены»). Так что приходится избегать пустых строк в начале и конце файла (одна после ?> не в счёт) или опускать закрывающий символ ?>.
Размещение
Часто развёртывание упоминают в качестве основного преимущества PHP: хуяк-хуяк и в продакшен закинул файлы — и готово. Действительно, разместить скрипт на Python, Ruby или Perl — целое дело. Но PHP оставляет желать лучшего.
С другой стороны, мне нравится запускать Web-приложения как сервер приложений и использовать обратный прокси. Установка этого всего требует минимальных усилий, а плюсов полно: можно управлять веб-сервером и приложением по отдельности, можно запускать сколько угодно процессов приложений на любом количестве машин без необходимости в большем количестве веб-серверов, можно легко запустить приложение от имени другого пользователя, можно переключать веб-серверы, можно отключить приложение, не затрагивая веб-сервер, можно делать бесшовное развёртывание переключением FIFO-точек входа и так далее. Абсурдно приваривать своё приложение к серверу, больше нет причин так делать.
-
PHP привязан к Apache. Запуск их по отдельности или под другим сервером требует столько же (или больше) танцев с бубном, сколько и развёртывание на любом другом языке.
-
php.ini позволяет PHP-приложению запускаться где угодно. Существует только один файл php.ini, его влияние глобально; если на общем сервере нужно изменить его или же двум приложениям нужны разные настройки, тебе не повезло; нужно задать общие настройки и урезать их из приложений, используя ini_set, файл конфигурации Apache или .htaccess. Если это возможно. Вообще, нужно проверить удивительное количество мест, чтобы понять, как настройкам достаются их значения.
-
Аналогично, невозможно «изолировать» приложение и его зависимости от остальной системы. Запустить два приложения, которым нужны разные версии библиотеки или самого PHP? Начни с установки второго экземпляра Apache.
-
Подход «связки файлов», с другой стороны, делает маршрутизацию большой анальной болью, также заставляя разработчика аккуратно составить «белый список» файлов, видимых из интернета, и скрыть остальное, т. к. URL также определяет точку входа в приложение. Файлы конфигурации и прочие «кусочки» кода требуют Си-подобной охраны от непосредственного скачивания. Файлы систем контроля версий (например, .svn, .git) требуют защиты. mod_php, делает всю файловую систему потенциальной точкой входа; с сервером приложений есть только одна точка входа, и только URL контролирует, будет ли она вызываться.
-
Нельзя бесшовно обновить кучку файлов, которые запускаются в стиле CGI, если не хочется падений и неадекватного поведения в моменты, когда пользователь заходит на сайт, находящийся в процессе обновления.
-
Несмотря на то, насколько «просто» заставить Apache исполнять PHP, даже здесь есть тонкая ловушка. В то время как документация PHP рекомендует использовать SetHandler чтобы заставить выполняться .php-файлы, AddHandler, кажется, работает так же замечательно, а Google даёт по этому запросу вдвое больше результатов. Вот в чём проблема.
Когда используешь AddHandler, говоришь Apache: «исполни это как PHP», это единственно возможный способ исполнить .php-файл. Но! Apache расценивает расширения файлов не так, как любое человеческое существо на планете. Он разработан так, чтобы позволить распознать, скажем, index.html.en одновременно как английский и HTML. Для Apache у файла может быть любое количество расширений одновременно.
Представь, что есть форма, которая грузит файлы в общедоступный каталог. Чтобы убедиться в том, что никто не загрузит туда скрипт, надо проверить, нет ли у файла расширения .php. Всё, что нужно сделать кулхацкеру, — это загрузить файл с именем foo.php.txt; скрипт-загрузчик не заметит подлога, а вот Apache опознает файл как PHP-скрипт и счастливо выполнит его.
Проблема здесь не в использовании исходного имени файла или плохой фильтрации; проблема в том, что веб-сервер настроен на запуск любого кода — как раз то свойство, которое делает PHP «простым для развёртывания». CGI требует наличия прав на выполнение, и это хоть что-то, но PHP не делает даже этого. И это не теоретическая проблема — я нашла много сайтов с такой конфигурацией.
Недостающие возможности
Я считаю, что всё это имеет разную степень критичности для построения веб-приложений. С тем, что PHP считается «Веб-языком», кажется разумным, что он должен обзавестись некоторыми из них.
-
Нет стандартного шаблонизатора. В общем случае шаблон страницы — это PHP-скрипт.
-
Нет фильтра от XSS. Нет, «не забывайте использовать htmlspecialchars» — это не XSS-фильтр. Вот фильтр.
-
Нет защиты от межсайтовой подделки запроса. Нужно делать это самостоятельно.
-
Нет общего стандартного API баз данных. Библиотеки наподобие PDO должны обернуть API каждой СУБД чтобы убрать различия.
-
Нет маршрутизации. Твой сайт выглядит в точности как файловая система. Многие разработчики думают, что mod_rewrite (и .htaccess в частности) — это допустимая замена.
-
Нет аутентификации или авторизации.
-
Нет сервера для разработки. («Исправлено» в 5.4. Привело к уязвимости с Content-Length (ниже). Также, придётся перевести все rewrite'ы к формату обёртки PHP, т. к. нет маршрутизации.)
-
Нет интерактивной отладки.
-
Нет связного механизма развёртывания, только «скопируй все эти файлы на сервер».
Безопасность
Ограничения языка
Хилая репутация безопасности PHP во многом сложилась оттого, что оно берёт информацию из одного языка и вываливает в другой. Это плохая идея. <script> ничего не значит в SQL, но, конечно, значит в HTML.
Что делает это ещё хуже, так это повсеместный крик «фильтруйте ввод». Это полностью неправильно; нельзя махнуть волшебной палочкой, чтобы кусок информации стал «чистым». То, что нужно делать, так это говорить на языке: использовать placeholder'ы в SQL, списки аргументов при запуске процессов и так далее.
-
PHP прямо-таки поощряет «дезинфекцию»: есть даже целое расширение для фильтрации данных.
-
Все эти addslashes, stripslashes и прочая ерунда, связанная со слешами, это отвлекающие манёвры, которые нифига не помогают.
-
Насколько я знаю, нет пути безопасно запустить процесс. Можно лишь выполнить строку в системном интерпретаторе. Тебе остаётся только экранировать запрос сумасшедшим образом и надеяться, что угадал с экранированием, или же звать pcntl_fork и pcntl_exec ручками.
-
И escapeshellcmd, и escapeshellarg имеют очень похожие описания. Обрати внимание, что на винде escapeshellarg не сработает (поскольку предполагает семантику Борна), а escapeshellcmd просто заменит всю пунктуацию пробелами, потому что никто не может понять, как командная строка Windows экранирует символы (и пытается молча похерить всё, что ты хочешь сделать).
-
По сей день широко используемая встроенная связка с MySQL не умеет подготавливать запросы.
На сегодняшний день документация PHP об SQL-инъекциях рекомендует сумасшедшие практики, как-то: проверка типа, использование sprintf и is_numeric, ручное использование mysql_real_escape_string повсюду или же ручное использование addslashes везде (которые «могут быть полезны»!). Там не нашлось места для PDO и параметризации, кроме как в комментариях пользователей. Как минимум два года назад я жаловалась PHP-разработчику на то, что это слишком необычно, и он был встревожен, а страница за это время так и не изменилась.
Небезопасно по умолчанию
-
register_globals. По умолчанию оно было отключено долгое время и удалено в 5.4. Мне всё равно. Это был полный отстой.
-
include принимает HTTP URLы. Такая же фигня.
-
Волшебные кавычки. Так близко к защищённости по умолчанию и так далеко от понимания концепции вообще.
-
Можно, скажем, зондировать сеть, используя поддержку XML в PHP, оскорбляя его вездесущую поддержку имёнФайлов-в-качестве-URL'ов. И только libxml_disable_entity_loader() может это исправить, а проблема упомянута только пользователями в комментариях.
(5.5 привносит функцию для хеширования паролей, password_hash, которая должна прийти на смену говно-крипто-коду и отсебятине.)
Ядро
У интерпретатора PHP есть завораживающие проблемы с безопасностью.
-
В 2007 году у интерпретатора была уязвимость, вызывавшая целочисленное переполнение. Исправление начиналось с if (size > INT_MAX) return NULL; и скатилось вниз оттуда. (Для тех, кто не знаком с Си: INT_MAX — это самое большое число, которое поместится в целочисленную переменную. Я думаю, остальное можно понять из этого.)
-
Не так давно, в PHP 5.3.7, появилась функция crypt(), которая позволяла кому угодно заходить с любым паролем.
-
Сервер для разработки с PHP 5.4 имеет DoS-уязвимость, т. к. берёт число, переданное в заголовке Content-Length (куда можно написать что угодно), и пытается выделить столько памяти. Это плохая идея.
Я могла бы накопать больше, но суть не в том, что существует %d эксплойтов — в конце-концов, в программах бывают ошибки. Сама природа этого ужасающа. И я не искала это; всё это случилось на моём пороге за последние несколько месяцев.
Заключение
В комментариях справедливо заметили, что у меня нет заключения. И, да, заключения у меня нет. Если ты дочитал до этого места, то, полагаю, был согласен с самого начала :)
Если ты знаешь только PHP и хочешь изучить что-нибудь ещё, можешь погрузиться в Учебник по Python и попробовать Flask для веб. (Я не большая фанатка такого шаблона языка, но свою работу он делает.) Он разбивает твоё приложение на части, но они остаются всё теми же частями и должны выглядеть достаточно знакомо. Позже мне стоит написать пост об этом; я не планирую писать это прямо в рамках этой статьи.
Затем для больших проектов тебе может понадобиться среднеуровневый Pyramid, или сложное чудовище Django, которое отлично работает для создания сайтов наподобие их собственного.
Если ты не разработчик, но по какой-то причине прочитал это, я не буду счастлива, пока все на планете не выучат Python сложным способом, так что пойди и сделай это.
Также есть Ruby с Рельсами и другие конкуренты, с которыми я не знакома, а Perl ещё жив и выясняет отношения с Catalyst. Читай, познавай, строй, сходи с ума.
Титры
Спасибо за вдохновение:
-
куче PHP-фанатиков и контр-фанатиков
-
и, конечно же, Расмусу Лердорфу за его дикое непонимание большей части Perl'а.
Дайте знать, если у вас есть какие-нибудь дополнения или я в чём-то не права.
Размещено Eevee в блоге.