SilverStripe 2.4: Adding Some Spice with Widgets and Short Codes


SilverStripe 2.4 Module Extension, Themes, and Widgets: Beginner's Guide: RAW

SilverStripe 2.4 Module Extension, Themes, and Widgets: Beginner's Guide: RAW

Create smashing SilverStripe applications by extending modules, creating themes, and adding widgets

        Read more about this book      

(For more resources on this subject, see here.)

Why can't we simply use templates and $Content to accomplish the task?

  • Widgets and short codes generally don't display their information directly like a placeholder does
  • They can be used to fetch external information for you—we'll use Google and Facebook services in our examples
  • Additionally they can aggregate internal information—for example displaying a tag cloud based on the key words you've added to pages or articles

Widget or short code?

Both can add more dynamic and/or complex content to the page than the regular fields. What’s the difference?

Widgets are specialized content areas that can be dynamically dragged and dropped in a predefined area on a page in the CMS. You can't insert a widget into a rich-text editor field, it needs to be inserted elsewhere to a template. Additionally widgets can be customised from within the CMS.

Short codes are self-defined tags in squared brackets that are entered anywhere in a content or rich-text area. Configuration is done through parameters, which are much like attributes in HTML.

So the main difference is where you want to use the advanced content.

Creating our own widget

Let's create our first widget to see how it works. The result of this section should look like this:

Time for action – embracing Facebook

Facebook is probably the most important communication and publicity medium in the world at the moment. Our website is no exception and we want to publish the latest news on both our site and Facebook, but we definitely don't want to do that manually.

