Six Apart Developer Wiki

The Movable Type and Professional Network Wiki has been moved to wiki.movabletype.org.

Recent Changes

AthenaComponentizationGuide

[Login] Change #8 by OpenID Identitydrry at 2007-04-27 14:01:17.

With the release of Athena, Movable Type is transitioning away from the separate Personal/Commercial/Enterprise product classes to a model where there is a core version of MT that defines a platform and additional packs of functionality may be purchased and added to it.

To facilitate this from an engineering standpoint, Movable Type as a product must be defined in a much more dynamic manner. Some elements of MT today are very customizable and extensible (for instance, the custom tag framework through which MT uses to publish templates). But many other parts of Movable Type are not as extensible and in fact are difficult to change without low-level 'hacking' of the internals.

The component architecture specified in this document will allow for every single element of Movable Type to be defined or replaced by a component or plugin.

Specification

Component Structure

Movable Type shall support a new infrastructure for defining components and plugins. The installed components will live underneath a 'components' directory.

   MT_DIR/
       components/
           component1/
           component2/
           component3/

An initial installation of Athena would provide the following components:

  • core
  • blogging
  • pages
  • themes

As with plugins, each component directory may contain a 'lib' subdirectory which will be added to the Perl include path. Components should use a unique namespace for their own packages here, to reduce risk of conflicts with other components or core packages (unless that is intentional).

Post-Athena Component Plan

The eventual plan for MT's component architecture is to make components out of every single discrete element provided by the MT product. The eventual component list would include these parts:

  • activity_feeds
  • assets
  • atom
  • backup
  • blogging
  • categories
  • commenting
  • core
  • enterprise
  • notifications
  • openid
  • pages
  • plugins
  • search
  • tagging
  • themes
  • trackback
  • typekey
  • xmlrpc

config.yaml: Component Configuration

A component will use a config.yaml file to define the parts of the product it provides. This file will be found immediately within the component directory.

   # sample config.yaml
   ---
   name: Sample Component
   class: MT::Compoent::Foo
   extends: core

   applications:
       sample: MT::App::Sample

   object_types:
       foo: MT::SampleFoo

(We will be utilizing YAML::Tiny to read these files. YAML is a preferred format since the syntax is very easy to read and write, and it allows for structured data at the same time: you can define arrays and hashes of data. YAML::Tiny is a pure-perl module, very fast and does not add a lot of overhead to process these files.)

A note regarding the 'extends' key. This config file will in effect define a 'MT::Component::Foo' class, which has a parent of 'MT::Component::Core'. If multiple items are given for the 'extends' key, then they will all be listed as parent classes of the component.

Configuration files are used to define all manner of metadata used by MT. These include, but are not limited to:

  • name: The name of the component.
  • author: Defines the author of the component.
  • extends: A list of other components this component relies on.
  • version: The version # of the component.
  • schema_version: The version # of the schema defined by the model elements of the component.
  • applications: For declaring a package name for an application entry point (ie: 'cms', 'search', 'comments', etc.)
  • object_types: For declaring type and package name for a MT object class.
  • object_drivers: For declaring database drivers.
  • auth_drivers: For declaring authentication schemes.
  • permissions: For defining permissions.
  • config_settings: For declaring mt-config configuration settings.
  • text_filters: For declaring text filters for publishing.
  • log_types: For declaring MT::Log types for custom activity log records.
  • callbacks: For defining callbacks.
  • tasks: For defining system tasks provided by the component.
  • asset_types: For defining custom MT::Asset types.

