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".
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.
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.
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; }
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.
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(); } } ?>