Плагин aceAdminPanel – новые возможности для разработчиков плагинов

Прямо так и хочется начать: «Кролики – это не только ценный мех, но и три-четыре килограмма легкоусвояемого мяса» :)

А все потому, что плагин админки (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

31 комментарий

avatar
Да, вот это реально круто! Респект!
avatar
Мож я не совсем понял, но…
Если я делаю плагин, в котором наследую парсер и переопределяю метод parser, и еще один разработчик делает тоже самое, то все равно конфликт? или конфликта не будет и обработка подет по цепи? а если плагины хотят обработать один и тот же контент (я все про парсер) по разному… я вообще запулся… доку подробнее, пожалуйста.
avatar
или конфликта не будет и обработка подет по цепи?
Да, если в каждом методе будет вызов родительского метода — обработка пойдет по цепи.

а если плагины хотят обработать один и тот же контент (я все про парсер) по разному
Если, скажем, есть тег <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), то, ясень пень, вся отвественность уже ложится на меня.

Причем, сам понимаешь — ты сам определяешь, будет родительский вызов стоять ДО или ПОСЛЕ твоей обработки. Я же говорю — чистый ООП, и ничего более. :)
avatar
ок, спасибо за ответ. оффтоп вопрос — в какой последовательности плагины обрабатываются? как указать очередность??? вот засада, такой итересный фунциклир и обязательно ложка дегтя в бочку…
avatar
Все учтено могучим ураганом ©… :)
В последней версии админки есть возможность указывать, в каком порядке плагины должны загружаться. Более того, предусмотрено, что и XML-файле плагина тоже можно указывать приоритет загрузки. Я в админке вообще чуток расширил механизм работы с плагинами, но это, пожалуй, тема для отдельного топика.
avatar
О, пожалуйста, напишите. механизм наследования. очень интересный функционал в плане того, что не нужно беспокоится о совместимости плагинов. такой уж я перфекционист…
avatar
отличное решение, возможно реализуем подобное в стандартных плагинах
правда преимущество такого подхода — наследование, является одновременно и проблемой :) Оно хорошо работает, например, для парсера текста. А если несколько плагинов наследуют метод, который делает какие либо единичные изменения (например, добавление пользователя в БД), то при наследование вызывать parent::* нельзя, но отмена выполнения метода родителя приведет к нарушению работы предыдущего плагина.
avatar
Главное — это чтобы разработчики плагинов понимали смысл того, что делают, тогда проблемы сводятся к минимуму.

Беру твой пример — добавление юзера в БД. Для этого есть стандартный метод 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(), и добавляем марку авто юзера

Что же мы получили? Мы добавили нового юзера в штатном режиме, плюс добавили парочку дополнительных полей, которые используются в сторонних плагинах. И нет конфликтов, сплошной профит. :)
avatar
забыли что при этом два раза вызовется parent::Add($oUser); и из первого плагина и из второго
avatar
Еще раз цепочку вызовов внимательно посмотрите — я же специально все по шагам расписал. Для первого плагина вызов parent::Add($oUser) — это вызов стандартного метода (п.3), а для второго — это вызов метода первого плагина (п.2). Эти вызовы делаются НЕ поочередно, а вложены друг в друга. Поэтому стандартный метод User::Add() будет вызван только один раз.
avatar
аа, понятно, т.е. по сути плагин PlginSecond переопределяет метод не от User, а от PluginFirst
avatar
думаю как вариант переделать что бы исходный метод вызывался всегда и обязательно перед вызовами всех переопределяющих методов из плагинов, и оставить возможность переопределять исходный метод полностью(то как работает сейчас) как дополнительную фичу, малоли какая там может возникнуть задача
и вообще множественное наследование это зло :)
еще как вариант посмотреть на это ru.wikipedia.org/wiki/Декоратор_(шаблон_проектирования) и это ru.wikipedia.org/wiki/Мост_(шаблон_проектирования) но для обоих вариантов мне кажется нужно переделывать ядро
avatar
и вообще множественное наследование это зло
В данном механизме «множественным наследованием», даже и не пахнет. «Множественное наследование» — это когда от нескольких «родителей» создается один потомок. В данном случае — классический ООП, т.е. у каждого «потомка» один, и только один непосредственный «родитель»
avatar
Это понятно, тоже самое можно сделать и с помощью хуков. Проблема в том, что один плагин наследует метод от «кота в мешке» другого плагина. Если все плагины разрабатывает один человек — проблемы нет, он может сделать так, чтоб они корректно работали в связке, что ты и показал примером выше. Но когда один разработчик наследует свой метод от другого метода(метод другого плагина), то он по сути не знает от чего наследует и остается только надеяться, что ничего у него из-за этого не отвалится.
В твоем примере, логичнее использовать хук, т.к. вмешательств в функционал не происходит, а выполняются только дополнительные действия уже после выполнения основного метода.
А вот именно для переопределения методов твоя реализация подходит отлично и очень удобна.
avatar
В твоем примере, логичнее использовать хук...
А как?
avatar
можно повесить код на хук «module_user_add_after»
avatar
Поправь меня, если я ошибаюсь, но, по-моему, будет выполнен дишь один хук «module_user_add_after». Т.е. подключить несколько плагинов, каждый из которых как-то реагирует на добавление нового юзера, не получится.
avatar
будут выполнены все хуки в порядке их приоритетов, так что проблем с несколькими плагинами не возникнет
avatar
В модуле плагина Text.class.php оформить наследуемый класс особым образом:

Это объявление указывает, с одной стороны, на принадлежность класса плагину PluginMyparser1, а с другой – на то, что он наследуется от стандартного модуля Text.

Так мы все таки правим стандартный модуль? А если два разработчика захотят его поправить?
avatar
Так в том и фишка, что стандартный модуль не трогается, он наследуется в плагине.

А сам файл внутри плагинв вполне может называться так же, как и стандартный — Text.class.php. В разных плагинах вполне могут файлы с одинаковыми именами, но имя класса у них все равно разное.
avatar
Так и думал, что не правильно понял. Отличная идея, спасибо вам
avatar
т.е. мы получаем возможность переопределять и расширять стандартные классы не затрагивая их исходный код, правильно? и это только для модулей доступно?
avatar
т.е. мы получаем возможность переопределять и расширять стандартные классы не затрагивая их исходный код, правильно?
Абсолютно верно

и это только для модулей доступно?
Не только — и для экшенов, и для мапперов — для любых классов
avatar
функционал останется в будущих версиях или нет, в связи с вводом подобного в 0.4.1?
avatar
Я пока не анализировал детально, как это работает в 0.4.1, но если функционал один в один будет перенесен в ядро, то смысла нет в админке это оставлять. Если же полного совпадения не будет, то функционал обязательно останется, чтобы плагины, под него разработанные, продолжали работать.
avatar
гут, спасибо.
лучше конечно оставить, как раз пишу под «Ace», ибо неизвестно когда стабильный 0.4.1 выйдет
avatar
А с точки зрения интерфейса в любом случае останется, как есть. Ты даже и думать не будешь, сам функционал на стороне плагина моего выполняется или непосредственно в ядре.
avatar
Этот функционал по-прежнему существует только в этой админке, в ядро он еще не включен?
avatar
Отвечу сам себе.
Нет, этот функционал включен в ядро — livestreet.ru/blog/dev_documentation/4499.html
avatar
Да, с небольшими изменениями
avatar
зато эти изменения конфликтуют теперь с админкой
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.