Статьи

Восхитительное зло PHP

Эта статья была рецензирована Верн Анчета и Дежи Акала . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!


Я хочу посмотреть на две функции PHP: eval и exec . Они так часто попадают под шину разумных разработчиков, которые никогда не используют, что я иногда задаюсь вопросом, сколько удивительных приложений мы пропускаем.

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

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

Злой слон

Создание динамического класса

Впервые я увидел динамическое создание классов в недрах CodeIgniter . В то время CodeIgniter использовал его для создания классов ORM. eval по-прежнему используется для перезаписи коротких открытых тегов для систем, в которых эта функция не включена…

Совсем недавно, однако, мой друг Адам Ватан написал в Твиттере об использовании его для динамического создания фасадов Laravel . Посмотрите, как обычно выглядят классы:

 namespace Illuminate\Support\Facades; class Artisan extends Facade { protected static function getFacadeAccessor() { return "Illuminate\Contracts\Console\Kernel"; } } 

Это из github.com/laravel/framework/blob/5.3/src/Illuminate/Support/Facades/Artisan.php

Эти классы фасадов не являются фасадами в традиционном смысле , но они действуют как статические ссылки на объекты, хранящиеся в классе локатора сервиса Laravel. Они проектируют простой способ обращения к объектам, определенным и настроенным в других местах, и имеют преимущества по сравнению с традиционными статическими (или одноэлементными) классами. Одно из этих преимуществ в тестировании:

 public function testNotificationWasQueued() { Artisan::shouldReceive("queue") ->once() ->with( "user:notify", Mockery::subset(["user" => 1]) ); $service = new App\Service\UserService(); $service->notifyUser(1); } 

… и хотя эти фасады просты в создании, их много. Это не тот код, который я нахожу интересным для написания. Кажется, Адам чувствовал то же самое, что и когда мы написали твит .

Итак, как мы можем динамически создавать эти классы фасадов? Я не видел код реализации Адама, но я думаю, он выглядит примерно так:

 function facade($name, $className) { if (class_exists($name)) { return; } eval(" class $name extends Facade { protected static function getFacadeAccessor() { return $className::class; } } "); } 

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

Я использовал подобный прием раньше, когда работал над библиотекой функционального программирования. Я хотел создать вспомогательный код, чтобы позволить мне писать приложения, используя меньше (если есть) классов. Вот что я использовал для создания eval :

 functional\struct\create("person", [ "first_name" => "string", "last_name" => "string", ]); $me = person([ "first_name" => "christopher", "last_name" => "pitt", ]); 

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

Следующий код использует много экземпляров ƒ . Это юникодный символ, который я использовал как своего рода псевдо-пространство имен для свойств и методов, которые я не могу сделать приватными, но не хочу, чтобы другие обращались напрямую. Интересно знать, что большинство символов Юникода являются допустимыми символами для имен методов и свойств.

