Pull to refresh

MovableType — отрывки из разработки плагинов

Reading time10 min
Views824
MovableType — не сильно популярная в нашей стране, однако весьма стоящая на посмотреть система ведения блогов. Из коробки поддерживает множество блогов, с 5й версии — еще и управление полноценное сайтом. Работает на perl, путем генерации множества статических html файлов, за счет чего очень хорошо выдерживает даже большие нагрузки.

Распространяется в двух вариантах: OpenSource и Pro. Плагины к нему распространяются в полу-бесплатном режиме: смотреть смотрите, юзать платите.

В целом, система написана в духе ООП, есть хуки на почти каждый чих, есть ORM, всё почти хорошо…

Установка плагинов


Итак, момент первый: установка плагинов. Установка плагинов состоит из:
  • Заливаем папочку плагина в $mt/plugins/<плагин> кода плагина
  • Заливаем папочку статических файлов плагина в $mtpub/mt-static/plugins/<плагин>
  • Проверяем не требуется ли заливки каких-либо библиотек в $mt/extlib/
  • Внимательно изучаем документацию на тему «не надо ли поставить какие-либо библиотеки к perl'у»
  • Внимательно изучаем документацию на тему «как мне теперь переписать шаблоны»

В зависимости от функционала и сложности плагина последние два пункта выливаются иногда в нетривиальную задачу. Некоторые производители плагинов даже разработали специальные плагины для упрощения этих задач, например TemplateInstaller от mt-hacks.com.
Поэтому, если вы решили написать плагин под MT, хорошо подумайте над тем, чтобы телодвижений пользователю нужно было делать как можно меньше.

Описание плагина для MT


Итак, мы решили родить плагин. Создали под него структуру папок:
plugins/coolplugin/
plugins/coolplugin/lib/
plugins/coolplugin/tmpl/
mt-static/plugins/coolplugin/

И задумались: о чем писать?

Для начала, напишем config.yaml. Начиная с некоторой (4.хх вроде как) версии, описание плагина можно сделать в простой и доступной форме через config.yaml файл, а не заниматься добавлением кучи кода в главном файле плагина. Итак, что же может и должен содержать config.yaml?
А никто не знает. Если у вас появится желание узнать что же там может быть — welcome, изучайте $mt/lib/MT/Plugin.pm и чужие плагины.

Наиболее полезные вещи, это:
name: Имя вашего плагина. Рекомендуется недлинная такая строка, чтоб влезало.
id: ИмяПлагинаВОдноСлово
author_link: Ссылка на автора плагина
author_name: Имя автора плагина
description: <__trans phrase="Описание плагина, появляется если в меню плагинов щёлкнуть по имени плагина">
version: ВерсияПлагина
schema_version: ВерсияСхемыДанныхПлагина
plugin_link: Ссылка на страницу где можно слить плагин
doc_link: Ссылка на страницу где описание плагина
blog_config_template:  имя_шаблона_со_страницей_конфигурации.tpl
l10n_class: Плагин::КлассИнтернационализации
icon: КартинкаПлагина.{gif/png/jpg/itd}
settings:
    какаятонастройка:
        default: "значениеподефолту"
        scope: blog
    глобальнаянастройка:
        default: "значениеее"

init: $Плагин::Модуль::функция_инициализации

callbacks:
    НекийХак: $Плагин::Модуль::функция_хака

tags:
    function:
        ИмяТега: $Плагин::Модуль::функция_тега
    block:
        ИмяБлочногоТега: $Плагин::Модуль::фукция_блочного_тега

object_types:
    объект:
        поле: тип # это поле будет добавлено к обекту в бд
        поле: тип meta # это поле будет храниться в таблице метаданных и не потребует обновления стурктуры бд

applications:
    # об этом потом, пока забыли


Врядли вам понадобятся все разделы сразу, но отсутствие вменяемого описания по поводу того, что тут может быть, создаст вам еще много проблем.
Выше я описал всё, с чем сталкивался сам. Даже такая мелочь как icon: была обнаружена в коде самого MT, а никак не в документации.

Настройка плагина


О настройке написано много. MT предлагает вам на выбор: создать страницу настройки самостоятельно, или воспользоваться автогенератором. Если ваш плагин настраивается 2-3-4 простыми строками/чекбоксами/итп, разумеется, гораздо проще и быстрее взять автогенератор. В общем, в азах вам поможет статья "HOWTO: Собственная страница настроек плагина" от Адепта MT Byrne Reese (Byrne реально знает крайне много о MT, его посты советую почитать).

