Модификация классов Webasyst и в частности Shop-Script 6 без потери изменений при обновлениях

Кратко:

Как вносить изменения в код фреймворка Webasyst, приложений и плагинов, написанных на нём, так, чтобы не отказываться от обновлений, не отслеживать вручную изменившиеся файлы, при этом, чтобы изменения хранились отдельно от самого кода и хорошо поддавались контролю версий git или других VCS? В этой статье будет предложен почти идеальный способ.

Длинно и подробно:

Утверждение первое: Фреймворк Webasyst, на котором основан Shop-Script 6 (раньше был 5), обладает встроенным механизмом обновлений.
Этот механизм позволяет достаточно удобно применять обновления, выпускаемые компанией Webasyst.
Утверждение второе: При этом, расширяемость приложений, таких как Shop-Script базируется исключительно на использовании плагинов, которые должны использовать «хуки» для улучшения или изменения функциональности. Если вам не достаточно «хуков», те, что есть не дают возможностей влиять на нужные вам элементы функциональности, либо вам нужно изменить работу некоей функции самого фреймворка Webasyst, то плагины вам не подойдут.
Утверждение третье: Для изменения функциональности «на уровне ядра» вы можете изменить непосредственно сами классы Webasyst, ShopScript6 и других приложений, и именно это и советуют разработчики Webasyst на своём форуме поддержки в особо сложных случаях. Однако, благодаря встроенному механизму обновлений, ваши изменения могут легко быть затёрты при следующем обновлении.

Что можно сделать?
Способ «в лоб»:
Можно, при изменении каждого системного файла, копировать то, что получилось куда-нибудь в специальное место, откуда копировать обратно при каждом изменении. Но это утомительно, неудобно, и «некрасиво» с точки зрения архитектуры веб-приложения.
Идеальный способ:
Идеально было бы хранить изменения где-то в отдельных файлах, да так, чтобы они применялись к исходным файлам webasyst при запуске приложения, либо при деплое на удалённый сервер.

У меня есть «почти идеальное решение»!

Для реализации данных требований я написал небольшую библиотеку, под незамысловатым названием MonkeyPatching.
Взять её можно здесь: https://github.com/MihanEntalpo/MonkeyPatching

Что может MonkeyPatching?

MonkeyPatching может:

  • Заменять в PHP-классах существующие функции на новые
  • Добавлять новые функции в PHP-классы
  • Добавлять новые PHP-классы
  • При замене функции позволяет обращаться к старой версии заменённой функции
  • Полностью подменять содержимое не-PHP файлов, таких как html, css, js и других
  • Менять уровень доступа к функции (например, с private на public)

Как MonkeyPatching работает?

Изначально были попытки реализовать MonkeyPatching на основе php runkit, который позволяет на лету подменять функции классов, добавлять новые. Проблема с ним в двух моментах:

  • Он есть далеко не всяком хостинге, а там где его нет, отдельным модулем его поставить просто так не получится, этот модуль нужно будет компилировать
  • Он может заменить функцию только в классе который уже инициализирован (создан в интерпретаторе PHP)

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

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

Поэтому MonkeyPatching был сделан на базе так называемого StreamWrapper’а.
StreamWrapper — это встроенный класс PHP, позволяющий переопределять работу таких файловых операторов, как fopen, fread, fwrite, feof, fclose, fseek и так далее. При помощи наследования от StreamWrappera, можно полностью взять под контроль то, как PHP открывает, записывает и вообще работает с файлами. Документацию об этом можно найти здесь: https://secure.php.net/manual/en/class.streamwrapper.php.

В моём случае, при помощи StreamWrapper’а я перехватываю запросы к файлам, которые имеют «исправленную версию», и подменяю их содержимое при чтении. В случае с PHP файлами вызывается функционал подстановки функций, а в случае с html-файлами, они подменяются целиком.

Как пользоваться MonkeyPatching’ом?

Итак, вы решили попробовать MonkeyPatching в деле:

Иcходные данные

  • Предположим, у вас есть сайт на базе Webasyst, либо на любом другом фреймворке.
  • Пусть файлы этого сайта лежат в папке /var/www/mysite.local/public_html
  • Основной точкой доступа к сайту является файл index.php, лежащий там же, в public_html
  • В настройках php разрешено подключать PHP-файлы, находящиеся за пределами public_html (в противном случае вам понадобится класть все указанные далее папки внутрь public_html и соответственно менять пути)

Что нужно сделать:

1. Склонировать репозиторий рядом с папкой public_html (если из-за настроек безопасности вы не можете подключать PHP-скрипты не из папки public_html — тогда склонировать прямо в неё)

git clone git@github.com:MihanEntalpo/MonkeyPatching.git /var/www/mysite.local/MonkeyPatching

2. Дать права на запись в папку кэширования (она используется для хранения информации о пропатченных файлов, без неё работать ничего не будет).

chmod 0777 /var/www/mysite.local/MonkeyPatching/src/monkey_patching/cache

3. Создать папку, в которой будут лежать ваши патчи (php-файлы с доработками классов, либо html-файлы, заменяющие исходные файлы)

