Template Engine

Overview

The dmBridge templating engine attempts to strike a balance between ease of use, advanced customization potential, and upgrade-safety (meaning that underlying implementation changes in either dmBridge or CONTENTdm® won't break your page templates). The macro-scale HTML layout is left entirely up to the designer, while some smaller bits - rendered by the Draw methods - return HTML in a fixed structure that can be styled, but not customized. This is the price to pay for the ease of being able to use "one-liners" to call up sophisticated functionality. The default functionality of any of the Draw methods can be overridden by extensions, but this is generally a job for a programmer.

In practice, even without custom extensions, the template development process is a whole lot easier and more flexible than that for the CONTENTdm® templates. (This was actually the #1 design priority for dmBridge.) However, it is also totally different, so there is a necessary-but-modest learning curve.

Template-to-collection mapping

A single set of dmBridge templates can be used with a single collection, multiple collections, or all collections. Conventionally, a "single collection" set of templates would be customized for that collection, and an "all-collections" set of templates would be styled to harmonize with the digital collections department or organization. That's just an idea, and you are free to do whatever you want. It is easy to reassign which collections appear in which set of templates at any time via the Control Panel.

Coexistence with the CONTENTdm® default templates

dmBridge templates can coexist peacefully with CONTENTdm® default templates. If you are just starting out with dmBridge, it wouldn't be a bad idea to dip your toes in the water with just one template set at first. You should never delete your default templates, as they are required by the CONTENTdm® administration module.

Deployment

The dmBridge template engine is separate from the core system and can reside on any web server, as long as it can communicate with your CONTENTdm® server. No extra steps are necessary to run it on another server; just move the dm/objects folder as well as any template folders you may have to another server, and they should just work.

Template customization

The template set included with the dmBridge distribution is deliberately spartan. It is intended to be a starting point for your own creations, not a finished set of templates itself.

The dmBridge templates can be designed like any other HTML page using the text or WYSIWYG editor of your choice. The only extra consideration is styling the dynamically-generated content produced by the Draw methods. You need to see the HTML code they produce in order to style it. One way to do this is by viewing the source of the generated page in your web browser. A better way is using a DOM inspection tool:

  • The Firebug extension for Firefox
  • The Developer Tools built into IE
  • The Web Inspector built into Safari
  • All of these will enable you to zoom in on a specific element in the page in order to find CSS classes/IDs that your CSS rules can "grab onto."

    The following is a walkthrough of the template customization process.

    1. In dm/objects/templates, make a copy of the basic folder. Give it a descriptive alphanumeric name in all-lowercase (you can use underscores for spaces). We'll call it newtpls.
    2. Register the new template set in the Control Panel, and assign one or more collections to it.
    3. Navigate to your dm/objects folder in your web browser, appending the collection alias you wish to view to the URL like so:

      http://my_server/dm/objects/?r=myalias

      Your collection should come up, displaying results view. Click on a result and object view should load, and so on.

    4. Enter the newtpls/templates folder. Notice subfolders like object, error, favorite, etc. Take a look inside any one of them and notice one or more files with the .html.php extension. These are dmBridge template files. Open one up and notice that it's an HTML file, with some <?php ?> tags in it. Each one of these files corresponds to one of the main views in the dmBridge templates: object view, results view, favorites view, etc. It should be fairly obvious just by looking at them which one is which; check the <title> tag if you're not sure.

    At this point, you know what you need to do, which is to go nuts editing these page templates. If you have inspected any of the template files, you may be wondering about the code within the <?php ?> tags. These are calls to methods in the PHP API, which will be addressed in the coming sections.

URI model

The Template Engine supports cool URIs, which are stable and based on a thoughtful schema. By default, they look like this:

With "clean URLs" enabled, they look like this:

Finally, to make them fully semantic, we could move the objects folder one level up:

To enable clean URLs, the template engine uses URL rewriting, which it supports using Apache mod_rewrite (Apache) and ISAPI Rewrite (IIS). This eliminates the "?r=" from the URI. Both the template engine and the core (HTTP API) support URL rewriting. See Clean URLs for more information about configuring this.

Views

The template engine is based on the concept of views. In practice, every digital collection uses several common views which, in dmBridge, each have their own page template. The following sections discuss the views in more detail.

Favorites view

Favorites view is a subset of results view that displays a user's favorites. Because it inherits from results view, it has the same display options (e.g. list, grid, tile). It adds checkboxes and a "Remove From Favorites" button.

The dmBridge favorites are stored in the same cookie as the default CONTENTdmĀ® favorites, so changes made to the favorites in dmBridge will show up in the default CONTENTdmĀ® templates, and vice versa.

Favorites view only displays those objects to which the user has access from the current template set. If two template sets (A and B) exist, and neither has access to any of the collections accessible by the other, neither will render any favorites that the other will. But the favorites are still present in the user's cookie at all times.

Object view

Object view is used in single-object context. Typically, it is accessed by clicking on a link from an object in results view. Object view supports both compound and non-compound objects. When using the HTML templates in this view, keep in mind that either view_simple.html.php or view_compound.html.php may be loaded depending on whether or not the object is compound.

Object viewers

Defining viewer associations

Every object viewer in dmBridge is its own class, in its own file on disk. A number of object viewers are provided (in the dm/objects/helpers/viewers folder) that are capable of handling some common file types. Which viewer is used for which file type depends on the associations defined in the Object View section of the Control Panel.

Due to the way the CONTENTdm® PHP API returns information about files, object viewers in dmBridge are associated with filename extensions. In the Control Panel, you can associate a particular object viewer class with a particular filename extension on a global, per-template-set, or per-collection basis. Be aware that some file formats may have more than one extension (e.g. .tif vs. .tiff), so you'll need to define the association once for each.

Writing custom viewer modules

Every object viewer class must implement the idmViewerDelegate interface. This requires writing just one method - getHTMLTag() - which should return an HTML tag. Take a look at dm/objects/helpers/viewers/dmGenericImageViewer.class.php for an example.

Adding your own custom object viewer is as simple as writing your own class in the pattern of the existing ones and defining the associations in the Control Panel.

You have two choices as to where to save your custom viewer:

  1. If you want it to be available to more than one template set, you can save it in the dm/objects/extensions/viewers folder (not the dmclient/helpers/viewers folder).
  2. If it is template-set-specific, you can also save it in the template set's extensions folder.

Object-results view

Object-results view is used to display compound object search results. Results are displayed in a two-column HTML table, one object per row, with the page title in the left column and the full text of the page in the right column. Only pages that match the search query are displayed; matching terms are enclosed in an HTML <span> element with class dmHighlightedTerm. This view is only available for searchable compound objects.

Results view

Results view is used in contexts where a number of objects are displayed alongside each other. It can be populated by any group of objects, such as search results, browse results, or a user's favorites. (Favorites are handled by Favorites view, a subset of results view.)

There are two results view templates inside any template set folder: object/results.html.php and object/results_faceted.html.php. When dmBridge has facets to display, it will load the latter.

To change the number of results per page, supply the rpp parameter in the URI query string; for example, ?rpp=10." This works in all output formats. The default is currently hard-coded at 20 and cannot be changed.

Results view provides several possible views within itself, each of which are just different ways of displaying objects. An easy way of rendering links to each of these views is using the ResultsDraw::viewLinks() method. The user can always get any view by supplying the view parameter in the URI query string, e.g. ?view=tile. Of course, they first have to know that they are able to do that, and there is no reason you need to tell them; but it's not currently possible to disable those views entirely.

Grid view

Grid view displays results in an HTML table, with one result per row. Columns are customizable and configurable in the Control Panel on a per-collection (not per-template-set) basis. Grid view is the default view within results view.

List view

List view is like an inverted grid view, with metadata fields displayed vertically per-object instead of horizontally. List view can be accessed within results view by supplying the ?view=list parameter in the URI.

Tile view

Tile view, like grid view, displays results in an HTML table. However, in tile view, each object has its own table cell. The number of columns is configurable in the Control Panel on a per-collection (not per-template set) basis. Tile view can be accessed within results view by supplying the ?view=tile parameter in the URI.

Search view

Search view is used to display the advanced search form(s) (not search results, which are handled by Results view). This view is not strictly necessary for all digital collections - you could write your own search interface, or use Custom Queries and Results (CQR) - but is provided anyway due to the complexity of the forms.

As you may already know, CONTENTdm® provides four different search forms on its Advanced Search page, allowing the user to search "across all fields;" "selected fields;" "by proximity;" or "by date." dmBridge merges the first two of these forms together, offering a total of three different search forms, which can be added to the advanced search template (search/index.html.php) using the fieldSearch(), proximitySearch(), and dateSearch() methods of the SearchDraw class.

PHP API

The dmBridge PHP API is a high-level, object-oriented API for CONTENTdm® that goes far beyond the CONTENTdm® PHP API. It expedites development by making CONTENTdm® objects easy to work with:

<?php
$col
= new dmCollection('/uw');
$obj = new dmObject($col, 5);

echo
$obj->getMetadata('title');
echo
$obj->getThumbnailURL();

if (
$obj->isCompound()) {
   echo
"Page 1 is titled: " . $obj->getChild(0)->getTitle();
   echo
"It's in this collection: "
     
. $obj->getChild(0)->getCollection()->getName();
}
?>

The API is fully documented and quite stable. The documentation is not hosted here; it is included with the templating engine module in your dm/objects/doc folder.

The PHP API is provided by the template engine and is available automatically within the page templates. (The Draw class and its subclasses are actually part of it, although they only work inside the templates.) It can also be included by other PHP scripts on the same server with code like the following:

<?php
include_once('/path/to/dm/objects/api.php');
?>

If you have been poking around, you may have noticed that similar classes exist in the dmapi and dmctrl folders. These are not part of the PHP API and should not be used.

How to read the API documentation

In the left frame, expand the "Data Structures" menu. Click on dmCollection. This is one of the many classes that the dmBridge API provides. Near the top, there are two sections: "public member functions" and "static public member functions." (A deep explanation of what these mean will not be provided here; please flip to the "object orientation" chapter of your favorite PHP book.)

dmBridge is all about giving objects intelligent behavior. If you want to know a collection's name, you ask it, by calling its getName() method:

<?php
// Create a dmCollection object
$col = dmCollection::instantiate('/uw');

// Get its name
echo $col->getName();
?>

What if you want a list of all collections to which you currently have access? You wouldn't ask a single collection, because it doesn't know; it only knows about itself. But the dmCollection class itself knows:

<?php
print_r
(dmCollection::getAuthorized());
?>

It returns the list as an array of dmCollection objects. You can loop through that array, ask each collection its name, ask it how many objects it contains, ask it what metadata fields it uses, ask each of those fields whether they are full-text-enabled, and so on.

In terms of trying to accomplish your goals with the dmBridge PHP API, think in terms of what CONTENTdm® entities are involved in the task. Objects? Collections, Metadata fields? Comments? Each one of these has its own class in the API, with lots of methods already written for interfacing with them. And like any other class, they can be extended by your own classes.

The API documentation provides a list of all methods that are publicly available in all classes. Most you will never have any reason to work with. But classes like dmObject,
dmCollection, dmComment, etc. may offer some interesting functionality if you need it.

The class you will probably work with the most in the templates is Draw and its subclasses. For a list of methods available in these classes, consult the API documentation. Also, check out the sample templates for working examples of these methods in action.

Using the PHP API in the templates

Digital collections websites perform a lot of common tasks: they display objects, render a paginated results list, etc. The Draw class exists to make a lot of this common functionality as close to automatic as possible, to spare you from having to get involved with the inner workings of dmBridge yourself. For example, by leveraging ResultsDraw::pageLinks(), you can do what would ordinarily take 100 lines of code with just one line. And if we ever update the way pageLinks() works, all of your templates that call it will use the upgraded functionality automatically. (We will try, of course, not to make major changes to the output HTML structure that might break your CSS rules.)

As the class diagram in the PHP API documentation depicts, Draw has several subclasses, each one of which is meant to correspond to a particular view. The child classes are essentially all supersets of Draw; they share all of its functionality, and add more of their own.

All superclass methods can be accessed via any subclass; so, for example, Draw::loginLink() and ResultsDraw::loginLink() are both valid because ResultsDraw is a superset of Draw (where loginLink() lives). But Draw::results() is invalid because Draw knows nothing about ResultsDraw, where results() lives. The idea is that superclasses should contain any functionality that would be used by more than one subclass. So, outside of the templates, where there is no conceptual "view" per se, Draw would be used exclusively, whereas object view could utilize both Draw and ObjectDraw, results view could utilize both Draw and ResultsDraw, etc.

Each view is associated with certain entities. For example, object view is handled by ObjectController::view(). When that method runs, it stores some important information (like the object being viewed) in a temporary location accessible by the templates: in this case, the dmObject class' getCurrent() method. To retrieve the current object from within object view, you would do:

$obj = dmObject::getCurrent();

The naming of these methods should be fairly straightforward. getCurrent() gets the current object in object view; getAllCurrent() returns all current objects in results view; etc. There is no centralized list of these methods, but most of them have the word "current" in them and are associated with the class representing what they are - for example, objects by dmObject, favorites by dmFavorite, queries by dmQuery, etc. Consult the API documentation for a full list of classes and their available methods.

Content representations

The dmBridge Template Engine supports several content representations (output representations) in addition to HTML. This list is different from that supported by the Core (HTTP API).

Name URI extension MIME type Object view Results view Object-results view Favorites view Search view
Atom 1.0 .atom application/atom+xml Yes Yes
HTML text/html Yes Yes Yes Yes Yes Yes
RDF/XML .rdf application/rdf+xml Yes
RSS 2.0 .rss application/rss+xml Yes Yes

HTML is the default representation. To specify an alternate representation, append the URI extension to the end of the URI. Feed metadata can be changed in the Feeds section of the Control Panel.

Mobile template sets

This section has not been written yet.

Record caching

Upon each load of a page template, the template engine initiates a number of requests against the dmBridge HTTP API - sometimes 40 or more, depending on the template. Every time the API receives one of these requests, the PHP engine has to compile and run the API application. Generally, this takes less than a tenth of a second per request; but even then, all 40 of the requests could still take four seconds or more, during which the web server is bogged down and the patron is waiting impatiently.

Under heavy traffic, these effects are multiplied. If two patrons were to hit a template at the same time, for example, they would each have to wait 8 seconds. This is obviously not acceptable.

To work around this problem, the template engine employs an API record cache. When it is enabled, the template engine saves most of the records it has received to disk. Upon each subsequent patron request, it checks to see if it has already downloaded the record, and if so, loads it from disk instead of requesting it from the API. Server load as well as response times are dramatically reduced.

The main drawback of any sort of cache is that changes to data that has been cached are not immediately reflected in the cached version of the data. This means that if a metadata record gets updated in CONTENTdm®, the changes will not appear in the dmBridge templates until the cache has been refreshed. Certain records, such as comment and tag lists that need to stay fresh, are never cached; but there is no way to completely circumvent this problem without disabling caching.

The record cache can be configured in the dm/objects/config.xml file, using the several parameters that begin with api.cache. Feel free to delete any of the .tmp files in the cache folder to force an update of particular (or all) records.

After a cached record exceeds duration days old, it gets overwritten with a fresh copy from the server. If your collections and metadata change often and those changes need to be public immediately, it may be best to reduce the duration parameter. If not, by all means, increase it, as the higher it is, the more performance benefit you will receive from caching.

Template extensions

If the functionality that the Draw classes provide does not meet your needs, and you have basic skills in PHP, you can add your own functionality by writing an extension.

There are actually several ways you could add custom code to dmBridge to either add or override existing functionality.

  • The wrong way: You could take a look at the source code of the method you're using and re-implement your own version right there in the page template. This would be quick, but it would clutter up your template.
  • A variation on the wrong way: Break that custom code out into a separate file and include() it. This would work, but would be nonstandard.
  • The way you should never even think about: Instead of reimplementing the code in the template, you could rewrite the method itself directly into the dmBridge. This was necessary in CONTENTdm® template customization process, but is highly discouraged in dmBridge - you should never touch the dmBridge files. There is a better way.
  • The right way: You could override the method. That's what extensions do. Extensions are the only supported way to add or override functionality in dmBridge.

Some of the advantages of using extensions are:

  • They are isolated from the rest of the dmBridge code
  • They are isolated from the rest of your template code
  • They are consolidated and organized, making customizations easy to find
  • They make upgrading easier
  • They respect the standard dmBridge data interfaces
  • They prevent unexpected side effects from propagating to unexpected places
  • They are systematically testable
  • They can be easily shared with other dmBridge users

dmBridge is an object-oriented system composed of classes which are themselves composed (in part) of methods, each one of which performs a specific task. For example, Draw::loadTime() displays the amount of time the page took to load, in seconds, rounded to 4 digits.

What if you wanted it to be rounded to 8 because time is money? Without extensions, there would be no clean way to do it. With extensions, you could add a file called CustomDraw.class.php to your templates' extensions folder, copy the loadTime() method into it from the original place, and tweak it to round to 8 digits instead of 4. Alternately, if you put this custom file in the dmBridge core's extensions folder, it would be available to all template sets.

PHP is not able to load two classes with the same name simultaneously (without namespaces, anyway, which dmBridge does not use). Whenever dmBridge looks for a class, its class loader will look in the template sets' extensions folders first. If it finds a matching class, it will load that class and then stop. The loaded extension will override the built-in class with the same name. The class loader will look for classes in the following sequence:

  1. Inside template sets' extensions folder
  2. Inside dmBridge core's extensions folder
  3. In the dm/objects/extensions folder

So, when an extension with the same name as a built-in class exists in an extensions folder, the built-in class will not be loaded. This is bad; don't give an extension class a name that is already being used by dmBridge.

But what if the extension only contains one overridden method from the built-in class? What happens to the rest of the methods in the overridden class? Do they just get ignored? What if they're important?

The answer is the the extension class needs to extend (inherit from) its overridden class, so that all of the rest of the functionality remains in place.

If you don't have much exposure to object-oriented programming, don't worry, because the hard part is all behind you now and the examples are straight ahead.

Here's what an extension looks like:

<?php
class CustomObjectDraw extends ObjectDraw {

  
/**
    * Here, we are reimplementing the method we want to
    * override: in this case, the ObjectDraw class' metadata()
    * method. And then we return the result:
    */
  
public static function metadata() {
     
// Do some stuff...
     
$result = '<p>Test</p>';

     
// Finished, now return it
     
return $result;
   }

  
/**
    * Here, we write our own totally new and unique method
    * which does not already exist in ObjectDraw.
    */
  
public static function doSomethingCool() {
     
$text = '<p>I just did something cool...</p>';
      return
$text;
   }

}
?>

In the first method ("public static function"), we are overriding the metadata() method in the ObjectDraw class. In the second, we are writing a new method called doSomethingCool(). Either of these will be accessible from our page templates like so:

<?php
echo CustomObjectDraw::metadata()
?>

<?php
echo CustomObjectDraw::doSomethingCool()
?>

Note that CustomObjectDraw::metadata() does not affect ObjectDraw::metadata() in any way. No code in any collection templates that references the latter will be affected. So, you can use the CustomObjectDraw version in some places, and the built-in version in others.

Extension files must be saved in one of two places:

  1. Your templates folder's extensions subfolder
  2. Your dm/objects/extensionsfolder

They need to be named the same as their class name; the example above would be named CustomObjectDraw.class.php. The class name doesn't matter; you could name it PeanutButter if you wanted. CustomObjectDraw just helps to make it clear what it is.

Real-world extension example

The above code is not very useful in practice. Here is an example of an extension written at the University of Nevada, Las Vegas for the Southern Nevada: The Boomtown Years collection. We have a bunch of classroom activities stored in an XML file, with references to certain objects within each activity node. We needed to be able to provide dynamic links, from object view, to activities that referenced the current object. Here's how we did it in an extension:

<?php
abstract class CustomObjectDraw extends ObjectDraw {

   private static
$html;

  
/**
    * @return Boolean
    */
  
public static function arelinksToRelatedActivities() {
      if (!
self::$html) {
        
self::$html = self::linksToRelatedActivities();
      }
      return (bool)
self::$html;
   }

  
/**
    * @return String of HTML anchor tags
    */
  
public static function linksToRelatedActivities() {
      if (
self::$html) {
         return
self::$html;
      }

     
// DOMDocument is part of PHP's XML DOM extension.
     
$dxml = new DOMDocument('1.0', 'utf-8');
     
$path = dirname(__FILE__) . '/activities/activities.xml';
     
// Load our activites XML file...
     
$dxml->load($path);

     
// Find the XML nodes we need using an XPath query...
     
$dxp = new DOMXPath($dxml);
     
$xpath = sprintf(
        
'//activity[relatedArtifacts/artifact/@uri = "%s"]',
        
dmObject::getCurrent()->getReferenceURL()
      );

     
// Insert the node values into HTML hyperlinks...
     
$links = array();
      foreach (
$dxp-&gt;query($xpath) as $node) {
        
$links[] = sprintf(
           
'<li><a href="/boomtown/activities/view.php?id=%d">%s</a></li>',
           
$node->getAttribute('id'),
           
dmString::websafer(
              
$node->getElementsByTagName('title')->item(0)->nodeValue
           
)
         );
      }
     
// Return the hyperlinks
     
self::$html = implode("\n", $links);
      return
self::$html;
   }

}
?>

From the object view template, we want to find out if there are any links to related activities, and if so, draw them. We put this in object/view_simple.html.php and object/view_compound.html.php:

<h4>Related activities:</h4>
<?php if (CustomObjectDraw::areLinksToRelatedActivities()): ?>
   <ul>
      <?php echo CustomObjectDraw::linksToRelatedActivities() ?>
   </ul>;
<?php else: ?>
   <p>None</p>
<?php endif ?>

Now when we upgrade either CONTENTdm® or dmBridge, our customization is safe. It's also neat, organized, and easy to understand from the templates.

The code above would look quite daunting to a non-programmer. For better or worse, extensions are for programmers. Non-programmers will have to rely on the Draw methods.