Note that some of these (like 'asset_types') is only relevant because the 'assets' component does something with it. And only components that extend the 'assets' component should be declaring additional types (although this isn't enforced in any way).

startup.pl

A component (or plugins) can optionally define a startup.pl file that is loaded when MT starts up. This file is not reprocessed with each request (unless running as CGI of course) but may be used to condition the registration of additional elements.

Plugin versus Component

Components and plugins are both methods that can be employed to extend the MT platform. There are important differences between them.

  • Component definitions are loaded by MT very early; even prior to loading any of the MT data model elements. This also means that components are loaded prior to processing the MT configuration file.
  • Components cannot be disabled through the user interface.

Plugins shall be extended to support a config.yaml file as well and may define elements much like components can. In fact, a plugin is just a kind of component, one that has the added ability of being disabled. Plugins are also listed in the UI as such, whereas components are more seamlessly integrated into the app and are not as visible as such.

So in terms of class hierarchy, this is how things look:

   MT::ErrorHandler
          |
    MT::Component
          |      \_
     MT::Plugin    |
          |        MT::Component::Blogging (specific component)
          |
   MT::Plugin::Foo (specific plugin implementation)

I expect plugins to start defining config.yaml files instead of a '.pl' file for most cases. Most '.pl' plugin files today only exist to define resources and put the actual implementation in separate modules that are loaded on demand.

Registry

To make Movable Type as open as possible, we must eliminate the concept of hard-coded lists within the application code. Wherever we require a list of something, we should use the registry to gather it. This registry is accessible to manipulation through components and plugins. For instance:

This is to standardize and replace (or at least deprecate) the multitude of methods that are currently used to register and variety of interfaces for retrieving these lists:

   MT->add_text_filter
   MT->all_text_filters
       mt->registry('text_filters')

   MT->add_plugin
   @MT::Plugins

   MT->add_task
   %MT::TaskMgr::Tasks
       mt->registry('tasks')

   MT->add_plugin_action
       mt->registry('plugin_actions')

   MT->add_itemset_action
       mt->registry('itemset_actions')

   MT->add_log_class
       mt->registry('log_classes')

   MT->add_callback
   @MT::Callbacks
       mt->registry('callbacks')

   MT->register_junk_filter
       mt->registry('junk_filters')

   MT::Template::Context->add_tag
   MT::Template::Context->add_container_tag
   MT::Template::Context->add_conditional_tag

   MT::App::CMS->register_type
   MT::App::CMS->_load_driver_for
       mt->model('entry') | mt->registry('models')

   MT::App::CMS->add_rebuild_option
       mt->registry('rebuild_options')

   MT::App->add_methods

   MT::App->plugin_actions
       $app->registry('plugin_actions')

   MT::Permission->add_permission
       mt->registry('permissions')

Other things that currently are hardcoded need to be extensible and use the registry:

  • archive_types
  • ping_services
  • (more to come...)

Extensible Data Models

For MT's component architecture, it is possible for object types declared in a base component to be extended by another component. The manner for doing this should not rely on inheritance. Because many components may wind up 'contributing' to an object class.

So to define additional properties for a given class that do not exist in the 'base' version of it, a component would define them as extensions (in the config.yaml file for the component):

   # sample 'ratings' component
   name: Ratings Component
   extends: blogging, comments, trackback
   version: 1.0
   schema_version: 1.0

   object_types:
       entry:
           rating: integer indexed
       comment:
           rating: integer indexed
       tbping:
           rating: integer indexed

Note that when a particular object type is defined in terms of columns, it is seen as an extension to an existing object type, not a declaration of a package for the object.

MT will supplement the base class definition with these additional properties. It will do this upon installing the properties into the base class. So this metadata isn't all parsed into place with each request. Only as needed.

Tags

In MT, we have different types of tag handlers:

  • 'singlet' tags, like <$MTBlogName$>
  • container tags, like <MTEntries> .... </MTEntries>
  • conditional tags, like <MTIfNonEmpty> ... </MTIfNonEmpty>

MT provides a large library of tag handlers to publish the various types of data managed by MT. Once we break MT into pieces, especially to the degree of granularity we hope to achieve, it becomes important to define them outside of the monolithic MT::Template::ContextHandlers module and instead within their individual components. So, there would be a set of core tag handlers defined that are not component-specific (<MTIfNonEmpty> for instance), but tags that relate to a component shall be defined within the component (<MTAsset*> in the 'assets' component).

Since MT doesn't have to define tag handlers with each invocation (think CGI here), it isn't necessary for the tag definitions to be loaded with each request. It may be loaded on demand. The tag handlers will be defined using a tags.yaml file. This logic is part of the publishing framework.

   # sample tags.yaml for blogging component
   ---
   handler: MT::Component::Blogging::Tags

   singlets:
       - BlogName
       - BlogID

   containers:
       - Blogs

   conditionals:
       - IfBlogHasEntries

The 'handler' element defines a module that is responsible for the implementation of the tag handlers. Within this module, subroutines will exist for each of the tags defined.

Application Handlers

Similar to tag handlers, there is potentially a lot of metadata that a component may have to define on a per-application basis. Because of this, an application-specific configuration file will be used to define app handlers and what resolves them.

So, if a component defines an app-<app_id>.yaml file, it will be processed. Here is a sample application yaml file:

   # sample app-cms.yaml
   ---
   handler: MT::Component::Foo::CMS

   # format is 'menu_id_key:' -> 'menu_option_key:' -> label/permission/mode
   menus:
       create:
           create_foo:
               label: Foo
               permission: create_foo
               mode: create/foo
       organize:
           organize_foo:
               label: Foos
               permission: edit_foo
               mode: organize/foo
       tools:
           tool_rebuild_foos:
               label: Rebuild Foos
               permission: rebuild
               mode: rebuild/foos

   # additional modes declared for this application space
   modes:
       foo: ~
       fiddle: ~

Flexible application menu extension is crucial for the component packs. We should not be relying on 'transformer' api calls to extend the menus.

Application Templates

A core 'tmpl' directory shall exist for the error.tmpl file, but the main application template directory will reside underneath the component that declares the application. If any components are registered as extending the base component, their path will supercede the base component application path. This will allow for packs to entirely replace base level app templates.

   MT_DIR/
       components/
           blogging/
               tmpl/
                   cms/
                       author_profile.tmpl
           enterprise/  (extends 'blogging')
               tmpl/
                   cms/
                       author_profile.tmpl

In this case, the 'enterprise' author_profile.tmpl template would be used in place of the 'blogging' author_profile.tmpl.

Localization

Localization for components will follow the model used for plugin localization. Each component will have it's own localization tree. Phrases unique to that component will be localized there, not in some massive localization lexicon.

So the component class will have the translation methods to translate a particular phrase. Some phrases will be core of course, but these will be mostly for reporting errors.

(The following hierarchy is desirable and would be also used for plugins. It may not be possible though... I need to do some additional discovery to determine this; if not possible, we will use the conventions used by plugins today. Absent a specific 'l10n_class' defined for the component/plugin, it would use this...)

   MT_DIR/
       components/
           blogging/
               l10n/
                   ja.pm
                   en.pm
                   de.pm

Static Files

TBD: How to handle static files in this new architecture. Ideally, we would use this model:

   MT_DIR/
       mt-static/
           images/
           js/
           css/
       components/
           blogging/
               tmpl/
               mt-static/
                   images/
                   js/
                   css/

But today, we have to put plugin static files under this structure:

   MT_DIR/
       mt-static/
           images/
           js/
           css/
           plugins/
               plugin_name/
                   images/
                   css/
                   js/
       plugins/
           plugin_name/
               non-static-files

One possibility would be to store all components and plugins under the static path. This would prevent the use of standalone plugin CGIs, but with Athena, we are promoting the use of reusing a single CGI script that dispatches requests based on path information following the script. So 'mt.cgi' would service multiple applications, not just the 'cms' app. This makes it less necessary to create and install additional CGI scripts. It will still be possible for someone to separate out a comment or trackback script, if they have to manage access to those applications independently of the administrative apps.

PHP Files

We can't do all of this componentization without affecting PHP. With Athena, MT should identify each installed plugin in the mt_config table with the PluginSwitch setting (And another hash for listing components? Or just traverse the component tree since all components are always enabled.). If a plugin directory is not registered within this hash, it should be added (with it enabled) and re-saved to the database. The mt.php script should then take that hash, select the enabled plugins and for each, examine the directory to determine if a "php" subdirectory is present. If so, it would add that directory to the PHP include path. This will allow plugins and components to include PHP code in their own respective compartments.

Questions

If you have any questions, enter them below.

Feedback

[Login] Change #8 by OpenID Identitydrry at 2007-04-27 14:01:17.
Six Apart
Makers of weblog software and services for individuals, organizations and businesses.