You can either transmit information from your website to Facebook or you can grab information off Facebook and put it into your website. We'll use the latter approach, so let's hack away:

  1. In the Page class add a relation to the WidgetArea class and make it available in the CMS:
  2. public static $has_one = array(
    'SideBar' => 'WidgetArea',
    public function getCMSFields(){
    $fields = parent::getCMSFields();
    new WidgetAreaEditor('SideBar')
    return $fields;

  3. Add $SideBar to templates/Layout/ in the theme directory, wrapping it inside another element for styling later on (the first and third line are already in the template, they are simply there for context):

    <aside id="sidebar">$SideBar</aside>

    <aside> is one of the new HTML5 tags. It's intended for content that is only "tangentially" related to the page's main content. For a detailed description see the official documentation at

  4. Create the widget folder in the base directory. We'll simply call it widget_facebookfeed/.
  5. Inside that folder, create an empty _config.php file.
  6. Additionally, create the folders code/ and templates/.
  7. Add the following PHP class—you'll know the filename and where to store it by now. The Controller's comments haven't been stripped this time, but are included to encourage best practice and provide a meaningful example:

    class FacebookFeedWidget extends Widget {
    public static $db = array(
    'Identifier' => 'Varchar(64)',
    'Limit' => 'Int',
    public static $defaults = array(
    'Limit' => 1,
    public static $cmsTitle = 'Facebook Messages';
    public static $description = 'A list of the most recent
    Facebook messages';
    public function getCMSFields(){
    return new FieldSet(
    new TextField(
    'Identifier of the Facebook account to display'
    new NumericField(
    'Maximum number of messages to display'
    public function Feeds(){
    * URL for fetching the information,
    * convert the returned JSON into an array.
    $url = '' . $this->Identifier .
    '/feed?limit=' . ($this->Limit + 5);
    $facebook = json_decode(file_get_contents($url), true);
    * Make sure we received some content,
    * create a warning in case of an error.
    if(empty($facebook) || !isset($facebook['data'])){
    'Facebook message error or API changed',
    * Iterate over all messages and only fetch as many as needed.
    $feeds = new DataObjectSet();
    $count = 0;
    foreach($facebook['data'] as $post){
    if($count >= $this->Limit){
    * If no such messages exists, log a warning and exit.
    if(!isset($post['from']['id']) || !isset($post['id'] ||
    'Facebook detail error or API changed',
    * If the post is from the user itself and not someone
    * else, add the message and date to our feeds array.
    if(strpos($post['id'], $post['from']['id']) === 0){
    $posted = date_parse($post['created_time']);
    $feeds->push(new ArrayData(array(
    'Message' => DBField::create(
    'Posted' => DBField::create(
    $posted['year'] . '-' .
    $posted['month'] . '-' .
    $posted['day'] . ' ' .
    $posted['hour'] . ':' .
    $posted['minute'] . ':' .
    return $feeds;

  8. Define the template, use the same filename as for the previous file, but make sure that you use the correct extension. So the file widget_facebookfeed/templates/ should look like this:

    <% if Limit == 0 %>
    <% else %>
    <div id="facebookfeed" class="rounded">
    <h2>Latest Facebook Update<% if Limit == 1 %>
    <% else %>s<% end_if %></h2>
    <% control Feeds %>
    <% if Last %><% else %><hr/><% end_if %>
    <% end_control %>
    <% end_if %>

  9. Also create a file widget_facebookfeed/templates/ with just this single line of content:


    We won't cover the CSS as it's not relevant to our goal. You can either copy it from the final code provided or simply roll your own.

  10. Rebuild the database with /dev/build?flush=all.
  11. Log into /admin. On each page you should now have a Widgets tab that looks similar to the next screenshot. In this example, the widget has already been activated by clicking next to the title in the left-hand menu.

    If you have more than one widget installed, you can simply add and reorder all of them on each page by drag-and-drop. So even novice content editors can add useful and interesting features to the pages very easily.

  12. Enter the Facebook ID and change the number of messages to display, if you want to.
  13. Save and Publish the page.
  14. Reload the page in the frontend and you should see something similar to the screenshot at the beginning of this section.

    allow_url_fopen must be enabled for this to work, otherwise you're not allowed to use remote objects such as local files. Due to security concerns it may be disabled, and you'll get error messages if there's a problem with this setting. For more details see

What just happened?

Quite a lot happened, so let's break it down into digestible pieces.

Widgets in general

Every widget is actually a module, although a small one, and limited in scope. The basic structure is the same: residing in the root folder, having a _config.php file (even if it's empty) and containing folders for code, templates, and possibly also JavaScript or images. Nevertheless, a widget is limited to the sidebar, so it's probably best described as an add-on. We'll take a good look at its bigger brother, the module, a little later.

You're not required to name the folder widget_*, but it's a common practice and you should have a good reason for not sticking to it.

Common use cases for widgets include tag clouds, Twitter integration, showing a countdown, and so forth. If you want to see what others have been doing with widgets or you need some of that functionality, visit

Keeping widgets simple
In general widgets should work with default settings and if there are additional settings they should be both simple and few in number. While we'll be able to stick to the second part, we can't provide meaningful default settings for a Facebook account. Still, keep this idea in mind and try to adhere to it where possible.

Facebook graph API

We won't go into details of the Facebook Graph API, but it's a powerful tool—we've just scratched the surface with our example. Looking at the URL<username>/feed?limit=5 you only need to know that it fetches the last five items from the user's feed, which consists of the wall posts (both by the user himself and others). <username> must obviously be replaced by the unique Facebook ID—either a number or an alias name the user selected. If you go to the user's profile, you should be able to see it in the URL.

For example, SilverStripe Inc's Facebook profile is located at—so the ID is 44641219945. That's also what we've used for the example in the previous screenshot.

For more details on the Graph API see

Connecting pages and widgets

First we need to connect our pages and widgets in general. You'll need to do this step whenever you want to use widgets.

You'll need to do two things to make this connection:

  1. Reference the WidgetArea class in the base page's Model and make it available in the CMS through getCMSFields().
  2. Secondly, we need to place the widget in our page.


You're not required to call the widget placeholder $SideBar, but it's a convention as widgets are normally displayed on a website's sidebar. If you don't have a good reason to do it otherwise, stick to it.

You're not limited to a single sidebar
As we define the widget ourselves, we can also create more than one for some or all pages. Simply add and rename the $SideBar in both the View and Model with something else and you're good to go. You can use multiple sidebars in the same region or totally different ones—for example creating header widgets and footer widgets. Also, take the name "sidebar" with a grain of salt, it can really have any shape you want.

What about the intro page?

Right. We've only added $SideBar to the standard templates/Layout/ Shouldn't we proceed and put the PHP code into ContentPage.php? We could, but if we wanted to add the widget to another page type, which we'll create later, we'd have to copy the code. Not DRY, so let's keep it in the general Page.php.

The intro page is a bit confusing right now. While you can add widgets in the backend, they can’t be displayed as the placeholder is missing in the template. To clean this up, let's simply remove the Widget tab from the intro page. It's not strictly required, but it prevents content authors from having a field in the CMS that does nothing visible on the website. To do this, simply extend the getCMSFields() in the IntroPage.php file, like this:

function getCMSFields() {
$fields = parent::getCMSFields();
$fields->removeFieldFromTab('Root.Content.Main', 'Content');
$fields->removeFieldFromTab('Root.Content', 'Widgets');
return $fields;

        Read more about this book      

(For more resources on this subject, see here.)

Facebook feed widget

Now that we've made the general connection, it's time to focus on our widget itself. As this is quite a bit of code, let's break it down:

Facebook output

As a quick example on how the data that we receive from Facebook looks like, go to This fetches the last two entries of SilverStripe Inc's (public) profile.

Here's the first entry, and the start of the second one:

"data": [
"id": "44641219945_305363039945",
"from": {
"name": "silverstripe",
"category": "Product/service",
"id": "44641219945"
"message": "We're looking for a senior PHP developer and an
office support person - could they be you? Work with great teams in
nice offices here in Wellington. It may not be sunny today, but it
might be tomorrow!",
"picture": "
"link": "",
"name": "Careers - SilverStripe - Open Source CMS /
"description": "SilverStripe is an award-winning, open
source web development company with our office based on Courtenay
Place, Wellington. We are one of the market leaders in New Zealand,
and take pride in the software we have created for the worldwide
"icon": "
"actions": [
"name": "Share",
"link": "\
"type": "link",
"application": {
"name": "HootSuite",
"id": "183319479511"
"created_time": "2011-02-07T22:27:03+0000",
"updated_time": "2011-02-07T22:27:03+0000"
"id": "44641219945_304306579945",
"from": {
"name": "Ignite Wellington",
"category": "Company",
"id": "144790912199888"

This is not a valid JSON document as the proper ending is missing.

The same information on the Facebook page looks like this:

The logic in general

The Widget class is actually a subclass of DataObject. This means it's automatically saved to the database in the table Widget and we can use the already known $db attribute.

First we add our two configuration options to the database—the Facebook ID and how many entries to display.

For the entries there is a sensible default value, so we use it. As we've already said, you should always provide them to make the use of your widget as easy as possible.

Then we add a title and description for our widget in the CMS so the content editors know what to do with it.

Finally we add the two configuration options to the CMS. We’ve done most of this before so let's jump to the new part:

$url = '' . $this->Identifier .
'/feed?limit=' . ($this->Limit + 5);
$facebook = json_decode(file_get_contents($url), true);

  • First we fetch the desired user's wall posts using file_get_contents()—that's why we need allow_url_fopen. As the wall posts include both the user's posts and posts by others on his wall, we fetch five more entries than we actually need. We'll later throw away the posts of other users, so the five additional messages are simply backup. Five may not even be enough—if you run into problems, fetch some more.
  • Facebook provides the information JSON encoded. Lucky for us PHP has a function (json_decode()) to simply convert that into a named array.

    if(empty($facebook) || !isset($facebook['data'])){
    'Facebook message error or API changed',

  • To avoid any nasty errors, we check if we really got a valid response, or if there might be a problem on Facebook's side, or if the API was changed.
    Never trust another system, always check!
    In case of an error, we log the error internally (user_error()) and return nothing to the user. So visitors won't be able to use this specific feature, but the rest of the site will still be working as expected.

    There are three kinds of errors that you can log and which are then (if configured) e-mailed to you. From the most to the least serious: E_USER_ERROR, E_USER_WARNING, E_USER_NOTICE.

    $feeds = new DataObjectSet();
    $count = 0;
    foreach($facebook['data'] as $post){
    if($count >= $this->Limit){
    if(!isset($post['from']['id']) || !isset($post['id']) ||
    'Facebook detail error or API changed',

  • Then we iterate over the data we got from Facebook. If we've already found enough messages, we can stop the loop. Again check for possible problems.

    There are three kinds of errors that you can log and which are then (if configured) e-mailed to you. From the most to the least serious: E_USER_ERROR, E_USER_WARNING, E_USER_NOTICE.

    if(strpos($post['id'], $post['from']['id']) === 0){
    $posted = date_parse($post['created_time']);
    $feeds->push(new ArrayData(array(
    'Message' => DBField::create(
    'Posted' => DBField::create(
    $posted['year'] . '-' .
    $posted['month'] . '-' .
    $posted['day'] . ' ' .
    $posted['hour'] . ':' .
    $posted['minute'] . ':' .

  • Before adding a message to our output, we make sure that it was really sent by the page's owner and not someone else. We simply check that the is equal to the start of id as this contains the target's ID.
    Don't rely on the to part of the message, there are messages which don't have it.
  • If everything is okay, we add the message to the final output. The JSON doesn't include HTML entities so we need to recreate newlines through the function nl2br(). We also add the date when it was created—parsing it into a date and time object in the format of YYYY-MM-DD HH:MM. Don't worry if this isn't totally clear right now, we'll take a detailed look at DBField::create() in the next section.
  • As the template doesn't know what to do with a raw array, it must be wrapped in a DataObjectSet which consists of ArrayData objects. These then contain arrays which are accessed in a template's control loop. After setting it up like this, it's as easy as pie to access the variables in the View.

Taking a look at the details

There are three elements in the previous code snippet that deserve special attention: Error handling, DBField::create(), and DataObjectSet().

Error handling

There are generally two approaches to handling errors: You can either rely on the user_error() method:

user_error('Facebook detail error or API changed', E_USER_ERROR);

Or you can throw an exception:

throw new SS_HTTPResponse_Exception(
'Facebook detail error or API changed'

Both code snippets achieve the same result, but there are some differences to consider:

  • SilverStripe core uses exceptions for errors.
  • Exceptions can only trigger an error. You must rely on user_error() to generate a warning or notice.
  • If your method calls the method of another Controller, the caller can catch an exception of the callee. In our example we can't catch an exception as there's just a single method involved. If you stumble upon a block starting with try { and ending with } catch(Exception $e), remember that this is how you catch an exception.
  • If you throw an SS_HTTPResponse_Exception() it's automatically caught by SilverStripe and an error message is displayed.

In our example we only want a warning: Log the problem, abort the current operation, and try to load the rest of the page. So we can't use an exception.

If you feel you need a full-blown error handler and want to stop the execution of the whole page, it's a matter of choice. But as core developers are using exceptions, it makes sense to do the same.


Instead of passing plain strings to the View, we can use DBField::create() to generate database objects. This has one big advantage over strings: You can use the View's formatters (such as $Posted.Nice for dates) instead of manually setting it up in the Controller. This makes it way more flexible and is generally the best approach in SilverStripe.

We've already discussed HTMLText and SS_Datetime. The first one is very simple: Simply provide a string which might contain HTML. The second one expects the format of YYYY-MM-DD HH:MM (following ISO 8601)—only then is the date is parsed correctly.


As the DataObjectSet is both often used and a bit complex, let's take a look at a minimal example:

public function Elements(){
// You would need to set up the $elements array here
$result = new DataObjectSet();
foreach($elements as $element){
$result->push(new ArrayData(array(
'A' => $element['a'],
'B' => $element['b'],
return $result;

DataObjectSet() is a set of DataObjects, which we've created with DBField::create(). Think of it as a container over which the View's control structure can iterate.

ArrayData() makes the array keys accessible as template variables. To access this example's data, your View would look like this:

<% control Elements %>
<% end_control %>

Not too hard after all, is it?

You could even do the same just using nested and associative arrays, which are automatically cast to ArrayData(). But we'll only use the first approach as setting the current index position is a bit cumbersome and more error prone than relying on the push() function.

The template

The template shouldn't really use anything new, but we're using some "clever" if-else statements:

  • First we're checking <% if $Limit == 0 %> (coming from the $db array in the widget class). Generally you'd disable the widget instead of displaying zero messages, but we're simply covering that situation in our template. As there isn't a negation we're providing an empty output for zero messages, otherwise the information box is added to the page.
  • If there is more than one message, we want to display Latest Facebook Updates instead of Latest Facebook Update. Without negation, bigger and smaller comparators, we're falling back to: <% if Limit == 1 %><% else %>s<% end_if %>. This might be a little cumbersome, but you're getting used to it—that's the price of a very simple template language.
  • The <% if Last %> ensures that a horizontal rule is added between messages, making sure it's added only in between messages and not after the last entry. But why are we making it so complicated—what about <% if Middle %>? Good idea, but that wouldn't put a line after the first element. And <% if First || Middle %> wouldn't work as well as this would always add a horizontal rule after the first element—even if there's just a single one.

Using the control structure we iterate over all messages, accessing $Message and $Posted. We've already taken a look at this in the previous paragraph. In the default case it's only a single iteration, but we're also prepared for more messages.

The other template

What is the file doing? Actually it's not really necessary for the widget to work. It only overwrites SilverStripe's default widget holder (located in sapphire/templates/ that wraps each of our widgets in some additional markup:

<div class="WidgetHolder $ClassName
<% if FirstLast %> $FirstLast<% end_if %>">
<% if Title %><h3>$Title</h3><% end_if %>

And if you don’t need it, we’ve just discussed how to replace it. We could edit this file directly, but that would be a really bad idea. We'd have to redo the change every time we update the core files. Thanks to inheritance, this is not necessary.

All we need to do is add a file with the same name to our widget's template directory and it's used instead. The $Content placeholder only includes our own—if you leave the widget holder file empty, our custom template is ignored.

Why is this file even there? We didn't need it and it only makes our life more complicated... Well, assume you want to wrap all your widgets in the same markup. Copying that code to every widget's template is pretty cumbersome, so it might be handy to have this container in place. And if you don't need it, we've just discussed how to avoid it.

More widgets

In our example we used Facebook as our external data source. In most cases you'll use information from inside the system using DataObjects. For example, for displaying the latest content, the newest member, and so on. This is just a hint to guide you in the right direction—you can, but you are not required to use the database when using widgets.

Additionally, widgets are very portable. We could have hard-coded all of this functionality right into our custom template, but only widgets can be easily reused on other sites. Simply copy the widget folder over, rebuild the database, and you're finished!

Have a go hero – Twitter

There are already Twitter widgets available for download. But if you want to give it a try yourself or want to include Twitter into our Facebook widget, don't be shy.

Twitter provides a pretty similar API for retrieving tweets, <username> must again be replaced:<username>.json?count=2

Note that the response JSON is different. Besides changing the URL, you'll also need to slightly adapt the PHP code processing the underlying data.

If you want more details, take a look at the official documentation at

Text parser

Is the nl2br() in the widget's logic bothering you as well? Adding markup anywhere else than in the template is definitely not nice. Luckily for us, SilverStripe provides a handy tool for this problem.

Time for action – doing it "right"

  1. Remove the nl2br() in the file FacebookFeedWidget.php, so the code looks like this:

    'Message' => DBField::create('Text', $post['message']),

  2. Create a new file mysite/code/ Nl2BrParser.php with the following code:

    class Nl2BrParser extends TextParser {
    public function parse(){
    return nl2br(htmlentities(

  3. In the template replace $Message with $Message.Parse(Nl2BrParser).
  4. Flush your page for the template engine to pick up the parser—rebuilding the database is not required.

What just happened?

Obviously we've created a text parser. That's pretty straightforward: Create a new class that extends TextParser and implement a function parse(); in the template, attach .Parse() to the placeholder and add the parser's class name inside the brackets. The placeholder that you want to change can be accessed through $this->content in the parser.

There are two things you should be aware of:

  • TextParser, as the name implies, can only work with the Text data type or subtypes of it, for example HTMLText; not on Varchar or a plain old string. Therefore in our example we need to make sure it has the right data type: DBField::create('Text', $post['message']).
  • When using your own text parser, you potentially circumvent SilverStripe's security mechanisms. Text only strings won't escape HTML automatically. This is only done by the default DBField, which we've overwritten. So we need to take care of that ourselves, for example with htmlentities()—stripping any markup from the text. We've done just that in our example.

Where to store the parser
Whether you want to put the parser in mysite/code/ or into the widget's code/ directory depends on where you want to use it. We assumed that it may come in handy for other elements besides widgets, so we used mysite/. If you're sure the parser will only be used in a single widget, you should store it there directly.

Other examples where text parsers can come in handy include displaying only the first X characters or words of a string, syntax highlighting, and so on.

        Read more about this book      

(For more resources on this subject, see here.)


It's great that we've added the Facebook integration, it makes our pages much more lively without adding any work for the content editors. But it's not efficient—every single time a visitor loads the page, we need to fetch the latest information from the remote server, parse it, and output it. While we don't care too much for extra load on Facebook (they can definitely handle it as we're hardly making a big difference), it's bad for our performance:

  1. The widget class needs to fetch the data and wait for the response. The visitor must wait for that to finish.
  2. We need to parse the response and output it, so there’s more work for our server to do. And in most cases it's the same work over and over again.

Can that work be done once and the result stored? From the second visitor onwards only the pre-computed output would need to be fetched. This is a very common concept called caching. SilverStripe provides multiple methods for making use of it.

Partial caching

This allows the caching of parts of a page—hence partial. You can mix cached and uncached parts easily. However this also provides the least performance gain of all methods as the Controller is always needed for generating the final page. That said, in our example it removes the network latency and web service call to Facebook, so it provides a major speed increase.

We'll take a better look at it shortly.

Static publisher

Partial caching is not enough once you get lots of hits, at which point you should try out the static publisher. It generates the raw HTML output and serves it without ever touching PHP or the database. You do need to regenerate it every time something changes, but it allows you to have hundreds of page views per second.

The downside of this is that it is rather complex to set up, as every page needs to know about all potential changes. For example if you add a page to the menu, all pages displaying the menu will need to be regenerated. We won't cover the static publisher in detail. Just remember that there is more than partial caching, should you ever need it.

Static exporter

This method lets you export a copy of your website as an archive. It's the same principle as the static publisher, only that you can't select specific pages (everything is exported) and that you need to manually upload the files to your server from the archive.

We won't cover this functionality as only few people should need it, but we've mentioned it for the sake of completeness.

Restful service

RestfulService enables you to easily connect to web services—just what we've done with Facebook. However its main power is that it can easily parse and search XML responses. It doesn't provide too much added value for our JSON documents and that's the reason why we haven't used it.

Representational State Transfer (REST) is an architectural style, and a web service is RESTful if it adheres to the REST principles. Without going into the finer details, the three aspects for REST are that a service has a URL, it communicates through HTTP, and the data is exchanged in valid Internet media types (commonly XML or JSON). Our Facebook example is a RESTful web service, we've just saved the proper definition for now.

This service doesn't only fetch a response, but can also cache it. So you don’t need to query Facebook again and again, but can instead rely on your server's cached version.

Partial caching

The basic principle of partial caching is to put control structures into the template files, telling the template engine what to cache. As the Controller is always used before getting to the template, you won't gain as much as with static publishing or exporting.


A basic example would look like this:

<% cached %>
The cached content...
<% end_cached %>

Time for action – caching

Let's try that out in our code.

  1. Add the following code to your mysite/_config.php:

    SS_Cache::set_cache_lifetime('any', -1, 100);
    } else {
    SS_Cache::set_cache_lifetime('any', 3600, 100);

  2. In mysite/code/Page.php add a new method in the Controller:

    protected function CacheSegment(){
    return $_SERVER["REQUEST_URI"];

  3. In themes/bar/templates/ wrap everything between the following two lines:

    <% cached 'page', LastEdited, CacheSegment %>
    <%-- The content of your page --%>
    <% end_cached %>

  4. Switch your page into test mode and reload a page.

We'll take a look at how to check if caching is actually working after going through the code.

What just happened?

Let's break it down into the following components:


In the configuration file we defined that caching should only be enabled in test and live mode, but not in development. So be sure to put the page into the right mode, before working on this.

SS_Cache::set_cache_lifetime('any', 36000, 100) sets the policy for any file, for a duration of one hour (time in seconds, 60 x 60 = 3600 s) with a priority of 100. You could set different strategies with various priorities, but we won't need that. We simply don't cache at all in development mode (-1 seconds) and cache everything for one hour in the other two modes.

We could of course make the duration much longer, but in our example this is not advisable. As we're caching the Facebook message on our page (our original goal), an update will appear after 60 minutes at most and 30 minutes on average. That sounds like a reasonable amount of time, so we'll go with that.


In the Controller we created a helper function, returning the current URL. Why didn't we use $URLSegment? Because it only returns the page's given URL, no actions, IDs or parameters. For finer control we'll need those later, so we create this simple helper.

Assume you have the URL /page/action/id/?param1=a&param2=b: $URLSegment returns page; our helper function returns /page/action/id/?param1=a&param2=b.


In the template we define when a cached page should be used and when not, so let's break down <% cached 'page', LastEdited, CacheSegment %>:

  • The part after cached is called the cache key. It consists of an unlimited number of comma separated elements. Having multiple elements allows you to have a fine-grained control over your cache strategy.
  • Cache keys can either be (quoted) strings or regular placeholders as well as Controller functions.
  • The values of the cache key are hashed. The system then tries to find a file-name of the hash value. If such a file is found, it is used, otherwise a new one is generated. So every time one of the elements changes, a new cache set must be created.
  • Old cache files are automatically subject to garbage collection.
  • First we define a name for our cache file (page) so that there are no collisions with other cache blocks.
  • Then we require the cache to be regenerated after the current page is saved—the $LastEdited placeholder or database field comes in handy here.
  • Finally we want one cache file per URL, using the Controller function CacheSegment that we created a little earlier. This makes sure that the /home and /start page don't rely on the same cache file. They display totally different content, so a common cache wouldn't work out.

General considerations

Should we use one big cache file, or many small ones instead?

It really depends, as there is always some trade-off. In general if your cache blocks are very small, you'll need fewer than with bigger blocks. Having small cache files, you can aggregate them in various combinations, whereas with bigger ones you'll need to create many, often containing the same content with minor variations.

So we should rather make the blocks as small as possible? Not really—fetching a cached file is expensive in terms of performance. You need to calculate the hash, check if the cache already exists, and possibly generate a new one. So bigger blocks with fewer checks for cache files will generally lead to better performance, at the cost of the number and size of cache files. Let's illustrate this with two simple examples. For clarity, the actual content of the different elements is simply represented by a SilverStripe comment:

One big cache block where /home/something results in a different “URL variation” block than /home/else.

<% cached 'page', LastEdited, CacheSegment %>
<%-- Header --%>
<%-- Menu --%>
<%-- Content --%>
<%-- URL variation --%>
<%-- Footer --%>
<% end_cached %>

Many small cache blocks where header, menu, and footer sections are relatively static and identical on all pages. /home/something results in a different "URL variation" block than /home/else, but the block is identical on /into/something. There is a Controller method UrlVariation() to check for this (referenced in the cached statement).

<% cached 'header' %>
<%-- Header --%>
<% end_cached %>
<% cached 'menu' %>
<%-- Menu --%>
<% end_cached %>
<% cached 'content', LastEdited %>
<%-- Content --%>
<% end_cached %>
<% cached UrlVariation %>
<%-- URL variation --%>
<% end_cached %>
<% cached 'footer' %>
<%-- Footer --%>
<% end_cached %>

Assume we have a lot of pages such as /home, /home/a, /home/b, /home/c, /intro, /intro/a, /intro/b, /intro/c, /about, /about/a, /about/b, /about/c, and so on. The difference between the two approaches are as follows:

  • The first code example runs faster than the second one. The system only needs to check one cache key and fetch a single file, whereas the second one requires five such actions.
  • The first example requires more disk space as a lot of information will be duplicated (header, menu, and footer) in each cached file.
  • The second approach requires fewer files: One global header, menu, and footer, one block for each first URL part (/home, /intro, /about) and one for each second URL part (nothing, /a, /b, /c), resulting in ten small files.
  • The first approach requires more and bigger files: One for each unique URL. Twelve altogether for the URLs listed above, but the more pages you have the more this number will grow.

You'll need to find the best solution for yourself. As our pages are pretty simple, we'll go with big cache blocks—as you've already seen.

Finally, at the risk of stating the obvious: Caching static parts in the template (pure HTML or CSS) won't provide much benefit, there are no database queries or PHP logic to avoid. Focus on the right parts when caching!

Flushing the page does not delete the cache! As you're adding a URL parameter, a new cache file is created in our example, but the general cache file is left untouched, waiting to time out. If you really need to get rid of the cache, remove the files from the caching directory (which we'll cover in a minute).

More on partial caching

Cached blocks can be nested, as they are handled independently. This can be useful for mixing general and specific blocks.

Cached blocks can't be contained in if or control statements. If you do, you'll get an error message from the template engine. In most cases this should be avoidable as there are some advanced caching statements you can use:

  • To cache a block for each user individually, use CurrentMember.ID. For example if you want to output the current user's first name you could cache it like this:

    <% cached 'user', CurrentMember.ID %>
    Hello $CurrentMember.FirstName!
    <% end_cached %>

  • To make it dependant on the user's rights, use CurrentMember.isAdmin
  • To require a new cache block whenever any page in the system changes, use Aggregate(Page).Max(LastEdited). This takes all pages (Aggregate(Page)) and by taking the highest (Max(LastEdited)), gets the last time, any page was edited.

If you want to exclude something within an otherwise cached block, wrap it between <% uncached %> and <% end_uncached %>. You can also provide a cache key, which is of course ignored, but it is very handy to (temporarily) disabling some caching.

Taking it even further
If you need more performance, be sure to check out the API documentation. You can for example use Memcached ( for keeping cache entries in memory. While you'll need more (expensive) memory in your server, it's much faster than reading cached files from disk.

Carefully test your cache settings

At first it sounds trivial—what could possibly go wrong?! But it can be tricky. Let's think about our (very simple) menu:

  • The naive approach would possibly be to simply cache the menu for all pages, as it's identical for all pages.
  • Not quite! There's a tiny bit of markup added to distinguish the currently active page: Instead of <li class="link"> it's <li class="current">. So in order to mark the current page through CSS, we need to store the menu for each page, as it's always different. Is that it?
  • Not quite so fast. If we change, delete, or add a page, we’ll want all pages to reflect such a change immediately. So instead of only caching the menu per page, we'll also need to expire all of these cached files as soon as a single menu entry is changed.

So for our menu we could either add a nested caching statement in the menu file themes/bar/templates/Includes/ or change the page-wide setting. In light of using bigger caching files, we're going with the second option, so change the original statement (which we've just described) to:

<% cached 'page', Aggregate(Page).Max(LastEdited), CacheSegment %>

Alternatively you could change your menu to:

<nav id="menu">
<% cached 'menu', Aggregate(Page).Max(LastEdited), URLSegment %>
<% control Menu(1) %>
<li class="$Linkingmode">
<a href="$Link">$MenuTitle</a>
<% end_control %>
<% end_cached %>

So we have our menu block. Whenever any page in the system is changed, a new cache block is required.

Very easy so far, but why do we use the old $URLSegment instead of our CacheSegment() function? In our specific case we don't need it. The menu is so simple that it only contains the main page—if there are any subpages or further arguments, the menu should be the same. So there is no point in generating multiple identical cache files.

Yes, it's a trade-off. As we've already said, consider for yourself which is the best option in your specific project.

While it's not really hard, some care is needed to find the right strategy. You'll want to remove outdated information quickly while reusing cached parts as often as possible. Even for such simple elements as our menu, it can be a little tricky.

Cache directory

Now that we know how caching works, it would be nice to know where the cached files are stored. The easy answer is: in SilverStripe's cache directory. But that can be one of three possible places:

  • If you define the constant TEMP_FOLDER, its value is used as the cache directory. However note that you can't set this up in mysite/_config.php, because the constant must be set up earlier. If you want to use this approach, you need to add the following line of code to _ss_environment.php, which we've already covered in configuration:

    define('TEMP_FOLDER', '/full/path/to/cache/directory');

  • If you didn't define TEMP_FOLDER, the framework checks if you have a folder silverstripe-cache/ in your installation's base directory (just beside sapphire/, cms/, mysite/, and so on). If such a folder is found, it's used as the cache directory.
  • If you didn't set the constant or create the folder, SilverStripe defaults to the value from the system's temporary directory and appends silverstripe-cache plus your installation's base directory (slashes replaced by hyphens). Assume you installation is located in Webserver/silverstripe6/ and PHP has not been configured to specifically use a different temporary directory:
    • On Linux or Mac, the first part defaults to the /tmp/ directory. So the cache directory would be: /tmp/silverstripe-cache-Webserver-silverstripe6.
    • On Windows, it's a user-specific folder, for example C:/DOCUME~1/theuser/LOCALS~1/Temp/—assuming the username is theuser and you're using an English version of Windows. So the cache directory would then be C:/DOCUME~1/theuser/LOCALS~1/Temp/silverstripe-cacheC--Webserver-silverstripe6—if your installation is on the C:\ drive.

Unless you're having issues with the cache directory, there is no point in changing the default.

Setting the correct permissions
For the caching to work, it's vital that the webserver is allowed to write and read the cache directory. This is a common pitfall. Double check that this is configured correctly.

Now let's get back to our original issue: Within your cache directory, there should be a subfolder cache/. It should include some zend_cache---cacheblock* files, that have been generated by your cache control structures. Take a look at them. You'll see plain HTML and possibly some CSS or JavaScript. It’s all client-side code ready to be sent to visitors without further server-side processing.

Once you've found these files, you can be sure that caching is working as it should.

In the cache directory itself (not the cache/ subfolder) SilverStripe automatically caches parsed templates and database-related files, speeding up the whole site's speed considerably. If you delete these files, they'll be automatically regenerated, but it may take a little while.

Performance gains

The final question before finishing this section is: How big is the performance benefit when using partial caching? Let's take a look at some simple benchmarks.

The "server" in this case was a netbook. However as many hosting providers won't provide much more power than that on their low to medium sized packages, this should be quite informative. Just don't think that SilverStripe can’t go faster on more powerful machines.

There are generally two sides when doing benchmarks: The server-side (how many requests can the server handle) and the client-side (how fast is my single request). We'll take a look at both.


For our simple benchmark, we're using ApacheBench (ab) which is part of the Apache HTTP server. It's a shell command line tool. The following example tries to fetch the given URL 50 times with five concurrent connections as fast as possible:

ab -n 50 -c 5 http://localhost/

The results of this test, done on our page including the latest Facebook messages, are (with minor variations):

  • With partial caching enabled, it took approximately 22.85 seconds, resulting in nearly 2.2 requests per second
  • With partial caching disabled it took approximately 32.38 seconds, resulting in a good 1.5 requests per second

This means we could fulfill nearly 50% more requests with partial caching than without!


To measure the time it takes to fulfill your request, you can (for example) use Firefox's excellent FireBug plugin,

While there's less difference when processing a single request than when processing many concurrent ones, the overall trend is the same. Take a look at the following screenshots—the first one was taken with partial caching enabled and the second one without it.

While not SilverStripe-specific, PHP accelerators can give your applications a major performance boost. They're also called op-code caches and are generally a PHP extension, which caches compiled bytecode. Therefore the PHP code can be parsed and compiled once instead of for each request. Popular implementations are the Alternative PHP Cache (APC), eAccelerator, or XCache. This is an easy enhancement you shouldn't miss!

Creating our own short code

It's time to get started on the second part of this article: Short codes.

Think of short codes as placeholders inside the CMS' input fields. They are automatically replaced when the page is loaded. Instead of forcing content authors to add some complex HTML, you can do the heavy lifting for them. Let’s build our own short code, as this will explain it best.

Time for action – how to find us

We want to add a map with the location of the bar—it must be easy for customers to find us! However we don't want to put the map in the sidebar (or footer), but right in the middle of $Content in the template.

If a widget satisfies your needs, go to the SilverStripe website where you can find readily available solutions for maps.

Let’s create our own short code.

  1. We'll need to register our short code in mysite/_config.php:

    array('Page', 'GMapHandler')

  2. In mysite/code/Page.php add the following method to the Model:

    public static function GMapHandler($attributes, $content = null,
    $parser = null){
    if(!isset($attributes['location']) || !(strpos(
    ) === 0)){
    if(isset($attributes['width']) &&
    $width = $attributes['width'];
    } else {
    $width = 700;
    if(isset($attributes['height']) &&
    $height = $attributes['height'];
    } else {
    $height = 530;
    $customise = array();
    $customise['Location'] = $attributes['location'] .
    $customise['Width'] = $width;
    $customise['Height'] = $height;
    $template = new SSViewer('GMap');
    return $template->process(new ArrayData($customise));

  3. Create the file mysite/templates/Includes/—you'll need to create two new folders for that:

    <iframe width="$Width" height="$Height" src="$Location"></iframe>

  4. Rebuild your site.
  5. Go to and search for an address. In our example we went with New York's Fifth Avenue. Quite a nice spot, don't you think?
    Now copy the Paste link in email or IM.

  6. Log into /admin and create a new location page for your site, just a regular content page is fine. Add the short code anywhere in the content area of the rich-text editor. You can position the map wherever you want in the flow of the page's content, giving control to the content author. You don't need to type the URL, simply copy it. The following is an example of our short code:

    [GMap location=

  7. Save and Publish the page.
  8. Go to the frontend and reload the page where you've added the short code.

  9. All done!

We've built our short code on Google Maps as it's widely used and free of charge, but you could use any other service you want. We're not trying to promote any specific provider.

What just happened?

Now that you've seen how it works, let's go over the code and see what actually happens.

What do we want to achieve?

The basic idea is to include the map from Google in an iframe on our page. For doing that, we create our short code which is then replaced with the external content by the CMS.

As we've already seen, short codes are always enclosed in squared brackets. HTML markup uses angle brackets but is otherwise very similar. With both, you can use a single tag or a starting and ending tag, optionally with content in between and you can add zero or multiple attributes. So the following examples are all valid:

  • [Tag]
  • [Tag]Content in between[/Tag]
  • [Tag firstattribute=value secondattribute="quoted value"]

Note the conventions:

  • Tag is always replaced by the template method of the same name, so they should be in UpperCamelCase
  • Attribute names will always be lowercased in the Model, so keep them like this too
  • Template placeholders should be Uppercased
  • Attribute values can be quoted (if they contain a space, they must be)

So if this could be done with plain HTML, why are we creating short codes? On the one hand it's easier for content authors and usability should always be a design goal. On the other hand it's a security issue. You'll want to be able to control which external pages can be embedded into your site.


In the configuration we register our short code, so the system can replace it:


The first argument is the name of the tag we want to use in the content field. As we want to use [GMap target=...] it's GMap. The second argument is an array where we define that this short code maps to the GMapHandler method in the Page class. The method name can be freely chosen, but attaching handler to the short code is a logical choice which will be easily recognizable.


So in the Page class we have our GMapHandler method. Public because it's referenced outside its own class, static because it's not bound to a specific object and only depends on the input—but you already know that.

General composition

Before going into the specific details of our example, let's cover how this works in general terms.

The first argument contains a named array with all attributes. The second one contains the optional content between tags, and the third one contains the optional parser to use for this method. The names of the attributes are of course up to you.

In the end, we return which 'include' template to use in order to render our short code, including any variables it needs.

$template = new SSViewer('GMap'); defines the template to use. It will first look for a file mysite/templates/Includes/, if that isn't found it will try templates/Includes/ in your theme's directory.

We return this template and add (->process()) a named array of the variables the template uses. SilverStripe has its own wrapper ArrayData for the inclusion of variables, so the template can handle the array data.

Example-specific code

The second and third arguments are not used in our example, so we can ignore them.

We first check that the mandatory attribute location is present and that it is indeed a Google Maps address. The second check is to ensure careless or malicious content editors can’t include unwanted content in our page. It's a good idea to check that. If there is anything wrong, we simply return nothing, thereby replacing the short code with nothing.

We then check whether the optional attributes height and width are used, and if they are a valid number. If they fail the validation, we'll use default values.

For the template, provide the appropriate width and height, and also make sure that the Google Map is used in the embedded mode. It's better to do that in the code rather than make content editors fumble with the link—either forgetting or breaking it.

So valid short codes for this method could be (replacing the Google Map URL with three dots):

  • [GMap location=...]
  • [GMap location="..."]
  • [GMap location=... width=200]
  • [GMap location=... width=200 height=200]
  • [GMap location=...]I'm ignored[/GMap]


First off, why didn't we put the include in the templates/ directory—didn't we say that all View related code should be saved there?

Well, it depends. It's of course a valid approach and you can safely copy the include there and remove the empty folders in mysite/. However, our code doesn't really contain any layout-specific parts. It's just the basic structure which we would need to copy to all themes. Copying code to different locations should always raise a warning flag—that's anti-DRY. It's kind of a trade-off. As a rule of thumb:

  • If it's just structural markup that's the same for all themes, keep a single copy in mysite/
  • If it may change depending on the layout or if you actually need varying markup, put it in your themes

As you can see, we can mix both approaches as needed.

Otherwise there's nothing special happening. We use the three named array elements of the $customise variable from the Model in our iframe: $Width, $Height, $Location.

And it even validates, when using the HTML5 doctype at least. Note that you can have validation issues when using an iframe with XHTML.

Common pitfalls

You obviously need all the parts we've just covered. Besides naming and referencing the files and methods correctly, don't forget to rebuild your site.

If the short code is displayed untouched in the frontend, you either didn't register the right code in the configuration or you didn't rebuild the site.

Once you've created the short code and it's working as expected, think about how to document it. While it's in the code, that's invisible to content authors. Spend some time writing all short codes and their options down and providing that information to everyone involved. Otherwise there's a pretty good chance of features being forgotten in the long run...

Go forth and build your own

Now that we know how to create a custom short code for maps, think about other use cases for it.

SilverStripe itself uses short codes for internal links: [link id=1]Link to the page with ID 1[/link]. So if you change a page's URL, the link is automagically updated. Including videos could be another common example, but there are many other clever things you can possibly achieve.


In this article, we added some nifty features to our page. Besides implementing our own widget and short code, we also explored when to use which approach: Short codes are added into CMS fields while widgets are placed through a dedicated placeholder in the template. We've also explored caching, making our page perform a lot better and using fewer resources. While moving along we also picked up the concept of text parsers.

Using widgets, we have integrated our Facebook status messages into the website, so we don't need to manually copy the content any more.

Using short codes we've added the feature to include a map anywhere in the content area of our page.

Further resources on this subject:

You've been reading an excerpt of:

SilverStripe 2.4 Module Extension, Themes, and Widgets: Beginner's Guide

Explore Title