Я же хочу описать несколько моментов, связанных с конфигурацией, описание которых найти мне не удалось нигде:

Валидация данных при настройке плагина

Сам MT нигде практически не производит валидации данных. Максимум — сохраняет после приведения к нужному типу, поэтому найти как же проводить валидацию оказалось не так-то просто.

Итак, вот у вас есть класс плагина, который был настроен в init хуке (см. выше config.yaml). Сам метод (например, лежит в plugins/OurPlugin/lib/Plugin.pm) выглядит в простейшем случае так:
package OurPlugin::Plugin
use base 'MT::Plugin';
sub cb_init {
    my $p = shift;
    return bless $p, 'OurPlugin::Plugin';
}

и описан в config.yaml так:
init: $OurPlugin::Plugin::cb_init

Значит, в момент сохранения будет вызван OurPlugin::Plugin:save_config, которому передадут три аргумента: наш объект, данные формы в хеше и область конфигурации (scope).

Поскольку у нас уже стоит base MT::Plugin, если мы ничего не будем делать — то всё, что навводил юзер, будет сохранено как-есть. Если мы хотим провести какую-либо валидацию, нам надо:
sub save_config {
    my ($plugin, $args, $scope) = @_;
    my @errors;
    # провести валидацию или совершить обряд черной магии с данными тут

    if (@errors) {
        return $plugin->error("<br />\n".join("<br />\n", @errors));
    }
    return $plugin->SUPER::save_config($args, $scope);
} ## end sub save_config


Это выдаст жуткую страницу ошибки при сохранении, которая будет иметь кнопку «назад». Поэтому? можно пойти другим путем: добавим еще функцию load_config:
sub load_config {
    my ($plugin, $args, $scope) = @_;
    $plugin->SUPER::load_config($args, $scope);
    # Проверим тут что-нибудь на тему соответствия, и выставим $plugin->error() если есть
    if($plugin->errstr) {
      # Set $args->{error} to display error banner right before plugin settings
      $args->{error} = $plugin->errstr;
    }
} ## end sub load_config

А в шаблон конфигурации вставляем кусок
    <mt:if name="error">
        <mtapp:statusmsg
            id="generic-error"
            class="error">
            <mt:var name="error">
        </mtapp:statusmsg>
    </mt:if>


Таким образом, сохранятся произвольные данные, но при открытии окна настроек пользователь увидит что где не так. Каким путем идти — вам решать, можно использовать оба способа для разных случаев жизни, важно помнить, что внутри шаблона конфигурации плагина не работают многие стандартные теги. То есть они доступны, но выдают лабуду, так как шаблон компилируется в отдельном независимом контексте, так что там нет даже ID блога, для котрого генерируются настройки — и вот тут опять придёт на помощь load_config, чтобы установить в $args->{...} нужные переменные, без которых не получится нормально выдать пользователю подсказки какие-либо.

Подключение jQuery для настройки плагина

Очень веселенький момент. MT5 пользуется jQ вовосю, но в MT4 его нет. При этом некоторые плагины для MT4 и так уже грузят jQ, так что если мы просто будем грузить jQ всегда — будет конфликт во всю голову.
Для себя я решил это таким куском в blog_config.tmpl:
<mt:If tag="Version" lt="5">
  <script type="text/javascript"> // prevent double-load of jQuery, save if jQ already loaded its state
    if(window.jQuery) {
      window.__PLUG_jQ = window.jQuery;
      window.__PLUGB = window.$;
    }
  </script>
  <script type="text/javascript" src="<$mt:StaticWebPath$>jquery/jquery.js"></script>
  <script type="text/javascript"> // Restore there jQ state, if it was loaded before
    if(window.__PLUG_jQ) {
      window.jQuery = window.__PLUG_jQ;
      window.$ = window.__PLUGB;
      window.__PLUG_jQ = undefined;
      window.__PLUGB = undefined;
    }
  </script>
</mt:If>

Метод не очень красивый, но позволяет быть уверенным что и jQ будет точно загружен, и $/jQuery не попортятся.

Магия тегов


Теперь рассмотрим теги: функции и блоки.

