Используем плагины для переопределения базовых классов
if (!isset($paths[$path])) { require_once $path; } $paths[$path] = true;
$paths
является ассоциативным массивом, содержащим все плагины, которые уже были подключены. Ключом является полный путь до файла плагина, а значением является имя класса. Используя PHP функцию isset()
мы проверяем, есть ли такой элемент в массиве. Если нет, то с помощью require_once
подключаем этот файл. И наконец, значение этого элемента устанавливается в логическое true
, что гарантирует установку этого элемента в массиве. Поэтому require_once
уже не будет вызвано для того же самого файла.Здесь необходимо понять две важные вещи:
- как уже обсуждалось ранее, обычно в плагине объявляется класс, поэтому не происходит вызова кода. Единственное что происходит, класс и его методы загружаются в рабочую память для того, чтобы потом его методы могли быть вызваны в цикле. В данном случае, в результате выполнения метода
JPluginHelper::importPlugin()
не происходит исполнения кода. - ничто в Joomla не заставляет плагин быть объявлением класса. Плагин может быть простым PHP скриптом - таким, который бы исполнялся сразу при его подключении. Если мы сделаем такой плагин, то он будет вызван незамедлительно после вызова метода
JPluginHelper::importPlugin()
. Это предоставляет механизм загрузки PHP скриптов при подключении плагинов.
Как загружаются классы Joomla
Теперь нам необходимо понять важную вещь о том, как в Joomla происходит загрузка базовых классов в рабочую память. Если мы взглянем на функциюjimport
, которая обычно используется для загрузки базовых классов Joomla, мы увидим, что это просто функция в файле libraries/loader.php. Обратите ваше внимание на то, что это независимая функция, а не метод класса. Именно поэтому она вызывается только с именем функции и без имени класса. Вот код этой функции:function jimport($path) { return JLoader::import($path); }
JLoader::import()
следующие:// Only import the library if not already attempted. if (!isset(self::$imported[$key]))
Это проверка - подключен ли класс или нет. Переменная
self::$imported
является статическим ассоциативным массивом с ключом (переменная $key
), равным аргументу, переданному в JImport
(например, "joomla.plugin.plugin"), и значением, равным логическому true
или false
. Когда класс подключен, элемент добавляется в массив и значение устанавливается в true
, если подключение было выполнено успешно, и в false
, если неуспешно. Поэтому, как только класс был подключен, Joomla не будет пытаться подключить его ещё раз.Методы JLoader::load(), JLoader::register() также проверяют до загрузки класса, не был ли класс загружен ранее. И здесь мы делаем важный вывод: если класс уже существует (загружен в рабочую память), мы пропускаем загрузку этого класса. Метод просто возвращает значение
true
и выходит. Ни один из методов Joomla не загрузит класс повторно.Это означает, что мы можем использовать плагин для загрузки класса в рабочую память перед тем, как он будет загружен базовыми программами Joomla. Если мы сделаем это, то методы нашего класса будут использоваться вместо методов базового класса.
Системные плагины очень рано загружаются в рабочую память в цикле исполнения Joomla, раньше большинства (но не всех) базовых классов Joomla. Это поможет нам достичь желаемого результата.
Пример: переопределение класса JTableNested
Давайте сделаем быстрый пример для иллюстрации вышеописанного. Мы переопределим базовый класс JTableNested. Этот класс является родительским классом для всех классов вложенных таблиц в Joomla (например, JTableCategory для таблицы#__categories
). В этом примере мы продемонстрируем, как переопределить класс, но оставим читателю возможность придумать, какой именно код и поведение хотелось бы изменить.Вот шаги, которые необходимо предпринять:
- Создайте новую папку plugins/system/myclasses в директории установки Joomla и скопируйте туда файл libraries/joomla/database/tablenested.php. В итоге вы получите файл plugins/system/myclasses/tablenested.php (не забудьте добавить файл index.html для всех создаваемых папок).
- Отредактируйте новый файл и замените существующий метод
rebuild()
следующим кодом:
public function rebuild($parentId = null, $leftId = 0, $level = 0, $path = '') { exit('Из файла myclasses/tabelnested.php'); }
Этот код просто докажет, что был загружен наш переопределенный класс, вместо базового класса. Когда мы нажмем "Перестроить" (например, в "Менеджере категорий: Материалы"), программа должна будет сделать выход с сообщением "Из файла myclasses/tabelnested.php". - Теперь мы должны добавить плагин для загрузки нашего класса вместо базового класса. Мы назовем плагин "myclasses". Для этого, создайте новый файл с именем myclasses.php в папке plugins/system/myclasses.
- В новый файл ( plugins/system/myclasses/myclasses.php) добавьте следующий код:
/** * Демонстрация плагина для замены базового класса. * Он исполняется перед первым импортом системы (перед * событием onBeforeInitialise). */ // Запрет прямого доступа. defined('_JEXEC') or die; // Заменяем базовый класс JTableNested переопределенной версией. include_once JPATH_ROOT.'/plugins/system/myclasses/tablenested.php';
Обратите внимание, что это код не объявляет класс. Это просто скрипт, а значит он будет исполнен во время подключения системных плагинов, перед первым системным событием. Этот код просто включает наш новый файл tablenested.php. - Создайте XML-файл манифеста для этого плагина ( plugins/system/myclasses/myclasses.xml) со следующим кодом:
<?xml version="1.0" encoding="utf-8"?> <extension version="2.5" type="plugin" group="system"> <name>plg_system_myclasses</name> <author>Mark Dexter and Louis Landry</author> <creationDate>November 2012</creationDate> <copyright>Copyright (C) 2012 Mark Dexter and Louis Landry.</copyright> <license>GPL2</license> <authorEmail>admin [at] joomla.org</authorEmail> <authorUrl>www.joomla.org</authorUrl> <version>1.0.0</version> <description>Демонстрация плагина MyClasses</description> <files> <filename plugin="myclasses">myclasses.php</filename> <filename>index.html</filename> </files> <config> </config> </extension>
- Зайдите в панель управления Joomla, выберите "Менеджер расширений", выполните "Поиск" и установите плагин. Не забудьте включить плагин в "Менеджере плагинов".
- Зайдите в "Материалы" -> "Менеджер категорий" и кликните на "Перестроить". Joomla должна остановиться и вы должны увидеть сообщение "Из файла myclasses/tabelnested.php". Это покажет нам, что мы успешно переопределили базовый класс.
Если таким образом вы переопределяете класс, то вам не стоит беспокоиться о том, что он будет переписан при обновлении Joomla. Поэтому эта техника намного лучше простого хака файлов ядра. Однако, стоит предупредить о следующем - если будет исправлен баг в классе, который вы переопределили, вам необходимо будет проверить, относится ли это исправление к вашему классу. Если это так, то вы должны будете сами внести это исправление вручную. Это будет особенно важно, если исправление багов касается уязвимостей в безопасности.
И под конец статьи небольшой трюк. Вы можете добавить следующий код в начало плагина, чтобы выяснить, какие классы уже загружены и не могут быть переопределены с помощью описанного в статье способа:
echo '<pre>'; print_r(JLoader::getClassList()); echo '</pre>'; die();
А теперь как правильно.
Для того, чтобы не только сохранить изменения в классе при обновлении системы, но и обеспечить возможность автоматического обновления измененных классов надо создать новый класс дочерний по отношению к исходному, но при этом сохранить ему имя исходного класса. для этого читаем файл исходного класса в переменную, переименовываем в класс и загружаем его в память при помощи встроиной функции eval, а на его место загружаем корректирующий класс под именем исходного.Код плагина:
<?php // No direct access defined('_JEXEC') or die; $app = JFactory::getApplication(); if($app->isSite()) { // Считываем базовый класс ContentModelArticles . $ContentModelArticlesOld = implode('',file(JPATH_ROOT . '/components/com_content/models/articles.php')); // переименовываем его $ContentModelArticlesOld = str_replace ( ' ContentModelArticles ', ' ContentModelArticles_Edit_contentmodelcategory ', $ContentModelArticlesOld ); // отрезаем <?php $ContentModelArticlesOld = substr($ContentModelArticlesOld,5); // запускаем переименованный класс eval($ContentModelArticlesOld); // Вызываем новый класс ContentModelArticles на замену include_once JPATH_ROOT . '/plugins/system/edit_contentmodelcategory/articles.php'; }
Файл замещающего класса articles.php:
<?php /** * @copyright Copyright (C) 2005 - 2013 Open Source Matters, Inc. All rights reserved. * @license GNU General Public License version 2 or later; see LICENSE.txt */ defined('_JEXEC') or die; jimport('joomla.application.component.modellist'); /** * This models supports retrieving lists of articles. * * @package Joomla.Site * @subpackage com_content * @since 1.6 */ class ContentModelArticles extends ContentModelArticles_Edit_contentmodelcategory { function getListQuery () { $query = & parent::getListQuery(); $dispatcher = & JDispatcher::getInstance(); JPluginHelper::importPlugin( 'content' ); $dispatcher->trigger( 'onContentAfterGetListQuery',array( & $query)); return $query; } }
Файл манифеста плагина:
<?xml version="1.0" encoding="utf-8"?> <install version="1.5" method="upgrade" type="plugin" group="system"> <name>Edit_contentmodelcategory</name> <creationDate>2013-05-19</creationDate> <author>Vadim Rigin</author> <authorEmail>vadim@rigin.net<;/authorEmail> <authorUrl>http://www.rigin.net</authorUrl> <copyright>Copyright (C) 2013 Vadim Rigin Open Source Matters. All rights reserved.</copyright> <license>http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL</license> <version>1</version> <description></description> <files> <filename plugin="edit_contentmodelcategory">edit_contentmodelcategory.php</filename> <filename>index.html</filename> <filename>articles.php</filename> </files> </install>
Этот плагин вставляет событие для перехвата переменной sql запроса перед его исполнением для модели списка материалов категории
И под конец статьи небольшой трюк. Вы можете добавить следующий код в начало плагина, чтобы выяснить, какие классы уже загружены и не могут быть переопределены с помощью описанного в статье способа:
echo '<pre>'; print_r(JLoader::getClassList()); echo '</pre>'; die();
Собственно вот этот список.
Array ( [jfactory] => D:\www\opiproject\libraries/joomla\factory.php [jexception] => D:\www\opiproject\libraries/joomla\error\exception.php [jrequest] => D:\www\opiproject\libraries/joomla\environment\request.php [jobject] => D:\www\opiproject\libraries/joomla\base\object.php [jtext] => D:\www\opiproject\libraries/joomla/methods.php [jroute] => D:\www\opiproject\libraries/joomla/methods.php [jlogger] => D:\www\opiproject\libraries/joomla\log\logger.php [logexception] => D:\www\opiproject\libraries/joomla/log/logexception.php [jloggerdatabase] => D:\www\opiproject\libraries\joomla\log/loggers/database.php [jloggerecho] => D:\www\opiproject\libraries\joomla\log/loggers/echo.php [jloggerformattedtext] => D:\www\opiproject\libraries\joomla\log/loggers/formattedtext.php [jloggermessagequeue] => D:\www\opiproject\libraries\joomla\log/loggers/messagequeue.php [jloggersyslog] => D:\www\opiproject\libraries\joomla\log/loggers/syslog.php [jloggerw3c] => D:\www\opiproject\libraries\joomla\log/loggers/w3c.php [jpath] => D:\www\opiproject\libraries/joomla\filesystem\path.php [jdate] => D:\www\opiproject\libraries/joomla\utilities\date.php [jrule] => D:\www\opiproject\libraries/joomla/access/rule.php [jrules] => D:\www\opiproject\libraries/joomla/access/rules.php [jmenu] => D:\www\opiproject\libraries/joomla\application\menu.php [juri] => D:\www\opiproject\libraries/joomla\environment\uri.php [jutility] => D:\www\opiproject\libraries/joomla\utilities\utility.php [jdispatcher] => D:\www\opiproject\libraries/joomla\event\dispatcher.php [jarrayhelper] => D:\www\opiproject\libraries/joomla\utilities\arrayhelper.php [jinput] => D:\www\opiproject\libraries/joomla\application\input.php [jresponse] => D:\www\opiproject\libraries/joomla\environment\response.php [jcomponenthelper] => D:\www\opiproject\libraries/joomla\application\component\helper.php [jprofiler] => D:\www\opiproject\libraries/joomla\error\profiler.php [jinputcli] => D:\www\opiproject\libraries\joomla\application/input/cli.php [jinputcookie] => D:\www\opiproject\libraries\joomla\application/input/cookie.php [jinputfiles] => D:\www\opiproject\libraries\joomla\application/input/files.php [jconfig] => D:\www\opiproject/configuration.php [jregistryformat] => D:\www\opiproject\libraries\joomla\registry/format.php [jtable] => D:\www\opiproject\libraries/joomla\database\table.php [jfolder] => D:\www\opiproject\libraries/joomla\filesystem\folder.php [jdatabasemysql] => D:\www\opiproject\libraries\joomla\database/database/mysql.php [jdatabasequerymysql] => D:\www\opiproject\libraries\joomla\database\database/mysqlquery.php [jdatabaseexportermysql] => D:\www\opiproject\libraries\joomla\database\database/mysqlexporter.php [jdatabaseimportermysql] => D:\www\opiproject\libraries\joomla\database\database/mysqlimporter.php [jcachestorage] => D:\www\opiproject\libraries\joomla\cache/storage.php [jcachecontroller] => D:\www\opiproject\libraries\joomla\cache/controller.php [jfile] => D:\www\opiproject\libraries/joomla\filesystem\file.php [jstream] => D:\www\opiproject\libraries/joomla\filesystem\stream.php [jplugin] => D:\www\opiproject\libraries/joomla\plugin\plugin.php )
А теперь совсем правильно ))
Поскольку переопределение классов имеет пару недостатков (крупных).
- Переопределенный класс болтается в памяти, а значит занимает ее вне зависимости,используется он или нет.
- Не все классы можно переопределить.
- Этими методами нельзя изменить код вне классов.
- Нельзя переопределить классы подгружаемые напрямую без использования JLoader.
Всем этим не страдает единственный метод - прямое редактирование файлов )).
Но у него есть один недостаток - это редактирование затирается при обновлении. Но этот недостаток можно победить системным плагином, запускаемым в начальной точке загрузки админки. Он должен читать измененные файлы, проверять их на наличие хаков, и если хаки отсутствуют - вставлять нужный код в файл и записывать его по новой.
Почему проверка при загрузке админки, а не морды? - во первых, чтобы не грузить лишними манипуляциями морду, а во вторых, потому, что все обновления в joomla происходят в админке, после чего, в любом случае, происходит загрузка панели администратора. То есть при любых манипуляциях в админке стстема всегда выходит на начальную точку.
Это метод реализован мной в плагине.