5 January 2015

Хук ООП не друг или Динамическое автонаследование классов

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

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

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

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

Вот для решения таких задач и был придуман способ, который я назвал «Динамическое автонаследование».

И объяснять этот способ я буду на примере того, как это реализовано в системе поддержки плагинов в 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 На всякий случай для перфекционистов от кодирования — да, я знаю про неймспейсы и понимаю, что все может быть оформлено с их помощью (и даже соглашусь, что желательно их тут использовать), но пост все же о другом, поэтому предлагаю не зацикливаться на этой стороне вопроса.
Tags:livestreetalto cmscms разработкаалгоритмырасширения
Hubs: CMS PHP ООP
-6
4.9k 33
Comments 36
Popular right now
PHP Backend разработчик
from 80,000 to 120,000 ₽Мир РыболоваМоскваRemote job
Full Stack разработчик
to 98,000 ₽КЛЮЧАВТОКраснодар
Разработчик Битрикс24
to 150,000 ₽ФинФортКраснодарRemote job
php-разработчик
from 50,000 to 100,000 ₽Mark WeberКазань
Программист PHP
from 150,000 to 200,000 ₽КАУСМосква
Top of the last 24 hours