The Movable Type and Professional Network Wiki has been moved to wiki.movabletype.org.
PluginDeveloperGuide
Movable Type Plugin Development Guide
The following document was originally authored by BradChoate to help Movable Type developers get started in developing their own Movable Type plugin. This document not only serves as a manual, but also as a guide to establishing PluginBestPractices. Developers that adhere to these recommended practices ensure:
- the highest degree of interoperability with Movable Type
- the highest possible performance of your plugin
- the highest degree of supportability and maintainability of your code base
Topics Covered Below
- Directory Layout
- Plugin Script Best Practices
- Lazy Loading of Resources
- Persistent Environments
- Localization
- Creating CGI Scripts
- Avoid Creating CGIs
- Extending Existing Application When Possible
- Supporting both Static and Dynamic Publishing Models
Directory Layout
To get started, let's take a look at the basic file structure of a Movable Type plugin. We recommend the following this directory layout when developing and distributing your plugins ("MyPlugin" is the name of the plugin folder in this case):
MT_DIR/
|__ (plugins)/
| \__ MyPlugin/
| |__ MyPlugin.pl
| |__ my_plugin.cgi
| |__ lib/
| | \__ MyPlugin.pm
| \__ tmpl/
| \__ config.tmpl
|
|__ mt-static/
| \__ (plugins)/
| \__ MyPlugin/
| |__ styles.css
| |__ images/
| | \__ logo.jpg
| \__ js/
| \__ myplugin.js
|
\__ php/
\__ plugins/
\__ init.my_plugin.php
This is an extreme example-- not every plugin will have all these kinds of files, but this serves to illustrate where these files should belong.
Why break up the files into three locations? Well, due to common CGI folder restrictions, the "mt-static" folder exists to allow users to place non-CGI files that are served by the web server outside of their "cgi-bin" path.
Another important thing to note is that you should never assume that the end user has a directory named "plugins" for their plugin files. It's possible for that directory to be renamed. It is also possible that the user has multiple plugin directories, something new with Movable Type 3.2.
Unlike previous versions, MT 3.2 makes it easier for MT plugins to find their resources. MT will automatically include your plugin's "lib" folder if it follows the convention above. Similarly, it will utilize a "tmpl" directory if one exists under the plugin directory.
If your configuration files need to refer to these locations and files, use the following HTML::Template tags:
<TMPL_VAR NAME=SCRIPT_URL>
Refers to MT_DIR/(plugins)/MyPlugin/my_plugin.cgi.
<TMPL_VAR NAME=MT_URL>
Always refers to the MT AdminScript address.
Unfortunately, there is currently no way (as of MT 3.2) to refer to the static folder path very well. So you have to specify a "plugins" in that path. For example:
<TMPL_VAR NAME=STATIC_URI>plugins/MyPlugin/logo.jpg
Or, another approach would be to override the build_page routine of your application so that it does supply the static path for images and other static files:
sub build_page {
my $app = shift;
my ($file, $param) = @_;
$param->app_static_path = $app->static_path . $app->envelope;
$app->SUPER::build_page(@_);
}
Allowing you to use this in your templates:
<TMPL_VAR NAME=APP_STATIC_PATH>
Which would yield something like this:
http://example.com/mt/mt-static/plugins/MyPlugin/
Note: When registering a 'config_link' for your plugin or a CGI script for a MT->add_plugin_action call, simply supply the name of the CGI file without any path information at all, and that will automatically prefix it with the full web address for the CGI.
Plugin Script Best Practices
Your main plugin script (ie, "MyPlugin.pl") is what MT uses to load the plugin. The filename can be anything you like, but it must have a ".pl" extension to be recognized. Note that the script is not executed in a traditional sense, so the permissions for this file should simply be readable. Nor does it require a "shebang" line (the "#!..." line commonly found in Perl scripts).
This .pl script (herein referred to as your plugin script) should be as short as possible and should not do any lengthy or resource expensive operations to keep load time at a minimum. Remember, when running MT as a CGI, each of these plugin scripts are processed as MT starts up to serve each request.
Your plugin script should create a "MT::Plugin" instance (or should declare a subclass of MT::Plugin) and register it with Movable Type. This isn't a mandatory component, but is a best practice. Registering your plugin displays helpful metadata about the plugin on the plugin listing screen. The common syntax for this is:
use MT;
use MT::Plugin;
my $plugin = new MT::Plugin({
name => 'My Plugin',
version => 1.0,
description => 'Something about this plugin.',
author_name => 'John Doe',
author_link => 'http://john.thedoes.com/',
plugin_link => 'http://john.thedoes.com/plugins/MyPlugin/',
doc_link => 'http://john.thedoes.com/plugins/MyPlugin/help/',
});
MT->add_plugin($plugin);
# register other resources here
1;
A first-class MT plugin would always declare it's own subclass and package. Like this:
package MT::Plugin::MyPlugin;
use MT;
use MT::Plugin;
use vars qw($VERSION @ISA);
@ISA = qw(MT::Plugin);
$VERSION = 1.0;
my $plugin = new MT::Plugin::MyPlugin({
name => 'My Plugin',
version => $VERSION,
description => 'Something about this plugin.',
author_name => 'John Doe',
author_link => 'http://john.thedoes.com/',
plugin_link => 'http://john.thedoes.com/plugins/MyPlugin/',
doc_link => 'http://john.thedoes.com/plugins/MyPlugin/help/',
});
MT->add_plugin($plugin);
sub instance {
$plugin
}
# register other resources here
1;
The "instance" method declared above gives you a way to globally refer to the plugin object.
$my_plugin = MT::Plugin::MyPlugin->instance;
You may use this technique to access your plugin from your plugin applications. Handy especially if you need to refer to the plugin's configuration settings.
my $plugin_config = MT::Plugin::MyPlugin->instance->get_config_hash;
Note that the version of the plugin is declared separately from the plugin name. Remember, the plugin name is used to uniquely identify the plugin when saving and retrieving your plugin's configuration data, so try to keep that identifier the same between releases.
Lazy Loading Resources
Again, the CGI-based installation of Movable Type is by far the most common way to run it among MT users, so you have to be mindful of that when you develop your plugins. One way to keep your plugin scripts lean is to move the bulk of their code into a separate module. Refer to the directory layout diagram where "MyPlugin.pm" is installed under the plugin folder's "lib" directory. MT automatically adds that "lib" directory to the Perl search path, so all you have to say is:
require MyPlugin;
and Perl will find it. There's no further need to place these secondary modules underneath the MT "extlib" directory. In fact, it's discouraged, since your plugin files should be grouped in places where plugin files should go, and the "extlib" directory isn't one of those places. The only case for installing modules under "extlib" is where your plugin requires a 3rd party module (perhaps from CPAN) that might be usable to other plugins as well. In that case, the files should be installed under "extlib".
So, when registering handlers for tags, or text formatting filters, or callback routines, or a junk filter routine, you should follow this convention (all of these registrations would take place in your plugin script, so the assumption is that a $plugin variable is available which refers to the plugin object):
# Adding a MT tag
MT::Template::Context->add_tag('MyPluginTag',
sub {
require MyPlugin;
&MyPlugin::MyPluginTag;
}
);
# Adding a Text Formatting filter
MT->add_text_filter('my_plugin' => {
label => 'My Plugin Formatter',
on_format => sub {
require MyPlugin;
&MyPlugin::MyPluginFormatter;
}
});
# Adding a callback
MT->add_callback('CallbackName', 1, $plugin, sub {
require MyPlugin;
&MyPlugin::MyPluginCallback;
})
# Adding a junk filter
MT->register_junk_filter({
name => 'My Plugin Junk Filter',
plugin => $plugin,
code => sub {
require MyPlugin;
&MyPlugin::MyPluginJunkFilter;
}
});
By delaying the load of MyPlugin.pm, you save that much additional overhead MT has to incur for each request of the application. This savings is especially realized when all plugins employ this best practice.
Persistent Environments
Movable Type supports two different persistent (where the application stays in memory between requests) environments: mod_perl 1.x and FastCGI. The former has been supported for some time, the latter is new as of MT 3.2. Plugins should also support for these environments. There are just a few issues to be aware of when running in a persistent environment:
Take care when caching data used by your plugin. For example:
$my_plugin->{'__something'} ||= LengthyOperation();Instead, you should store as much as you can within the
MT::Requestobject:my $req = MT::Request->instance; my $something = $req->stash('my_plugin_something'); if (!$something) { $something = LengthyOperation(); $req->stash('my_plugin_something', $something); }Don't assume that your plugin is initialized with each request by your plugin script. The "mainline" code of your plugin script will initialize your plugin upon the first request in a persistent environment, but that is only executed upon starting up Movable Type. MT registrations need only happen once, but if your plugin expects other actions to take place with each request, you should create an "init_request" method for your plugin.
To do this, you will have to create a
MT::Pluginsubclass as already shown. Then, you would declare the routine like this:sub init_request { my $plugin = shift; my ($app) = @_; # do request initialization here. }And of course, since this routine is called for each request, take care that you limit your work as much as possible.
This hook provides for some interesting possibilities, since it lets your plugin intercept the request and do something with it if it wants to.
Localization
Movable Type 3.3 has much better support for plugin localization. If you would like to localize your plugin, you should declare a 'l10n_class' element when registering your plugin:
my $plugin = new MT::Plugin({
name => 'My Plugin',
version => 1.0,
description => 'Something about this plugin.',
author_name => 'John Doe',
author_link => 'http://john.thedoes.com/',
plugin_link => 'http://john.thedoes.com/plugins/MyPlugin/',
doc_link => 'http://john.thedoes.com/plugins/MyPlugin/help/',
l10n_class => 'MyPlugin::L10N',
})
MT->add_plugin($plugin);
When doing this, your plugin directory should have some additional files:
MT_DIR/
\__ (plugins)/
\__ MyPlugin/
|__ MyPlugin.pl
|__ my_plugin.cgi
\__ lib/
|__ MyPlugin.pm
\__ MyPlugin/
|__ L10N.pm
\__ L10N/
|__ en_us.pm
|__ fr.pm
\__ es.pm
MT 3.3 ships with Widget Manager, Style Catcher and SpamLookup, all of which support the plugin localization framework. They're good examples to learn from.
The L10N.pm package is pretty basic:
package MyPlugin::L10N;
use strict;
use base 'MT::Plugin::L10N';
1;
Then, assuming your plugin is written with English as it's default language, your en_us.pm file would look like this:
package MyPlugin::L10N::en_us;
use strict;
use base 'MyPlugin::L10N';
our %Lexicon;
1;
The localization strings for another language would redeclare all the English phrases in that language. So for French, you might have:
package MyPlugin::L10N::fr;
use strict;
use base 'MyPlugin::L10N::en_us';
our %Lexicon = (
'Yes' => 'Oui',
);
1;
Note that the parent class for the French package is en_us.pm package, which allows any phrases that are untranslated to at least be shown in English. Furthermore, the plugin localizations are extending MT's own localization modules, so, if you use the phrase "Weblog" and don't translate it within your own plugin's localization modules, it will use MT's instead.
So, wherever you have a phrase, you would do this:
$plugin->translate("English Phrase")
Or in your plugin's templates:
<MT_TRANS phrase="English Phrase">
Creating CGI Scripts
One of the annoyances of pre-3.2 releases was the difficulty of creating a CGI script that located the MT directory properly. With 3.2 we sought to simplify this task and have developed a new recommendation for plugin CGIs to use:
#!/usr/bin/perl -w
use strict;
use lib "lib", ($ENV{'MT_HOME'} ? "$ENV{'MT_HOME'}/lib" : "../../lib");
use MT::Bootstrap App => 'MyPluginApp';
With MT 3.2, we've made the assumption that one of the following is true:
- The "working directory" when the CGI runs is the parent directory of the CGI itself.
- The "working directory" when the CGI runs is the Movable Type directory.
- The "working directory" when the CGI runs is totally foreign, but the "MT_HOME" environment variable points to the MT directory.
The CGI script above handles each of these scenarios. First and foremost, the "use lib" statement is trying to include MT's lib directory in the Perl search path. If it can do that, MT::Bootstrap will load properly and take over from there.
These are the environments that are most common for CGIs:
- Apache: The current directory is the parent of the CGI script, so the
use lib "lib";adds the plugin's "lib" directory to the search path. Then, the reference to"../../lib";statement should add MT's "lib" directory to the search path. Finally, if the MT_HOME environment variable is set, that will be used to construct the search path. - Windows/IIS 6. The current directory here is the MT directory (this actually requires MT to be accessed via a "virtual directory", otherwise the current directory is set to something else altogether). In this setting, the first "lib" relative directory adds the MT lib directory to the search path. MT::Bootstrap then determines the actual path to the requested CGI and appends the "lib" directory underneath that path to the Perl search path.
- cgiwrap. With cgiwrap, the current directory is the parent directory for the cgiwrap wrapper. This is neither the MT or plugin directory, so either the
MT_HOMEenvironment variable should be set for that virtual host or the CGI scripts themselves should be modified to add an explicit reference to the MT "lib" directory. Another choice is to preset thePERLLIB/PERL5LIBenvironment variable for this virtual host to include the MT lib directory.
Avoid Creating CGIs
We've taken some extra steps with MT 3.2 to make plugin development easier and one of those is the new plugin configuration API. It's now possible to register configuration panels that are visible on a system and blog-level basis. A very simple example of using the new APIs can be found in the Nofollow plugin which comes with MT 3.2.
The Nofollow plugin provides all of it's functionality in a single script. It's roughly 150 lines in length and in addition to implementing the nofollow functionality, registers a configuration template that lets the user configure the plugin on both a system and blog-level (if no blog-level configuration is available, it will use the system-wide settings).
Extend the Existing Application when Possible
Another way to avoid creating CGIs is to consider whether your plugin is really an application of it's own or merely an extension to an existing one, like the 'CMS'. If it's really just an extension, then perhaps you don't need to declare a MT::App descendant to develop it. It's possible with MT 3.2 to attach new 'modes' add to an existing application's method list. This lets you extend or alter the application in many ways.
Here's an example of this (the following are methods of your MT::Plugin
subclass):
sub init_app {
my $plugin = shift;
my ($app) = @_;
if ($app->isa('MT::App::CMS')) {
$app->add_methods(
myplugin_custom_mode => sub {
$plugin->custom_mode(@_)
}
);
}
}
sub custom_mode {
my $plugin = shift;
my ($app) = @_;
# do something interesting!
return "Hello, world!";
}
The init_app routine is called by each MT::App application when it starts up. In a persistent environment like mod_perl, this happens once per hosted application. So it's possible under mod_perl or FastCGI for this routine to be called multiple times, but with different application classes.
With the new MT::Plugin->init_request method, it's possible to "redirect" from one application mode to another:
sub init_request {
my $plugin = shift;
my ($app) = @_;
if ($app->isa('MT::App::CMS')) {
if ($app->mode eq 'view') {
if ($app->param('_type') eq 'entry') {
# conditionally redirect the application
# mode:
my $blog = $app->blog;
if ($blog && $blog->id == 3) {
$app->mode('myplugin_custom_mode');
}
}
}
}
}
Support Both Static and Dynamic Publishing
PHP dynamic publishing is becoming increasingly popular and if your plugins provide custom tags, you should offer PHP versions of those tags whenever you can do so. If your plugin does not support both environments be sure to make that clear in your documentation.

