Athena Componentization Guide

From MovableType

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.

Contents

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