Чтобы разрешить это, я создал следующий код:

 abstract class ƒstruct { /** * @var array */ protected $ƒdef = []; /** * @var array */ protected $ƒdata = []; /** * @var array */ protected $ƒname = "structure"; public function __construct(array $data) { foreach ($data as $prop => $val) { $this->$prop = $val; } assert($this->ƒthrow_not_all_set()); } private function ƒthrow_not_all_set() { foreach ($this->ƒdef as $prop => $type) { $typeIsNotMixed = $type !== "mixed"; $propIsNotSet = !isset($this->ƒdata[$prop]); if ($typeIsNotMixed and $propIsNotSet) { // throw exception } } return true; } public function __set($prop, $value) { assert($this->ƒthrow_not_defined($prop, $value)); assert($this->ƒthrow_wrong_type($prop, $value)); $this->ƒdata[$prop] = $value; } private function ƒthrow_not_defined(string $prop) { if (!isset($this->ƒdef[$prop])) { // throw exception } return true; } private function ƒthrow_wrong_type(string $prop, $val) { $type = $this->ƒdef[$prop]; $typeIsNotMixed = $type !== "mixed"; $typeIsNotSame = $type !== type($val); if ($typeIsNotMixed and $typeIsNotSame) { // throw exception } return true; } public function __get($prop) { if ($property === "class") { return $this->ƒname; } assert($this->ƒthrow_not_defined($prop)); if (isset($this->ƒdata[$prop])) { return $this->ƒdata[$prop]; } return null; } } function type($var) { $checks = [ "is_callable" => "callable", "is_string" => "string", "is_integer" => "int", "is_float" => "float", "is_null" => "null", "is_bool" => "bool", "is_array" => "array", ]; foreach ($checks as $func => $val) { if ($func($var)) { return $val; } } if ($var instanceof ƒstruct) { return $var->class; } return "unknown"; } function create(string $name, array $definition) { if (class_exists("\\ƒ" . $name)) { // throw exception } $def = var_export($definition, true); $code = " final class ƒ$name extends ƒstruct { protected \$ƒdef = $def; protected \$ƒname = '$name'; } function $name(array \$data = []) { return new ƒ$name(\$data); } "; eval($code); } 

Это похоже на код, найденный на github.com/assertchris/functional-core

Здесь много чего происходит, поэтому давайте разберемся с этим:

  1. Класс ƒstruct является абстрактной основой для этих самопроверяющихся структур. Он определяет поведение __get и __set которое включает проверку наличия и достоверности данных, используемых для инициализации каждой структуры.
  2. Когда структура создана, ƒstruct проверяет, были ли предоставлены все необходимые свойства. То есть, если какое-либо из свойств не смешано, они должны быть определены.
  3. Поскольку каждое свойство установлено, предоставленное значение сверяется с ожидаемым типом этого свойства.
  4. Все эти проверки предназначены для работы с (и включенными) вызовами assert . Это означает, что проверки выполняются только в средах разработки.
  5. Функция type используется для возврата предсказуемых строк типов для наиболее распространенных типов переменных. Кроме того, если переменная является подклассом ƒstruct , ƒname свойства ƒname возвращается в виде строки типа. Это означает, что мы можем определить вложенные структуры так же легко, как: create("account", ["holder" => "person"]) . Предостережение заключается в том, что предопределенные типы (такие как "int" и "string" ) всегда будут разрешаться до структур с одинаковым именем.
  6. Функция create использует eval для создания новых подклассов ƒstruct , содержащих соответствующие имена классов, ƒname и ƒdef . var_export принимает значение переменной и возвращает ее синтаксическую var_export форму.

Функция assert обычно отключается в производственных средах, zend.assertions в php.ini zend.assertions 0. Если вы не видите ошибок утверждений там, где вы ожидаете их, проверьте, установлен ли этот параметр.

Специфичные для домена языки

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

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

CSS это еще один отличный DSL. Он предоставляет множество различных средств адресации одного или нескольких элементов HTML (с помощью селектора), так что к ним можно применять визуальные стили.

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

 Post::where("is_published", true) ->orderBy("published_at", "desc") ->take(6) ->skip(12) ->get(); 

Это пример кода, который вы можете увидеть в приложении Laravel. Он использует ORM под названием Eloquent для создания запроса к базе данных SQL .

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

 SELECT * FROM posts WHERE is_published = 1 ORDER BY published_at DESC LIMIT 12, 6; 

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

Если бы мы хотели создать наш собственный внешний DSL, нам нужно было бы преобразовать собственный синтаксис в код, понятный машине. Если не понимать, как работает ассемблер , мы могли бы перевести пользовательский синтаксис на язык более низкого уровня. Как и PHP.

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

 $numbers = [1, 2, 3, 4, 5]; print_r($numbers[2..4]); 

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

 function replace($matches) { return ' call_user_func(function($list) { $lower = '.explode('..', $matches[2])[0].'; $upper = '.explode('..', $matches[2])[1].'; return array_slice( $list, $lower, $upper - $lower ); }, $'.$matches[1].') '; } function parse($code) { $replaced = preg_replace_callback( '/\$(\S+)\[(\S+)\]/', 'replace', $code ); eval($replaced); } parse(' $numbers = [1, 2, 3, 4, 5]; print_r($numbers[2..4]); '); 

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

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

параллелизм

Давайте посмотрим на другую основную функцию: exec . Возможно, больше осуждено, чем даже eval , exec повсеместно осуждается всеми, кроме самых авантюрных разработчиков. И мне интересно, почему.

Если вы незнакомы, exec работает так:

 exec("ls -la | wc -l", $output); print $output[0]; // number of files in the current dir 

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

 exec("sleep 30 > /dev/null 2> /dev/null &"); 

Для этого: мы перенаправляем stdout и stderr в /dev/null и добавляем & в конец команды, которую мы хотим запустить в фоновом режиме. Есть много причин, по которым вы захотите сделать что-то подобное, но мое любимое — это возможность выполнять медленные и / или блокирующие задачи вне основного процесса PHP.

Изображение у вас был такой скрипт:

 foreach ($images as $image) { $source = imagecreatefromjpeg($image["src_path"]); $icon = imagecreatetruecolor(64, 64); imagecopyresampled( $source, $icon, 0, 0, 0, 0, 64, 64, $image["width"], $image["height"] ); imagejpeg($icon, $image["ico_path"]); imagedestroy($icon); imagedestroy($source); } 

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

Вот как мы можем запустить медленный код:

 exec("php slow.php > /dev/null 2> /dev/null &"); 

Мы могли бы даже сделать шаг вперед, сгенерировав динамический скрипт для запуска интерфейса командной строки PHP. Для начала мы можем установить SuperClosure :

 require __DIR__ . '/vendor/autoload.php'; use SuperClosure\Serializer; function defer(Closure $closure) { $serializer = new Serializer(); $serialized = $serializer->serialize($closure); $autoload = __DIR__ . '/vendor/autoload.php'; $raw = ' require \'' . $autoload . '\'; use SuperClosure\Serializer; $serializer = new Serializer(); $serialized = \'' . $serialized . '\'; call_user_func( $serializer->unserialize($serialized) ); '; $encoded = base64_encode($raw); $script = 'eval(base64_decode(\'' . $encoded . '\'));'; exec('php -r "' . $script . '"', $output); return $output; } $output = defer(function() { print "hi"; }); 

Зачем нам нужно жестко кодировать скрипт (для параллельного запуска), когда мы можем просто динамически генерировать код, который хотим запустить, и направлять его прямо в двоичный файл PHP?

Мы даже можем объединить этот трюк exec с eval , кодируя исходный код, который мы хотим запустить, и декодируя его при выполнении. Это делает команду для запуска подпроцесса намного более аккуратной в целом.

Мы даже можем добавить уникальный идентификатор, чтобы подпроцесс легче отслеживать и уничтожать:

 function defer(Closure $closure, $id = null) { // create $script if (is_string($id)) { $script = '/* id:' . $id . ' */' . $script; } $shh = '> /dev/null 2> /dev/null &'; exec( 'php -r "' . $script . '" ' . $shh, $output ); return $output; } 

Оставаться в безопасности

Основная причина, по которой многие разработчики не любят и / или не советуют использовать eval и exec заключается в том, что их неправильное использование приводит к гораздо более катастрофическим последствиям, чем, скажем, count .

Я бы посоветовал вместо того, чтобы слушать этих людей и немедленно отмахиваться от eval и exec , вы узнаете, как их безопасно использовать. Главное, чего вы хотите избежать — это использовать их с нефильтрованным пользовательским вводом.

Избегайте любой ценой:

 exec($_GET["op"] . " " . $_GET["path"]); 

Попробуйте вместо этого:

 $op = $_GET["op"]; $path = $_GET["path"]; if (allowed_op($op) and allowed_path($path)) { $clean = escapeshellarg($path); if ($op === "touch") { exec("touch {$clean}"); } if ($op === "remove") { exec("rm {$clean}"); } } 

… Или еще лучше: избегайте помещения любых пользовательских данных непосредственно в команду exec ! Вы также можете попробовать другие экранирующие функции, такие как escapeshellcmd . Помните, что это ворота в вашу систему. Все, что пользователь, выполняющий процесс PHP, может делать, exec разрешено делать. Вот почему он намеренно отключен на виртуальном хостинге.

Как и во всех основных функциях PHP, используйте их в качестве модераторов. Обратите особое внимание на данные, которые вы разрешаете, и избегайте нефильтрованных пользовательских данных в exec . Но не избегайте функций, не понимая, почему вы избегаете их. Это не полезно для вас или людей, которые будут учиться у вас.