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_method_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().

Спасибо!

Комментарии (25)


  1. FractalizeR
    25.09.2015 10:28
    +1

    А как продвигается работа в направлении поддержки PHP7? Релиз уже близко :)


    1. dzenovich
      25.09.2015 11:05
      +3

      Владислав, спасибо за вопрос. Дело вот в чем, чтобы Runkit завелся под PHP7, нужно проделать большую работу, на длительное время сделать это основной своей деятельностью. Но надо же при этом что-то есть. Так что вся надежда на пожертвования пользователей: если это будут нормальные достойные суммы и, главное, если таких пожертвований будет много, то Runkit под PHP7 быстро станет реальностью.

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


    1. OnYourLips
      25.09.2015 13:17

      А зачем использовать Runkit на PHP7? Он нужен, чтобы заткнуть архитектурные проблемы старого и/или неподдерживаемого кода.
      Если же позаботиться о переводе такого кода на PHP7, то можно и проблемы решить, и необходимость в Runkit исчезнет.


      1. dzenovich
        25.09.2015 13:19
        +2

        Предполагаю, что для модульного тестирования и песочниц. И еще для обновления кода на лету. Кроме того, программы на PHP5 очень легко переедут на PHP7.


      1. FractalizeR
        25.09.2015 14:27
        +2

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

        Я бы не был столь категоричен. RunKit используется по-разному. И аналога многим его возможностям в PHP7 нет.


  1. maxru
    25.09.2015 10:58
    +2

    Ух ты, он живой :)
    Отличная работа, спасибо.


    1. dzenovich
      25.09.2015 11:07

      Пользуйтесь на здоровье! :)


  1. taliban
    25.09.2015 11:02
    +3

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


    1. youROCK
      25.09.2015 11:24

      В продакшен-системе его использовать и правда не стоит, а вот для юнит-тестов и создания моков без необходимости использовать DI это расширение очень даже ничего.


      1. taliban
        25.09.2015 11:38
        +3

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


        1. dzenovich
          25.09.2015 11:52
          +1

          Если не заменять заглушками тестируемые функции, то ошибки никуда не скроются, все будет хорошо. А как без такого инструмента тестировать поведение методов, не поддерживающих dependency injection?


          1. Fedot
            25.09.2015 18:57

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


            1. dzenovich
              25.09.2015 19:52

              Да, AspectMock может, если у вас PHP 5.4+ и вы никогда не делаете заглушки для функций.
              На мой взгляд, заглушки для функций (в том числе для функций, встроенных в PHP) в тестах бывают очень полезны.


              1. Fedot
                25.09.2015 22:56

                Что значит заглушки для функций? AspectMock как раз и позволяет мокать любые функции.
                В том числе стандартные функции PHP пруф


                1. dzenovich
                  26.09.2015 01:28
                  +1

                  Простите, недооценил AspectMock.

                  Тут, как говорится, на вкус на цвет. Можно по-настоящему заменять методы и функции в рантайме на заглушки, а можно вместо каждого include() сначала делать file_get_contents(), потом preg_replace(), оборачивающий всевозможные места в коде (вызовы функций, тела методов и т.п.) в if'ы с заглушками, а потом получившийся код eval()'ить, как это примерно и происходит в Go-AOP, на основе которого работает AspectMock.

                  В 2006-м я использовал и оба подхода параллельно, сейчас мне больше нравится Runkit.


                  1. Fedot
                    26.09.2015 03:32

                    В целом вы правы это на вкус и цвет.
                    Но у runkit есть существенный минус в том что это отдельное расширение для PHP и его нужно ставить отдельно.
                    И всего пол года назад я его поставить и вовсе не смог, а точнее не смог использовать, по причине сегфолтов. У AspectMock такого минуса нет.

                    К слову не «как это происходит в Go-AOP», а собственно AspeckMock на Go-AOP и построен =)


                    1. dzenovich
                      26.09.2015 12:23

                      Полгода назад релиза 1.0.4 еще не было и PHP 5.5 и 5.6 не поддерживались. Надеюсь, сейчас у вас никаких сегфолтов Runkit не вызывает. Если что-то не работает — ставьте issue на github'е, починю.


                      1. andrewnester
                        27.09.2015 12:42

                        В Go-AOP для стандартных функций не используется eval, там фишка с namespace


      1. dzenovich
        25.09.2015 11:40

        А кстати, почему бы не использовать в продакшене? Я знаю один высоконагруженный проект, в котором это делали еще в 2006-2007-м.


        1. Adelf
          25.09.2015 12:25

          Есть у меня мнение, что у этого высоконагруженного проекта был плохой код. Не нужен runkit на продакшене. Да даже в тестах, опять-таки он нужен, когда код плох.


          1. dzenovich
            25.09.2015 12:31
            +1

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


        1. maar
          25.09.2015 12:30

          Дима, разве в этом высоконагруженном проекте runkit не только для тестов использовался?


          1. dzenovich
            25.09.2015 12:32

            В том проекте он не использовался в тестах ;)


    1. batyrmastyr
      06.10.2015 18:23

      Если глючит сторонний код (библиотеки, каркаса или расширения), то почему бы и не «починить», то есть подменить его таким макаром? Всё лучше чем ждать у моря погоды.


      1. dzenovich
        06.10.2015 19:08

        Ситуации, когда нельзя или нет смысла править сторонний код, бывают очень разные, в каждой из них свои причины. Сторонний код может быть закрытым (например, с помощью энкодера), наложение патча на чужой код при каждом обновлении может оказаться намного дороже, чем monkey patching, «починка» может противоречить видению авторов библиотеки или нуждам остальных ее пользователей и т.д. и т.п.