Тег-функция представляет собой некую функцию, которая получает на вход текущий контекст, аргументы, условия и возвращает строку, которая и будет использована. В шаблонах теги ссылаются через <$mt:ИмяТега$>.

Классические вопросы:
  • Как получить свои настройки в теге?
    sub tag_somemytag {
        my ($ctx, $args, $cond) = @_;
        my $blog_id = $ctx->stash('blog_id');
        my $some_param = MT->component('OurPlugin')->get_config_value('some_param', 'blog:' . $blog_id); # конкретный параметр
        my $config = MT->component('OurPlugin')->get_config_hash('blog:' . $blog_id); # Весь хеш сразу
        ....
    } ## end sub tag_somemytag

  • Как создать тег, который использует тругие теги?
    sub tag_somemytag {
        my ($ctx, $args, $cond) = @_;
        my $text = "<$mt:SomeOtherTag$>";
        ....
        my $t = MT::Template->new;
        $t->text($t);
        return $t->build($ctx) or $ctx->error($t->errstr);
    } ## end sub tag_somemytag
  • Как сообщать об ошибках?
    Ну вообще как раз через $ctx->error("error message");, но помимо этого есть и «заточенные» методы ошибок, например $ctx->_no_entry_error();.
    За подробностями — в $mt/lib/MT/Template/Context.pm.


