
Причем, механизм расширения функционала движка должен позволять «вешать» на него любое число расширений, написанных разными разработчики, которые не знают ни друг о друге, ни о расширениях, которые пишут другие разработчики.
В различных движках это может делаться разными способами. Наиболее распространенный, наверное, это хуки – сторонний разработчик, создающий расширение для движка, регистрирует обработчики хуков, а потом эти обработчики вызываются системой в нужных местах, выполняя код расширения.
Но когда движок написан с использованием ООП и все разложено на классы, то использование хуков – как это чужеродно и «костыльно», и хочется более чистого и более простого ООП-подхода, когда в создаваемом расширении просто расширяется «коробочный» класс с перекрытием родительских методов.
Вот для решения таких задач и был придуман способ, который я назвал «Динамическое автонаследование».
И объяснять этот способ я буду на примере того, как это реализовано в системе поддержки плагинов в Alto CMS.
Допустим, есть исходный «коробочный» класс:
class ModuleUser {
public function Init() {
// Init code here
}
public function GetRecord() {
// Some code here
return $oRecord;
}
}
И у сторонних разработчиков возникает необходимость расширить класс ModuleUser, причем, один хочет изменить метод Init(), другой – метод GetRecord(), а третий – добавить своей логики в оба метода. И при этом надо обеспечить работоспособность всех трех расширений на любом сайте и в любой комбинации (т.е. где-то стоит одно расширение, где-то – другое, где-то – два из них, а где-то – и все три).
Итак, пусть сторонние разработчики независимо друг от друга пишут плагины, которые будут называться незатейливо First, Second и Third, и в каждом из них требуется класс-наследник от ModuleUser. В Alto CMS такие классы-наследники оформляются следующим образом:
class PluginFirst_ModuleUser extends PluginFirst_Inherits_ModuleUser {
public function Init() {
parent::Init();
// New code here
}
}
class PluginSecond_ModuleUser extends PluginSecond_Inherits_ModuleUser {
public function GetRecord() {
$oRecord = parent::GetRecord();
// Some code with $oRecord here
Return $oRecord;
}
}
class PluginThird_ModuleUser extends PluginThird_Inherits_ModuleUser {
public function Init() {
parent::Init();
// Init code here
}
public function GetRecord() {
// Yet another code here
return parent::GetRecord();
}
}
Вы, конечно же, обратили внимание, что классы наследуются не напрямую от родителя ModuleUser, а через классы-посредники — PluginFirst_Inherits_ModuleUser и т.д. Вот в этих классах-посредниках и заложена вся соль.
Кроме того, в плагинах в специальных свойствах указывается, что в них используется динамическое автонаследование от класса ModuleUser:
class PluginFirst extends Plugin {
/** @var array $aInherits Объявление переопределений (модули, мапперы и сущности) */
protected $aInherits = array(
'module' => array(
'ModuleUser',
),
);
// Plugin code here
}
Теперь при каждой загрузке ядра и инициализации подключенных плагинов будет создаваться стек наследований класса ModuleUser (пусть в нашем примере порядок будет такой: PluginFirst_ModuleUser, PluginSecond_ModuleUser, PluginThird_ModuleUser). И как только будет обращение к классу ModuleUser (создание экземпляра объекта выполняется через вызов специального метода), то автозагрузчик будет сначала проверять стек наследований и подгружать последний зарегистрированный там класс (в нашем примере — PluginThird_ModuleUser). При этом, разумеется, проверяется наличие класса-родителя PluginThird_Inherits_ModuleUser и т.к. его нет (а такого класса действительно нет), то делается попытка загрузить и его. И вот тут и начинается «магия».
Автозагрузчик анализирует имя родительского класса и понимает, что это – класс-посредник, его на самом деле не существует, а он является лишь алиасом предыдущего класса в стеке наследований, и сей факт закрепляет с помощью PHP-функции:
class_alias('PluginThird_Inherits_ModuleUser', 'PluginSecond_ModuleUser');
И теперь вместо класса-посредника PluginThird_Inherits_ModuleUser выполняется загрузка реального класса PluginSecond_ModuleUser. Его родитель – тоже класс-посредник, и он становится алиасом предыдущего класса из стека PluginFirst_ModuleUser. А вот PluginFirst_ModuleUser – это последний класс в стеке, поэтому его класс-родитель становится алиасом уже исходного «коробочного» класса ModuleUser.
В итоге цепочка наследований в системе получается такая:

Теперь, каждый раз, когда в системе будет запрос на создание экземпляра класса ModuleUser, то на самом деле объект будет создаваться от класса PluginThird_ModuleUser, и, с учетом цепочки наследований, он наследует все то, что было задумано разработчиками всех трех плагинов.
Преимущества принципа динамического автонаследования
- Никаких костыльных хуков, чистый ООП со всеми вытекающими
- Плагины (и классы-наследники) могут разрабатываться независимо друг от друга, нет необходимости знать, как в точности будет называться класс-родитель
- В цепочке наследования может быть неограниченное число классов, которые автоматически будут наследоваться друг от друга
Недостатки:
- Как верно было замечено в комментах, IDE таких «фокусов» не поймут, ибо наследование «динамическое»
- … — а вот иных существенных недостатков я, честно говоря, не вижу.
Не исключаю, что какие-то недостатки, все же, могут быть, а у меня просто «глаз замылися», и носом я в них ни разу не ткнулся, поэтому будет интересно услышать мнение тех, кто таковые обнаружит.
Так же будет интересно узнать, встречал ли кто подобный способ расширения функционала в других CMS (мне до сих пор такого не попадалось).
И как я уже писал, подобная методика лежит в основе системы плагинов Alto CMS, поэтому если кто-то захочет посмотреть реализацию подробней, то это можно сделать либо на гитхабе, либо скачать код движка с официального сайта.
PS На всякий случай для перфекционистов от кодирования — да, я знаю про неймспейсы и понимаю, что все может быть оформлено с их помощью (и даже соглашусь, что желательно их тут использовать), но пост все же о другом, поэтому предлагаю не зацикливаться на этой стороне вопроса.