Guide to Creating and Modifying Content Modules

Table of Contents

  1. Overview
  2. Getting Started
  3. Implementing Classes
    1. Content Module
    2. Editor
    3. Preview
    4. Translator (Optional)

Overview

A content module is a class that is responsible for managing the content of a version. It provides a standard interface to both the internals of the Snap2 API and outside world by which content can be manipulated without having to know the details of the content or how it is stored. For example, some content may be stored on disk in the form of image files and so forth. The image content module handles this automatically, so image content can be used just like plain HTML content.

Below is a graph showing how the various aspects of the Snap2 system interact with each other and content modules. The arrow roughly means "uses".

inline_dotgraph_1.dot

It is now fairly easy to add new content types to Snap2. You have to create a few simple classes and register your classes in one central location. You may also need to create a translator if generating HTML requires more than just calling getHTML() on the content module.

This guide covers how to create a content module and related classes.

Getting Started

The first step is to create the skeleton classes and register them with Snap2. You will need to create a content module class (which extends from SnapContent), a preview class (which extends from TKSnapPreview), an editor class (which extends from TKSnapEditor) and optionally a PageRender2 translator. That sounds like a lot, but the preview class is usually very small and the editor class is usually straightforward. The only complex piece is the content module, which is what the bulk of this guide will cover. The editor and preview classes are Toolkit components, so you will want to make sure you are familiar with the Toolkit before proceeding to do any serious work on those (documentation at http//intranet.shodor.org/commondocs/tk2/).

You can use the following skeletons for the classes you need to create. The content module class should be named "SnapContentXxx" where Xxx is the name of your content type, camel-cased. The editor class should be named "TKSnap<i>Xxx</i>Editor" and the preview class should be named "TKSnapXxxPreview". These nomenclature conventions are not enforced, but it is nice to have everything named similarly.

Skeleton for content module class (lives in common/snap2/content):

<?php

class SnapContentMyContent extends SnapContent {
    // TODO: uncomment the following line if your content is HTML/XML
    //protected $NEED_TRANSLATION = false;

    protected function validate() {
        // TODO: return boolean indicating whether content module is valid
    }

    protected function doGetHTML($params) {
        // TODO: return HTML suitable for embedding by a PR2 translator
    }
}

?>

Skeleton for editor class (lives in common/snap2/tk2/editors):

<?php

class TKSnapMyContentEditor extends TKSnapEditor {
    protected function showEditor($cm, $failed) {
        // TODO: add toolkit component or HTML to $this; the HTML/component being
        //   the editor; e.g.: use a TKTextArea for an editor box
    }

    protected function processSave($oldCM) {
        // TODO: return new content module representing saved content
    }
}

?>

Skeleton for preview class (lives in common/snap2/tk2/previews):

<?php

class TKSnapMyContentPreview extends TKSnapPreview {
    protected function getTranslators() {
        return array(
            'MyContent' => 'STransMyContent'
            // TODO: if you want to have additional translators show up in the dropdown
            //   list them here (key is descriptive name, value is translator class name)
        );
    }

    protected function getFormattedSource() {
        // TODO: return HTML to go in Formatted Source slot of preview frame; should be a
        //   meaningful display of the content or its metadata
    }
}

?>

Once you've created these, you have to do two more minor things to set up your content type so that Snap2 can use it. The first is to add a new content type constant to SnapResource. Add a line that looks like the following to SnapResource.php5 with the other constants at the top of the class:

const TYPE_MYCONTENT = 42;

The number you pick must not already be used, but it doesn't have to be consecutive after the last used number. It would be nice for that to be the case, to keep things simple. Whatever you do, though, do NOT change the numbers of existing content types. These numbers are used in the database, so if they are changed, the database will not match up with the code, resulting is massive breakage. People will storm into my office and that's never a good thing.

The second thing to do is register your classes in a special file called contentModules.php5 that lives in common/snap2/content. In this file, you will see a list arrays that describe content types. At the bottom, you want to add the following:

SnapContent::$CONTENT_MODULES[SnapResource::TYPE_MYCONTENT] = array(
    'contentModule' => 'SnapContentMyContent',
    'editor'        => 'TKSnapMyContentEditor',
    'preview'       => 'TKSnapMyContentPreview',
    'translator'    => 'STransMyContent',
    'suffix'        => 'txt',
    'name'          => 'My Special Content'
);

The first four items simply give the names of various classes responsible for managing, displaying and editing the content. The last two need some explanation. The 'suffix' parameter gives the file suffix that should be used when the content is downloaded from special links. The 'name' parameter gives a descriptive name for the content type, to be used in the dropdown on the create resource page in the Snap2 Admin Tool, among other places.

Implementing Classes

Content Module

The content module class represents a piece of content. Each instance of a content module should represent one logical "chunk" of content. If you create a new content module, it represents a new "chunk" that is separate and distinct from any other "chunks". At least, that's the ideal. Because it is not possible to align content module instances with "chunks" of content, some concessions had to be made. Firstly, content modules exist in one of two states: read-only or read-write. A read-only content module cannot be modified, but there can be many instances of a read-only content module that refer to the same logical "chunk" of content. A read-write content module, on the other hand, has a one-to-one relationship with a "chunk" of content. Each instances of a read-write content module refers to different content. Each one would refer to its own copies of external files, etc. The read-only content modules are created by a factory method in SnapVersion (specifically, SnapVersion::getContentModule()) and are used for querying and ultimately displaying content. The read-write modules can be created directly and are used for updating or adding new content into Snap2. While the system isn't air-tight, it works well enough to get the job done. Follow the cycles explained below and everything should just work.

One cycle is retrieving existing content. In this case, you call the SnapVersion::getContentModule() method to retrieve a content module object for that version. You can then call query methods on it, to retrieve, e.g., the HTML that would be dumped into a page for that content, or other information, such as image width or height (if the content is image content), etc. PageRender2 uses the query methods of content modules to generate HTML in its translators when Snap2 content is involved.

The other cycle is for updating a version. To prevent failures in the process from resulting in dangling content, or a corrupt database, the decision was made to consider updating a process of creating new content (based on the old) and then deleting the old content after the update succeeded. This way, if the update failed, the old content would still be around. So, when it is time for an update, one must create a new read-write content module, fill it with content, send it to the SnapVersion::update() method, and then the old content module will be deleted automatically. Certain methods that aren't allowed to be used for read-only content modules are available for read-write content modules, and these are used to upload the new content. For HTML/XML content modules, there will usually just be a single setHTML() or setXML() method. For media content modules, there may be multiple methods to input a range of content information. It is up to the content module author to decide what input methods there should be. In general, keep the interface simple and unlikely to result in a broken content module. Also note that it is likely that the new content module will at least be partially filled with information from an old content module, you will want to make that process of copying as easy as possible, usually be providing an parameter to input functions that takes the old content module.

Before we delve into implementing a content module, there are a few other bits of important information. First, the content module base class (SnapContent) tries to do as much of the work for you as possible. One of the key things that it does is keep an array of values that represent your content. You access this array using the SnapContent::get() and SnapContent::set() methods. These methods also enforce the read-only vs. read-write distinction (i.e., you can't call set() on a read-only content module). For HTML/XML content, you will want to use setAll() and getAll() instead to set the content. For complex content types (i.e., anything else), you will want to use get() and set(). The array that these fill up will be serialized for you automatically when its time for the information to go into the database.

Secondly, if the content module you are writing makes use of external files, you will want to use the SnapExternalFile class. This class represents a file or directory in much the same way that a content module represents a chunk of content. It does not have the read-only/read-write distinction as it is meant not to be an API, but a convenience class for dealing with external files. It also makes sure the files are in the right place in the /media directory (where all external Snap2 files must go).

Alright, now it's time to start looking at implementation. I will essentially show how to implement the Interactivate applet content module (and the same for editors and previews). The final and complete source code is obviously already available in common/snap2/content/SnapContentMediaInteractivateGuide.php5. I will only show relevant snippets here.

There are two functions that you have to have in a content module: validate() and doGetHTML(). The former returns a boolean indicating whether the data stored in the content module is correct enough to be serialized and put into the database. The latter returns a string of HTML that would be used by PageRender2 when rendering the version associated with the content module.

If the content module you are making represents content that has external files, then you will want to override doDelete() and doCopy(). These functions are called when the content module is deleted and copied, respectively. What you will want to do in these functions is to delete or make copies of any external files associated with your content. The doDelete() method simply requires that you delete any external files associated with the content module itself. The doCopy() method is different, though. It requires that you return a new content module that is a replica of the current one (but with new unique copies of external files). Below are commented implementations of these two methods, stolen from SnapContentInteractivateApplet:

protected function doDelete() {
    // don't try to delete an invalid content module
    if(!$this->validate())
        return true;

    // create external file object to represent our external file (the jarfile)
    $jar = new SnapExternalFile($this->version, $this->get('jar'));

    // and then tell it to delete the external file
    $jar->delete();

    return true;
}

protected function doCopy() {
    // create empty content module that we will fill in
    $newCM = new SnapContentMediaInteractivateApplet($this->version);

    // create external file object to represent our external file (the jarfile)
    $jar = new SnapExternalFile($this->version, $this->get('jar'));

    // copy the non-external attributes
    $newCM->set('width', $this->get('width'));
    $newCM->set('height', $this->get('height'));
    $newCM->set('appclass', $this->get('appclass'));

    // copy the jarfile
    $jarCopy = $jar->copy();

    // and put the copy in the new content module
    $newCM->set('jar', $jarCopy->getArray());

    return $newCM;
}

Editor

The editor class has a two-fold purpose. It is used to generate the editor form (HTML) and then it processes that form. The base class for editors handles the details of generating the rest of the page and allowing the user to cancel the edit, among other things. The editor must implement two methods: showEditor() and processSave(). Let us take a look at these two methods.

The first method, showEditor(), must return a string of HTML that will be embedded in the Snap2 editor page. It should be a form that allows the user to input any relevant values for the content, or upload files. The method takes two arguments, $cm, a content module, and $failed, a boolean. $cm is the current content module for the version and should be used to fill in values in the form. $failed indicates whether the form is being displayed because there was an error on the last save (e.g., invalid input). The $failed parameter allows you to choose how to rebuild the form. If the update failed, then you probably want to fill in the editor with what the user just entered (and not what was already in the database). Otherwise, you can just fill it in with what's in the database (via the content module). Take a look at TKSnapHtmlEditor for an example of a simple case of using this methodology.

The second method, processSave(), is called when the user has submitted the editor form. It must validate the data and, if it is valid, create a new content module and return it. If the data is invalid, then it needs to generate proper error messages, display them on the editor object (e.g., by using $this->addText('Error: blah blah blah') -- see TKSnapXmlEditor for a simple example of this) and then return false, so that the controller knows the redisplay the editor. If the user selects Exit without Saving, then this method will obviously not be called.

Preview

The preview class is considerably simpler than the the editor and content module classes. It needs to implement only two functions: getTranslators() and getFormattedSource(). The first method must return an associative array that lists the various translators that should be visible in the drop down in the content preview panel. The keys to this array are the nice names that are displayed to the user in the drop down, and the values are the names of the translator classes. These translators must be available in either common or in the Snap2 admin tool. You cannot use project specific translators.

The second method is getFormattedSource() and all it must do is return something that is suitable for displaying in the formatted source panel in the preview pane. For XML/HTML and content types based on that, it should display the source using, e.g., SXMLHelper::formatXMLSource(). Other types should display something nicer that describes the content. The MEDIA IMAGE type, for example, displays a table of information about the image.

Here is a commented example of a preview class:

<?php

class TKSnapXmlPreview extends TKSnapPreview {
    protected function getTranslators() {
        // three valid translators for this content type, names are the keys
        //   and the translator classes are the values
        return array(
            'Standard XML 3' => 'STransStdXML3',
            'HTML'           => 'STransHTML',
            'HTML (Media)'   => 'STransHTMLMedia'
        );
    }

    protected function getFormattedSource() {
        // grab the XML from the content module
        //   and format it using SXMLHelper::formatXMLSource
        // nothing complicated
        return SXMLHelper::formatXMLSource($this->version->getContentModule()->getXML());
    }
}

?>

The TKSnapMediaImagePreview class has a different look. For one, it offers a set of translators that have the same class, but different names. It also presents a more interesting formatted source display. But more importantly, it has two extra functions. One of these is getParameters(). This is called, if available, to provide additional parameters to translators. It is given the name of the translator and it returns an array with parameters that will be passed to $prm->load(). The other method is showAdditionalInfo(). This method can be used to display extra information about the resource above the content preview pane.

<?php

class TKSnapMediaImagePreview extends TKSnapPreview {
    protected function getTranslators() {
        // note that we have multiple options, but they all use the same translator
        return array(
            'Media Image (Thumb)'    => 'STransMediaImage',
            'Media Image (Medium)'   => 'STransMediaImage',
            'Media Image (Display)'  => 'STransMediaImage',
            'Media Image (Original)' => 'STransMediaImage',
        );
    }

    protected function getParameters($name) {
        // the $name field tells us which option was selected by the user
        switch($name) {
            case 'Media Image (Thumb)':
                return array('which' => 'thumb');
            case 'Media Image (Medium)':
                return array('which' => 'medium');
            case 'Media Image (Display)':
                return array('which' => 'display');
            case 'Media Image (Original)':
                return array('which' => 'original');
            default:
                return array();
        }
    }

    // this will display information about the image and will show up above
    //   the content display pane
    protected function showAdditionalInfo() {
        $cm = $this->version->getContentModule();
        if(!$cm->isValid())
            return null;
        $res = $this->version->getResource();

        $table = new TKTable();

        $table->addTextTo('header', "Size");
        $table->addTextTo('header', "Width");

        // ... snip ...

        return $table;
    }

    // this displays similar informatin to showAdditionalInfo(), but it appears in
    //   the Formatted Source panel
    protected function getFormattedSource() {
        $table = new TKTable();
        $table->addTextTo('header', 'Size');
        $table->addTextTo('header', 'File Path');
        $table->addTextTo('header', 'URL');
        $table->addTextTo('header', 'Width');
        $table->addTextTo('header', 'Height');

        $cm = $this->version->getContentModule();

        $thumbURL = $cm->getURL('thumb');

        // ... snip ...

        return $table->render();
    }
}

?>

Translator (Optional)

If you choose to make custom translators for your content, you will need to see the PR2 documentation on creating translators (forthcoming). To have your translator as the default for the content type, you will change the 'translator' field in common/snap2/content/contentModules.php5 in common.

Generated on Wed Nov 24 02:01:29 2010 for Common by  doxygen 1.5.6