Новое в Runkit 1.0.4: PHP 5.6+, closures везде и еще 12 новых фич
Runkit 1.0.4 для PHP выпущен!
Поздравляю всех пользователей Runkit с новым долгожданным мега-релизом! Если вы постоянно используете Runkit и хорошо знакомы с его возможностями, историей и развитием, то можете сразу переходить к описанию изменений релиза 1.0.4. В любом случае предлагаю прочесть статью целиком.
Что такое Runkit?
Runkit – это расширение языка PHP, позволяющее делать вещи, невозможные с точки зрения этого языка. Функционал расширения состоит из трех частей.
Runtime Manipulations
Первая и самая крупная часть функционала Runkit позволяет динамически (в процессе выполнения PHP-программы) копировать, изменять и удалять такие сущности, динамическое изменение которых самим языком PHP не предусмотрено.
Runkit позволяет копировать, переопределять и удалять существующие функции (в том числе встроенные в язык), динамически делать класс потомком другого класса, наследуя всё содержимое (runkit_class_adopt), или откреплять класс от родителя, удаляя унаследованное содержимое (runkit_class_emancipate). Также можно добавлять, копировать, переопределять и удалять методы существующих классов, добавлять и удалять их свойства. Кроме того, Runkit позволяет переопределять и удалять определенные ранее константы.
Runkit_Sandbox
Вторая большая часть функционала – «песочницы» Runkit_Sandbox. Они позволяют выполнять часть программы на PHP в изолированном окружении. У каждой «песочницы» могут быть по-своему настроены параметры безопасности PHP такие как safe_mode, safe_mode_gid, safe_mode_include_dir, open_basedir, allow_url_fopen, disable_functions, disable_classes. Кроме того, каждая «песочница» может по-своему настраивать внутри себя функционал Runkit: проставлять свои суперглобальные переменные (о них речь пойдет ниже) и запрещать изменение встроенных функций.
«Песочницы» могут подключать PHP-файлы (через include(), include_once(), require() и require_once()), вызывать внутри себя функции, выполнять произвольный код на PHP, печатать значения своих внутренних переменных, завершать свою работу. Кроме того, можно указать функцию для перехвата и обработки вывода «песочницы».
Внутри «песочницы» также можно создать объект «анти-песочницы» Runkit_Sandbox_Parent для связи «песочницы» с родительским окружением. Функционал «анти-песочниц» очень похож на функционал «песочниц», но из соображений безопасности, каждая связующая с внешним окружением функция должна быть явно включена при создании «песочницы».
Superglobals
Runkit также позволяет добавлять в PHP новые суперглобальные переменные. Чтобы добавить такие переменные, достаточно перечислить их имена через запятую в свойстве runkit.superglobal внутри файла конфигурации PHP.
Прочее
Помимо трех основных частей функционала в Runkit также есть средства для проверки синтаксиса кода на PHP (runkit_lint и runkit_lint_file) и функция runkit_import, позволяющая импортировать PHP-файл подобно include, но игнорирующая весь глобальный код в этом файле. В зависимости от флагов runkit_import может импортировать функции или классы (полностью или частично), переопределяя или сохраняя уже существующие.
Зачем нужен Runkit?
Runkit помогает PHP-программистам решать множество различных задач. Расскажу о нескольких основных.
Патчинг чужих программ
Представьте, что вы используете стороннюю библиотеку (или фреймворк) и в какой-то момент вам понадобилось изменить ее поведение. Однако код, который нужно изменить находится в private-методе одного из классов библиотеки. Очевидное решение — отредактировать файл, содержащий этот метод. Это рабочее решение, однако код библиотеки теперь изменен и ее обновление становится хлопотной задачей, потому что нужно будет применять патч при каждом обновлении библиотеки. Другое решение — с помощью Runkit переопределить интересующий нас метод, это делается с помощью одного вызова функции runkit_method_redefine. Аналогичное решение есть для переопределения уже существующих в программе функций (runkit_function_redefine) и констант (runkit_constant_redefine). Подобное изменение кода программы во время выполнения называется «monkey patching». На специализированных интернет-форумах можно найти различные рецепты патчинга с помощью Runkit таких библиотек как WordPress, 1С-Битрикс, CodeIngniter, Laravel и т.п. Для решения некоторых проблем бывает полезно заменять функции, встроенные в сам язык PHP, и Runkit это тоже умеет.
Изолированное окружение для выполнения пользовательских скриптов
С помощью «песочниц» Runkit_Sandbox часто делают окружения для выполнения пользовательского кода. При правильной настройке это дает возможность изолировать пользовательский код от основной системы. В простейшем виде это выглядит так:
$options = […];
$sandbox = new Runkit_Sandbox($options);
$sandbox->ini_set(…);
$sandbox->eval($code);
Другие варианты использования
С помощью runkit можно также организовать обновление кода программы на лету, как это, например, делается в phpdaemon (см. habrahabr.ru/post/104811).
Юнит-тесты
Возможности Runkit по переопределению функций и методов делают его крайне полезным при написании unit-тестов на PHP. С помощью Runkit изготовление тестовых двойников (заглушек или «шпионов») во время выполнения тестов становится простым делом, даже если архитектура тестируемого кода не поддерживает dependency injection. Существуют готовые библиотеки, реализующих подмену методов и функций PHP на заглушки в контексте unit-тестов (например, ytest, phpspy и другие). При правильном выборе библиотеки можно получить изумительно простые тесты (см. например, здесь).
История развития Runkit
Начало
Runkit был создан в 2005-м году Сарой Големон (Sara Golemon). Последний авторский релиз (версия 0.9) был выпущен 06.06.06. В октябре 2006-го года Сара перестала поддерживать расширение, так и не выпустив версию 1.0. На тот момент Runkit содержал в себе функции для манипулирования константами, функциями, методами, runkit_import, функцию добавления свойств в классы, функции проверки синтаксиса, песочницы и суперглобальные переменные. Документация на сайте php.net (http://php.net/runkit) застыла в районе версии 0.7, так что в ней до сих пор не описана даже часть функций, сделанных самой Сарой. Кроме того, в этой документации весь функционал Runkit называется экспериментальным, что было актуально в 2006-м, но абсолютно не соответствует действительности сейчас.
Упадок
С октября 2006-го по октябрь 2009-го расширение никем не поддерживалось, а язык PHP шел вперед, из-за чего, несмотря на правки от участников PHP-сообщества, уже в версии PHP 5.2 Runkit работал нестабильно и вызывал ошибки сегментации.
Возрождение
В октябре 2009-го я стал чинить Runkit, а потом и развивать его на https://github.com/zenovich/runkit. Расскажу, какие релизы выпущены за это время и какие изменения в них включены.
Релиз 1.0.0 (1 апреля 2010 года)
На самом деле этого релиза никогда не было, он фиктивный :). К нему относятся все правки сообщества после выпуска версии 0.9 и до релиза 1.0.1.
Релиз 1.0.1 (3 октября 2010 года)
Первый настоящий релиз Runkit после 2006-го года. Теперь Runkit поддерживает все версии PHP до 5.3 включительно. Исправлено более десяти серьезных ошибок, в том числе приводивших к падениям PHP. Основные из них:
- устранены падения при импорте через runkit_import() свойств и констант со значениями-массивами,
- устранены падения при импорте функций и методов со статическими переменными,
- устранено падение при манипуляциях с функциями,
- устранено падение runkit_method_copy при работе с protected методами,
- устранено падение при завершении работы PHP после изменения встроенных функций,
- устранено падение при вызове исходного метода после применения к нему функции runkit_method_copy, если в методе были статические переменные,
- имена создаваемых методов больше не переводятся в нижний регистр.
В релизе 1.0.1 добавлена возможность определять и модифицировать статические методы с помощью новой константы RUNKIT_ACC_STATIC:
runkit_method_add('MyClass', 'newMethod', '$arg1, $arg2', '/* some code here*/', RUNKIT_ACC_STATIC);
Также добавлена возможность импортировать статические свойства класса. При импорте класса статические свойства будут копироваться по умолчанию, однако их импорт можно отключить с помощью новой константы RUNKIT_IMPORT_CLASS_STATIC_PROPS:
runkit_import('myfile.inc', RUNKIT_IMPORT_CLASSES); // импортировать классы целиком
runkit_import('myfile.inc', RUNKIT_IMPORT_CLASSES & ~ RUNKIT_IMPORT_CLASS_STATIC_PROPS); // импортировать классы, но не импортировать их статические свойства
runkit_import('myfile.inc', RUNKIT_IMPORT_CLASS_STATIC_PROPS); // импортировать только статические свойства классов
Кроме того, в релизе была добавлена возможность применять замыкание к песочнице с помощью Runkit_Sandbox::call_user_func().
Релиз 1.0.2 (5 октября 2010 года)
Баг-фикс предыдущего релиза. Улучшена совместимость с PHP 5.3.
Релиз 1.0.3 (2 января 2012 года)
Исправлено наследование при переименовании методов с помощью runkit_method_rename. Починена сборка расширения под Windows.
Релиз 1.0.4 (25 сентября 2015 года)
Долгожданный Мега-Релиз! Полная поддержка PHP5 (вплоть до PHP 5.6 включительно).
В этом релизе было сделано очень много для стабилизации работы Runkit: тесты прогонялись для каждой версии PHP в четырех вариантах: c ZTS и без, под valgrind и без. Практически по каждому изменению добавлялись новые тесты. Благодаря этому удалось выявить и исправить огромное количество ошибок.
Среди важных исправлений можно выделить следующие:
- устранены падения при изменении, удалении или переименовании функций, методов и свойств классов, для которых ранее были созданы объекты Reflection,
- устранено падение при создании Runkit_Sandbox при включенной настройке register_globals,
- устранено падение при ошибке синтаксиса в файле, загружаемом через runkit_import(),
- устранено падение при работе с константами, имеющими имена из одного символа,
- устранено падение при вызове переименованного private или protected метода.
Всего в релизе было сделано больше сорока (!!!) важных исправлений, их полный список можно посмотреть в файле package.xml.
Теперь расскажу о главных изменениях функционала.
Функции и методы
Closures
Для PHP 5.3+ функции runkit_function_add, runkit_function_redefine, runkit_method_add и runkit_method_redefine теперь поддерживают замыкания (closure) в качестве параметров. Например, если раньше для переопределения функции нужно было писать выражение вида
runkit_function_redefine('sprintf', '$s', 'return $s;');
которое для превращения строки в байт-код использовало eval, что очень медленно, то теперь можно писать
runkit_function_redefine('sprintf', function($s) {return $s;});
Никаких eval’ов при этом не выполняется, к тому же поддерживать такой код намного проще – больше нет частей программы внутри строковых литералов! То же самое касается функций runkit_function_add, runkit_method_add и runkit_method_redefine.Магические методы
Также в Runkit теперь полностью поддержаны манипуляции с магическими методами __get, __set, __isset, __unset, __clone, __call, __callStatic, serialize, unserialize, __debugInfo и __toString. То же самое касается конструкторов и деструкторов как при современном способе наименования, так и при наименовании в стиле PHP4.Doc-comments
Теперь при добавлении или переопределении методов и функций с помощью старого синтаксиса (когда аргументы новой функции и ее тело передаются строками) можно указывать doc-comment’ы. С этой целью у функций runkit_function_add, runkit_function_redefine, runkit_method_add и runkit_method_redefine появился новый опциональный (последний по порядку) аргумент – doc_comment:
runkit_method_redefine('MyClass','myMethod', '$arg', 'return $arg', RUNKIT_ACC_PRIVATE, 'my doc_comment'); // переопределяет приватный метод с doc-comment’ом
runkit_method_add('MyClass','myMethod2', '$arg', 'return $arg', NULL, 'my doc_comment2'); // добавляет приватный метод с doc-comment’ом
При определении функций и методов в новом стиле (через замыкания) doc-comment’ы можно задавать так же, как это делается при определении обычных функций, – через комментарии над телом функции. Оба способа можно комбинировать – приоритет у doc-comment’а, переданного через аргумент. Кроме того, было починено проставление doc-comment’ов при наследовании, копировании и переименовании методов и функций.Возврат значений по ссылке
Добавлена возможность добавлять и переопределять функции и методы так, чтобы новая функция (или метод) возвращала значение по ссылке. Для того чтобы новая функция, заданная с использованием старого синтаксиса (когда аргументы новой функции и ее тело передаются строками), возвращала значение по ссылке, нужно передать в функцию runkit_function_add (или runkit_function_redefine) новый аргумент – return_ref – со значением TRUE. Например,
runkit_function_redefine('my_function', '$a', 'return $a;', TRUE); // возвращает значение по ссылке
При аналогичном добавлении (или переопределении) метода используется аргумент flags с установленным битом RUNKIT_ACC_RETURN_REFERENCE. Например,
runkit_function_redefine('MyClass', 'myMethod', '$a', 'return $a;', RUNKIT_ACC_PROTECTED | RUNKIT_ACC_RETURN_REFERENCE); // protected-метод возвращает значение по ссылке
Если же вы определяете функцию или метод с помощью нового синтаксиса (через замыкания), то все эти флаги вам не нужны – достаточно добавить амперсанд перед списком аргументов функции:
runkit_function_redefine('my_function', function &($a) {return $a;}); // возвращает значение по ссылке
Свойства классов
Внутренняя реализация манипуляций со свойствами классов была полностью переработана. Добавление, удаление и импорт свойств класса теперь правильно отражаются на классах-потомках. Более того, теперь эти действия могут влиять и на объекты класса и его потомков. Чтобы включить такое влияние, нужно установить бит RUNKIT_OVERRIDE_OBJECTS в аргументе flags при вызове функций runkit_default_property_add и runkit_default_property_redefine. Например,
runkit_default_property_add('MyClass', 'newProperty', 'value'); // не влияет на объекты класса и его потомков
runkit_default_property_add('MyClass', 'newProperty', 'value', RUNKIT_OVERRIDE_OBJECTS); // добавит свойство не только в классы и классы-потомки, но и в их объекты
То же самое касается и импорта свойств классов:
runkit_import('myfile.inc', RUNKIT_IMPORT_CLASS_PROPS); // импортирует свойства классов, не переопределяя существующие свойства и не затрагивая объекты
runkit_import('myfile.inc', RUNKIT_IMPORT_CLASS_PROPS | RUNKIT_IMPORT_OVERRIDE); // импортирует свойства классов, переопределяя существующие свойства, но не затрагивая объекты
runkit_import('myfile.inc', RUNKIT_IMPORT_CLASS_PROPS | RUNKIT_IMPORT_OVERRIDE | RUNKIT_OVERRIDE_OBJECTS); // импортирует свойства классов, переопределяя существующие свойства и изменяя свойства в объектах
Кроме того, была добавлена новая функция runkit_default_property_remove() для удаления свойств из классов. Чтобы удалять свойство не только из класса, но и из его объектов у функции runkit_default_property_remove есть третий необязательный параметр:
runkit_default_property_remove('MyClass', 'myProperty'); // удаляет свойство из класса, но оставляет его в объектах
runkit_default_property_remove('MyClass', 'myProperty', TRUE); // удаляет свойство из класса и из его объектов
Также теперь при добавлении и переопределении свойств классов теперь можно использовать не только скалярные значения, но и массивы.Классы
Раньше функции runkit_class_adopt и runkit_class_emancipate хотя и меняли содержимое классов, но не влияли на их иерархию (т.е. после применения runkit_class_adopt у класса формально по-прежнему не было родителя, а после runkit_class_emancipate родитель по-прежнему оставался). Теперь это исправлено.Регистр в именах сущностей и namespaces
Работа с константами, функциями, методами и свойствами теперь полностью поддерживает namespace’ы. Также Runkit перестал переводить в нижний регистр названия свойств, классов, методов и функций, которые он создает (как это было раньше).Дополнительная безопасность песочниц
Для песочниц Runkit_Sandbox теперь можно отключать INI-настройку allow_url_include. Также теперь, независимо от платформы, настройка open_basedir поддерживает списки путей (раньше можно было ввести только один путь).Обновления
Обновлять Runkit стало намного проще. Теперь это можно делать привычным для всех пользователей PECL способом через новый канал zenovich.github.io/pear. Канал подключается одной командой:
pear channel-discover zenovich.github.io/pear
Далее, чтобы установить последний релиз Runkit’а, достаточно набрать
pecl install zenovich/runkit
Кроме того, все архивы с релизами теперь доступны по адресу https://github.com/zenovich/runkit/releases.
Заключение
Сейчас Runkit используется во многих известных компаниях и проектах по всему миру как для unit-тестирования, так и для многих других задач. Уверен, что впереди его ждет большое будущее. Это станет возможным благодаря пожертвованиям, которые теперь можно делать одним кликом со страницы проекта github.com/zenovich/runkit или прямо из phpinfo().
Спасибо!