Используем плагины для переопределения базовых классов
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 происходят в админке, после чего, в любом случае, происходит загрузка панели администратора. То есть при любых манипуляциях в админке стстема всегда выходит на начальную точку.
Это метод реализован мной в плагине.