Мы можем использовать существующий фреймворк плагинов для переопределения большинства базовых классов Joomla. И вы сможете убедиться в том, что это довольно легко сделать.

Как подключаются плагины

В статье "Общая информация о плагинах" мы говорили о том, что для вызова плагина сначала используется JPluginHelper::importPlugin() для включения его класса и методов в рабочую память. Если мы присмотримся к тому, как работает этот метод, мы увидим, что код (который выполняет подключение) расположен в приватном методе import() класса JPluginHelper (libraries/joomla/plugin/helper.php):
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(). Первые строки метода 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). В этом примере мы продемонстрируем, как переопределить класс, но оставим читателю возможность придумать, какой именно код и поведение хотелось бы изменить.

Вот шаги, которые необходимо предпринять:
  1. Создайте новую папку plugins/system/myclasses в директории установки Joomla и скопируйте туда файл libraries/joomla/database/tablenested.php. В итоге вы получите файл plugins/system/myclasses/tablenested.php (не забудьте добавить файл index.html для всех создаваемых папок).
  2. Отредактируйте новый файл и замените существующий метод rebuild() следующим кодом:
    public function rebuild($parentId = null, $leftId = 0, $level = 0, $path = '')
    {
        exit('Из файла myclasses/tabelnested.php');
    }
    Этот код просто докажет, что был загружен наш переопределенный класс, вместо базового класса. Когда мы нажмем "Перестроить" (например, в "Менеджере категорий: Материалы"), программа должна будет сделать выход с сообщением "Из файла myclasses/tabelnested.php".

  3. Теперь мы должны добавить плагин для загрузки нашего класса вместо базового класса. Мы назовем плагин "myclasses". Для этого, создайте новый файл с именем myclasses.php в папке plugins/system/myclasses.
  4. В новый файл ( plugins/system/myclasses/myclasses.php) добавьте следующий код:
    /**
     * Демонстрация плагина для замены базового класса.
     * Он исполняется перед первым импортом системы (перед
     * событием onBeforeInitialise).
     */
    
    // Запрет прямого доступа.
    defined('_JEXEC') or die;
    
    // Заменяем базовый класс JTableNested переопределенной версией.
    include_once JPATH_ROOT.'/plugins/system/myclasses/tablenested.php';
    Обратите внимание, что это код не объявляет класс. Это просто скрипт, а значит он будет исполнен во время подключения системных плагинов, перед первым системным событием. Этот код просто включает наш новый файл tablenested.php.

  5. Создайте 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>
  6. Зайдите в панель управления Joomla, выберите "Менеджер расширений", выполните "Поиск" и установите плагин. Не забудьте включить плагин в "Менеджере плагинов".
  7. Зайдите в "Материалы" -> "Менеджер категорий" и кликните на "Перестроить". 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
)

А теперь совсем правильно ))

Поскольку переопределение классов имеет пару недостатков (крупных).

  1. Переопределенный класс болтается в памяти, а значит занимает ее вне зависимости,используется он или нет.
  2. Не все классы можно переопределить.
  3. Этими методами нельзя изменить код вне классов.
  4. Нельзя переопределить классы подгружаемые напрямую без использования JLoader.

Всем этим не страдает единственный метод - прямое редактирование файлов )).

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

Почему проверка при загрузке админки, а не морды? - во первых, чтобы не грузить лишними манипуляциями морду, а во вторых, потому, что все обновления в joomla происходят в админке, после чего, в любом случае, происходит загрузка панели администратора. То есть при любых манипуляциях в админке стстема всегда выходит на начальную точку.

Это метод реализован мной в плагине.