Прямо так и хочется начать: «Кролики – это не только ценный мех, но и три-четыре килограмма легкоусвояемого мяса» :)
А все потому, что плагин админки (aceAdminPanel) – это не только облегчение работы администратора сайта, но и новые возможности для разработчиков, пишущих различные расширения для движка. Здесь я расскажу об одной интересной фиче, которую я смог реализовать в плагине, и которая дает гораздо больше возможностей при создании плагинов, чем стандартные средства.
Начну с того, что плагины, появившиеся в последнем релизе – это классная вещь. Особенно вкупе с хуками. Механизм расширения функционала движка стал значительно гибче и универсальнее. Проблемы совместимости плагинов разных разработчиков решаются гораздо легче.
Но мне все равно как-то не хватало еще большей гибкости. Мне не хватало настоящего ООП, чтоб я мог без «костылей» расширить любой класс, входящий в стандартный движок, и чтоб любой разработчик мог сделать то же самое, причем, без всякого согласования со мной. И пусть хоть десяток плагинов расширяют один и тот же класс, если они функционально не конфликтуют друг с другом.
Идея, как это сделать, пришла довольно быстро. Но потребовалось некоторое время, чтобы ее реализовать. И вот сегодня выложил билд, где это, наконец, реализовано в более-менее завершенном виде.
Тем, кому аббревиатура ООП ничего не говорит, дальше можно не читать. Остальным покажу на примере, как этот механизм работает.
Допустим, решили вы как-то по-своему дополнить обработку текстов парсером и создаете для этого плагин, например, PluginMyparser1. Пишете плагин, как обычно, но с учетом двух нюансов.
Нюанс 1.
class PluginMyparser1 extends AcePlugin {
public $aInherits=array(
'module' => array('Text')
);
/* ... */
}
Как видите, плагин создается от класса AcePlugin, а не от стандартного Plugin. К тому же, в классе плагина PluginMyparser1 надо объявить свойство $aInherits. Синтаксис точно такой же, как и у известного уже разработчикам $aDelegates, только указываются в нем классы, которые будут не «делегироваться», а «наследоваться», ровно в том смысле, как это понимается в ООП.
В примере выше указывается, что модуль плагина PluginMyparser1_Text будет наследоваться от стандартного модуля Text. Замечу, что тут дана сокращенная запись. Возможны следующие формы записи:
public $aInherits=array('module' => array('Text'));
public $aInherits=array('module' => array('Text'=>'_Text'));
public $aInherits=array('module' => array('Text'=>' PluginMyparser1_Text'));
Все эти три записи абсолютно идентичны.
Нюанс 2.
В модуле плагина Text.class.php оформить наследуемый класс особым образом:
class PluginMyparser1_Text extends PluginMyparser1_Inherits_Text {
}
Это объявление указывает, с одной стороны, на принадлежность класса плагину PluginMyparser1, а с другой – на то, что он наследуется от стандартного модуля Text.
И всё! Теперь можно программировать этот класс так, как если бы он явно наследовался от модуля Text, например:
public function Parser($sText) {
$sResult=parent::Parser ($sText);
// здесь вы дополнительно обрабатываете текст
return $sResult;
}
И, что самое главное, точно также может поступить любой другой, независимый от вас, разработчик, создающий свой плагин, расширяющий модуль Text.
Теперь о том, как это работает. Допустим, у нас есть три разных плагина, каждый из которых расширяет стандартный метод Text::Parser(), добавляя какие-то свои фишки по обработке текста, и у нас есть три разных класса: PluginMyparser1_Text,PluginMyparser2_Text, PluginMyparser3_Text. И плагины эти грузятся именно в таком порядке. Тогда «иерархия наследования» будет выглядеть так:
Модуль Text => PluginMyparser1_Text => PluginMyparser2_Text => PluginMyparser3_Text
Понятно, что метод Text::Parser() я привел лишь для примера. На самом деле один плагин может этот метод перекрывать, другой — метод Text::JevixConfig(), в общем — чистый ООП и никаких фокусов с хуками и иными «костылями».
Спасибо тем, кто дочитал до конца! Удачи вам в разработке! :)
ЗЫ Забыл добавить — данная фича в полной мере реализована в админке версии 1.4-dev.60
Мож я не совсем понял, но…
Если я делаю плагин, в котором наследую парсер и переопределяю метод parser, и еще один разработчик делает тоже самое, то все равно конфликт? или конфликта не будет и обработка подет по цепи? а если плагины хотят обработать один и тот же контент (я все про парсер) по разному… я вообще запулся… доку подробнее, пожалуйста.
Да, если в каждом методе будет вызов родительского метода — обработка пойдет по цепи.
а если плагины хотят обработать один и тот же контент (я все про парсер) по разному
Если, скажем, есть тег <video> и есть два плагина, которые его заменяют на что-то другое, то кто первым доберется до него — тот и схавает.
Берем мой пример: ты разработал плагин PluginMyparser1, а я — PluginMyparser2.
И я в наследуемом методе пишу:
public function Parser($sText) {
$sResult=parent::Parser ($sText);
sResult='<b>'.sResult.'</b>'; // моя обработка текста PluginMyparser2_Text
return $sResult;
}
А ты в своей обработке текста, например, добавляешь тег перевода строки:
public function Parser($sText) {
$sResult=parent::Parser ($sText);
sResult .= '< br/>'; // твоя обработка текста в PluginMyparser1_Text
return $sResult;
}
На выходе получим:
<b>Text< br/></b>
Т.е. если я в своем методе вызываю метод родителя, и ты в своем методе вызываешь метод родителя, то отработает и стандартный метод, и твой, и мой. Т.е. разработчик должен понимать, что его класс не является «монопольным отпрыском», и что его родителем может быть другой плагин, и его потомок тоже, и тогда все будет в порядке. Конечно, если я всю отработку метода возьму на себя и обойдусь без вызова parent::Parser($sText), то, ясень пень, вся отвественность уже ложится на меня.
Причем, сам понимаешь — ты сам определяешь, будет родительский вызов стоять ДО или ПОСЛЕ твоей обработки. Я же говорю — чистый ООП, и ничего более. :)
ок, спасибо за ответ. оффтоп вопрос — в какой последовательности плагины обрабатываются? как указать очередность??? вот засада, такой итересный фунциклир и обязательно ложка дегтя в бочку…
О, пожалуйста, напишите. механизм наследования. очень интересный функционал в плане того, что не нужно беспокоится о совместимости плагинов. такой уж я перфекционист…
отличное решение, возможно реализуем подобное в стандартных плагинах
правда преимущество такого подхода — наследование, является одновременно и проблемой :) Оно хорошо работает, например, для парсера текста. А если несколько плагинов наследуют метод, который делает какие либо единичные изменения (например, добавление пользователя в БД), то при наследование вызывать parent::* нельзя, но отмена выполнения метода родителя приведет к нарушению работы предыдущего плагина.
Главное — это чтобы разработчики плагинов понимали смысл того, что делают, тогда проблемы сводятся к минимуму.
Беру твой пример — добавление юзера в БД. Для этого есть стандартный метод User::Add(). Я пишу плагин PluginFirst, где наследую модуль User и там перекрываю этот метод так:
class PluginFirst_User extends PluginFirst_Inherits_User {
public function Add($oUser) {
$oUser = parent::Add($oUser);
if ($oUser) {
// А здесь код для добавления аськи юзера
}
return $oUser;
}
}
Другой разработчик пишет плагин PlginSecond, где тоже наследуется модуль User и перекрывается этот метод:
class PlginSecond_User extends PlginSecond_Inherits_User {
public function Add($oUser) {
$oUser = parent::Add($oUser);
if ($oUser) {
// А здесь код для добавления марки авто юзера
}
return $oUser;
}
}
И что произойдет, когда будет вызван где-то метод $this->User_Add($oUser)?
Расписываем по пунктам:
1. Будет вызван метод PlginSecond_User::Add()
2. Из него будет вызван родительский метод PluginFirst_User::Add()
3. Из него будет вызван стандартный метод User::Add()
4. Возвращаемся в PluginFirst_User::Add(), и (если все ок) добавляем аську юзера
5. Возвращаемся в PlginSecond_User::Add(), и добавляем марку авто юзера
Что же мы получили? Мы добавили нового юзера в штатном режиме, плюс добавили парочку дополнительных полей, которые используются в сторонних плагинах. И нет конфликтов, сплошной профит. :)
Еще раз цепочку вызовов внимательно посмотрите — я же специально все по шагам расписал. Для первого плагина вызов parent::Add($oUser) — это вызов стандартного метода (п.3), а для второго — это вызов метода первого плагина (п.2). Эти вызовы делаются НЕ поочередно, а вложены друг в друга. Поэтому стандартный метод User::Add() будет вызван только один раз.
думаю как вариант переделать что бы исходный метод вызывался всегда и обязательно перед вызовами всех переопределяющих методов из плагинов, и оставить возможность переопределять исходный метод полностью(то как работает сейчас) как дополнительную фичу, малоли какая там может возникнуть задача
и вообще множественное наследование это зло :)
еще как вариант посмотреть на это ru.wikipedia.org/wiki/Декоратор_(шаблон_проектирования) и это ru.wikipedia.org/wiki/Мост_(шаблон_проектирования) но для обоих вариантов мне кажется нужно переделывать ядро
В данном механизме «множественным наследованием», даже и не пахнет. «Множественное наследование» — это когда от нескольких «родителей» создается один потомок. В данном случае — классический ООП, т.е. у каждого «потомка» один, и только один непосредственный «родитель»
Это понятно, тоже самое можно сделать и с помощью хуков. Проблема в том, что один плагин наследует метод от «кота в мешке» другого плагина. Если все плагины разрабатывает один человек — проблемы нет, он может сделать так, чтоб они корректно работали в связке, что ты и показал примером выше. Но когда один разработчик наследует свой метод от другого метода(метод другого плагина), то он по сути не знает от чего наследует и остается только надеяться, что ничего у него из-за этого не отвалится.
В твоем примере, логичнее использовать хук, т.к. вмешательств в функционал не происходит, а выполняются только дополнительные действия уже после выполнения основного метода.
А вот именно для переопределения методов твоя реализация подходит отлично и очень удобна.
Поправь меня, если я ошибаюсь, но, по-моему, будет выполнен дишь один хук «module_user_add_after». Т.е. подключить несколько плагинов, каждый из которых как-то реагирует на добавление нового юзера, не получится.
В модуле плагина Text.class.php оформить наследуемый класс особым образом:
…
Это объявление указывает, с одной стороны, на принадлежность класса плагину PluginMyparser1, а с другой – на то, что он наследуется от стандартного модуля Text.
Так мы все таки правим стандартный модуль? А если два разработчика захотят его поправить?
Так в том и фишка, что стандартный модуль не трогается, он наследуется в плагине.
А сам файл внутри плагинв вполне может называться так же, как и стандартный — Text.class.php. В разных плагинах вполне могут файлы с одинаковыми именами, но имя класса у них все равно разное.
т.е. мы получаем возможность переопределять и расширять стандартные классы не затрагивая их исходный код, правильно? и это только для модулей доступно?
Я пока не анализировал детально, как это работает в 0.4.1, но если функционал один в один будет перенесен в ядро, то смысла нет в админке это оставлять. Если же полного совпадения не будет, то функционал обязательно останется, чтобы плагины, под него разработанные, продолжали работать.
А с точки зрения интерфейса в любом случае останется, как есть. Ты даже и думать не будешь, сам функционал на стороне плагина моего выполняется или непосредственно в ядре.
31 комментарий
Если я делаю плагин, в котором наследую парсер и переопределяю метод parser, и еще один разработчик делает тоже самое, то все равно конфликт? или конфликта не будет и обработка подет по цепи? а если плагины хотят обработать один и тот же контент (я все про парсер) по разному… я вообще запулся… доку подробнее, пожалуйста.
Если, скажем, есть тег <video> и есть два плагина, которые его заменяют на что-то другое, то кто первым доберется до него — тот и схавает.
Берем мой пример: ты разработал плагин PluginMyparser1, а я — PluginMyparser2.
И я в наследуемом методе пишу:
А ты в своей обработке текста, например, добавляешь тег перевода строки:
На выходе получим:
Т.е. если я в своем методе вызываю метод родителя, и ты в своем методе вызываешь метод родителя, то отработает и стандартный метод, и твой, и мой. Т.е. разработчик должен понимать, что его класс не является «монопольным отпрыском», и что его родителем может быть другой плагин, и его потомок тоже, и тогда все будет в порядке. Конечно, если я всю отработку метода возьму на себя и обойдусь без вызова parent::Parser($sText), то, ясень пень, вся отвественность уже ложится на меня.
Причем, сам понимаешь — ты сам определяешь, будет родительский вызов стоять ДО или ПОСЛЕ твоей обработки. Я же говорю — чистый ООП, и ничего более. :)
В последней версии админки есть возможность указывать, в каком порядке плагины должны загружаться. Более того, предусмотрено, что и XML-файле плагина тоже можно указывать приоритет загрузки. Я в админке вообще чуток расширил механизм работы с плагинами, но это, пожалуй, тема для отдельного топика.
правда преимущество такого подхода — наследование, является одновременно и проблемой :) Оно хорошо работает, например, для парсера текста. А если несколько плагинов наследуют метод, который делает какие либо единичные изменения (например, добавление пользователя в БД), то при наследование вызывать parent::* нельзя, но отмена выполнения метода родителя приведет к нарушению работы предыдущего плагина.
Беру твой пример — добавление юзера в БД. Для этого есть стандартный метод User::Add(). Я пишу плагин PluginFirst, где наследую модуль User и там перекрываю этот метод так:
Другой разработчик пишет плагин PlginSecond, где тоже наследуется модуль User и перекрывается этот метод:
И что произойдет, когда будет вызван где-то метод $this->User_Add($oUser)?
Расписываем по пунктам:
1. Будет вызван метод PlginSecond_User::Add()
2. Из него будет вызван родительский метод PluginFirst_User::Add()
3. Из него будет вызван стандартный метод User::Add()
4. Возвращаемся в PluginFirst_User::Add(), и (если все ок) добавляем аську юзера
5. Возвращаемся в PlginSecond_User::Add(), и добавляем марку авто юзера
Что же мы получили? Мы добавили нового юзера в штатном режиме, плюс добавили парочку дополнительных полей, которые используются в сторонних плагинах. И нет конфликтов, сплошной профит. :)
и вообще множественное наследование это зло :)
еще как вариант посмотреть на это
В твоем примере, логичнее использовать хук, т.к. вмешательств в функционал не происходит, а выполняются только дополнительные действия уже после выполнения основного метода.
А вот именно для переопределения методов твоя реализация подходит отлично и очень удобна.
Так мы все таки правим стандартный модуль? А если два разработчика захотят его поправить?
А сам файл внутри плагинв вполне может называться так же, как и стандартный — Text.class.php. В разных плагинах вполне могут файлы с одинаковыми именами, но имя класса у них все равно разное.
Не только — и для экшенов, и для мапперов — для любых классов
лучше конечно оставить, как раз пишу под «Ace», ибо неизвестно когда стабильный 0.4.1 выйдет
Нет, этот функционал включен в ядро — livestreet.ru/blog/dev_documentation/4499.html