Тег-блок представляет собой либо цикл, либо условие, либо еще что-нибудь, при котором «внутренности» определяются в шаблоне, а вот как они компилируются определяет сам блок. Описываются они в tags/block в config.yaml (см. выше). Классический тег-цикл по какому-либо массиву выглядит так:
sub tag_myloop {
    my($ctx, $args, $cond) = @_;
    my $builder = $ctx->stash('builder'); # берём текущий используемый билдер для этого тега.
                                          # можно создать новый, но тогда получим геморрой а-ля плагины
    my $tokens = $ctx->stash('tokens'); # берём разобранный шаблон, который и надо обработать
    my $res = '';
    my $vars = $ctx->{__stash}{vars} ||= {};
    for my $i (0..$#looparray) {
        my $item = $looparray[$i];
        # ставим какие угодно переменные, какие нам надо чтоб появились для шаблона
        local $vars->{__item__} = $item;
        # ставим стандартные переменные цикла
        local $vars->{__first__} = $i == 0;
        local $vars->{__last__} = ($i==($#looparray-1));
        local $vars->{__odd__} = ($i % 2) == 0;
        local $vars->{__even__} = ($i % 2) == 1;
        local $vars->{__counter__} = $i+1;
        defined(my $out = $builder->build($ctx, $tokens, $cond))
            or return $ctx->error($builder->errstr);
        $res .= $out;
    }
    return $res;
}

Таким образом, тело будет скомпилировано нужное число раз, сконкатенировано и выдано наружу. При желании, разумеется, можно понаворотить другой логики, другое дело, что это уже будет довольнотаки не очевидно, так что лучше придерживаться общего правила: таг-блок обеспечивает доступ до нужных данных, всё остальное если надо в других тагах и в самом шаблоне.

Callbacks: хаки в ядро


В общем-то, MT содержит возможность воткнуть свои обработчики на почти все случаи жизни. Подробнее о том, какие места доступны можно найти в документации, вот только там опять же перечислены не все, а только основные. Если вам понадобилось, например, настроить какой-либо шаблон для какой-либо страницы в админке, то необходимо определить хук на MT::App::CMS::template_param.имяшаблона. А если надо поменять вывод страницы — то используется template_output. Эти хуки крайне полезны, если вы хотите создать собственную страницу в админке (о чем, если будет спрос, в следующем посте). Когда и если вам понадобится найти какие же хуки бывают, смело grep'айте код MT на предмет run_callbacks, и анализируйте POD соответствующего модуля.

Поскольку тему callback'ов разжевывать можно долго на все случаи жизни, я приведу только простейший пример, как, например автоматически развернуть сыслки на твиттер внутри поста.
Определяем в config.yaml
callbacks:
    MT::Entry::pre_save: $OurPlugin::Plugin::cb_entry_pre_save
    #cms_pre_save.entry: $OurPlugin::Plugin::cb_cms_pre_save_entry
    #api_pre_save.entry: $OurPlugin::Plugin::cb_api_pre_save_entry

У нас есть три точки, куда мы можем воткнуться для обработки текста записи перед её сохранением.
  • MT::Entry::pre_save — вызывается из ORM обработчика непосредственно перед сохранением в БД.
  • cms_pre_save.entry — вызывается перед сохранением записи при редактировании через админку
  • api_pre_save.entry — вызывается перед сохранением записи при редактировании через RPC или ATOM


Для нашей задачи прекрасно подходит entry_pre_save, так как нам надо-то просто по регекспу обработать текст. Итак, пишем:
sub cb_entry_pre_save {
    my ($cb, $entry, $original) = @_;

    my $txt = $entry->text; 
    if ($txt ne $original->text) {
        my $newtxt = $txt;
        # regexp from twitter-blackbird-pie plugin
        $newtxt =~ s/([^a-zA-Z0-9_]|^)([@\xef\xbc\xa0]+)([a-zA-Z0-9_]{1,20})(\/[a-zA-Z][a-zA-Z0-9\x80-\xff-]{0,79})?/$1@<a href="http://twitter.com/intent/user?screen_name=$3" class="twitter-action">$3</a>/ug;
        $entry->text($newtxt) if ($newtxt ne $txt);
    }
    return 1;
} ## end sub cb_entry_pre_save


Applications: настройка админки


Сам MT разделён на несколько «раздельных» частей, каждая из которых запускается со своего startpoint. Это:
  • CMS (mt.cgi, админка)
  • Comments (mt-comments.cgi, прием новых комментариев от населения)
  • Wizard (mt-wizard.cgi, установка
  • Upgrader (mt-upgrade.cgi, вызывается при обновлении БД)
  • Search, Search::FreeText (mt-search.cgi, mt-ftsearch.cgi, поиск на публичной части сайта)
  • ActivityFeeds (mt-feed.cgi, генератор RSS/Atom)
  • Trackback, NotifyList


Для каждой части в разделе config.yaml applications можно задать свои параметры и настройки, которые будет расширять/заменять плагин. Весь раздел крайне плохо документирован (только в PODах и смотреть само использование).
Основное использование раздела — для объявления нужных себе ajax вызовов. Например, ajax метод, доступный на публичном сайте:
applitcations:
    comments:
        methods:
            record_some_info: $OurPlugin::Plugin::ajax_record_some_info

И функция к нему в плагине:
sub ajax_record_some_info {
    my ($app) = @_;
    my $blog  = $app->blog;
    my $result = "{'error': 'No information supplied'}";

    my $user_name = $app->param('user_name');
    my $info = $app->param('info');
    if ($user_name && $info) {
        $result = do_store_info($user_name, $info); # неважно что тут делается
    }

    $app->send_http_header("");
    $app->print($result);
    return $app->{no_print_body} = 1;
} ## end sub ajax_record_some_info

В интерфейс добавляем куда-нибудь что-нибудь типа (код должен быть сгенерирован, либо сгенерирован только путь):
var u = mtGetUser();
if(u && !u.is_anonymous) {
  jQuery.post({
    url: '<mt:CGIPath encode_js='1'><mt:CommentScript encode_js='1'>',
    '__mode': 'record_some_info',
    'user_name': u.name,
    'info': 'he he'
  });

Тут важно, что URL для поста должен быть получен из комплекта <mt:CGIPath encode_js='1'><mt:CommentScript encode_js='1'>, так как может отличаться по каждому месту. Классическое решение — сгенерировать .js файл, так делает сам MT, либо вписать в шаблон Header что-нибудь типа
<script>COMMENTS_URL = '<mt:CGIPath encode_js='1'><mt:CommentScript encode_js='1'>';</script>

и пользоваться COMMENTS_URL где нужно.

Пропущенные моменты


Я не описал работу с базой данных, как расширять имеющиеся и как создавать новые объекты, а так же как создавать свои страницы в админке и вопросы i18n. Читайте об этом в следующем выпуске.

Полезные ссылки


  • MovableType — официальный сайт платформы
  • MovableType Developer Center — официальная документация для разработчиков
  • HOWTO on forums.movabletype.org — гугль в помощь для поиска очень полезных вещей на форуме.
  • mt-dev maillist — еще одно рыбное место для ловли полезной информации. Чуть-чуть тухловато.
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+5
Comments25

Articles