mkdir /var/www/mysite.local/patched_html

Имя папки patched_html выбрано созвучно с public_html, потому что структура папки patched_html будет частично повторять структуру папки public_html.

4. Зарегистрировать библиотеку в точке входа и запустить её в работу

Для этого, в самом начале файла index.php, являющегося вашей точкой входа, нужно добавить следующие строчки:

require_once __DIR__ . "/../MonkeyPatching/src/monkey_patching/MonkeyPatching.php";
MonkeyPatching::I("/var/www/mysite.local/public_html", "/var/www/mysite.local/patched_html")->go();

Функция I() возвращает синглтон-объект класса MonkeyPatching, её параметры — это папка с исходными файлами (public_html) и папка с файлами патчей (patched_html).

5. Проверим работоспособность библиотеки

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

К примеру, если у вас приложение на Webasyst, создадим патч для класса waSmarty3View, чтобы при создании объекта Smarty, который отвечает за рендеринг шаблонов, в нём отключалось отображение Notice’ов. Без этого очень тяжело отлаживать сайт, потому что, если ваш error_reporting выставлен в E_ALL (то есть отображает Notice’ы), то каждая неопределённая в шаблоне переменная вызывает Notice.
Поскольку неопределённые переменные в шаблонах как правило проблемой не являются, было бы неплохо, если бы error_reporting у Smarty был не такой, как у всего PHP-интерпретатора в целом.

Казалось бы, ничего сложного нет, берем объект smarty, и меняем его поле error_reporting:

$smarty->error_reporting = E_ALL & ~E_NOTICE;

Но не тут то было. Этот объект создаётся и хранится в классе waSmarty3View, и никакой возможности изменить его error_reporting нет. Здесь нам и пригодится MonkeyPatching:

5.1 Создадим файл, который будет содержать патч:
Класс waSmarty3View находится в файле public_html/wa-system/view/waSmarty3View.class.php

Значит наш файл нужно положить в аналогичное место, но уже в pached_html:

$ mkdir -p /var/www/mysite.local/patched_html/wa-system/view
$ nano /var/www/mysite.local/patched_html/wa-system/view/waSmarty3View.class.php

5.2 В этом файле определим класс waSmarty3View, а в нём определим конструктор

<?php
/**
 * Обезьяний патч файла waSmart3View чтобы подавить нотайсы
 */
class waSmarty3View
{
    /**
    * Изменения внесены для отключения нотайсов в смарти
    */
    public function __construct(waSystem $system, $options = array())
    {
	//Получим имя старого конструктора (ведь при патчинге функция __construct была переменована)
	$old_constructor = MonkeyPatching::get_old_function_name("waSmarty3View", "__construct");
	//Вызовем старый конструктор
	$this->{$old_constructor}($system, $options);
	//Отключение нотайсов в смарти
	$this->smarty->error_reporting = E_ALL & ~E_NOTICE;
    }
}

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

5.3 Проверяем

Чтобы проверить, как это работает, вам достаточно включить в настройках PHP параметр error_reporting = E_ALL, и открыть любую страницу сайта, в шаблоне которой (*.html) есть неопределенная переменная. Если такой шаблон найти не можете, дам подсказку:
1) Открываете файл public_html/wa-apps/site/templates/actions/settings/Settings.html
2) Вписываете в начале файла отображение несуществующей переменной, {$nonexistentVar}
3) Открываейте админ-панель вашего сайта на фреймворке Webasyst, и заходите по адресу /webasyst/site/#/settings/
Если всё получилось, то никаких Notice’ов вы не увидите. В противном случае (можете изменить ваш патч-файл, закомментировав в нём строку «$this->smarty->error_reporting = E_ALL & ~E_NOTICE;») увидите нотайс, о том что переменная не определена.

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

Обновления

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

Ограничения

  • MonkeyPatch пока не может заменять функции частично. Только полностью, только хардкор! Правда, эту проблему можно частично решить с помощью get_old_function_name
  • Вы можете заменять только функции в классах. Заменять глобальные функции пока нельзя.
  • Возможно, какие-то проблемы возникнут с namespace’ами, не проверял на них.

Дальнейшие планы:

  • Реализация возможности ускорения работы при деплое, то есть, когда вы выгружаете сайт на боевой сервер, можно будет подменять файлы фреймворка и приложений на пропатченные версии, что позволит работать ничуть не медленнее чем без MonkeyPatching. Сейчас же небольшую задержку при работе он создаёт, однако, судя по результатам профилирования, эта задержка составляет от 1% до 4% от всей работы веб-приложения.
  • Создание полноценного экрана отображения изменений файлов, который бы отображался в случае, если в исходных файлов с момента создания патча произошли изменения. На этом экране можно было бы отображать 3 редактора кода: с патчем, с исходным файлом (до обновления) и с конечным файлом (после обновления), с возможностью внести в патч изменения, сохранить его, и сбросить файл хэшей
  • Возможность патчить глобальные функции (не находящиеся в каких-то классах)

Оставить комментарий