I'm not sure that we've publicly explained the SilverStripe templating system thoroughly yet. It is significantly different from other templating systems out there.
Our goal when designing the template system was to make SilverStripe's deliberately simple templating language tightly integrate with the rest of the system. Here are some of our motivations behind our architectural decisions, and an explanation on why we decided not to reuse conventional approaches and libraries.
Looking back in time
We started working with PHP around the time of the switch from PHP3 to PHP4. Code written in this era is easy to spot, as it tends to combine presentation (e.g. echo() statements) and business logic. Sure, it was very empowering to have HTML files interspersed with PHP logic, but this mentality wasn't appropriate for online software applications. Forum software written in PHP4 is a great example: if you wanted to change how a forum looked, you would need to edit various core PHP files. So you would suffer when it came time to upgrade.
This approach matured into the idea of having template files. While there are many template file syntaxes, they all succeed in separating the concerns of business logic and presentation - substance and style, if you will.
Most template approaches of that time were more or less valid HTML with a few placeholders that are substituted with data. In pseudo templating-syntax, this might look like:
What interested us was how they typically followed the same process:
Step 1: Determine what page of the website needs to be rendered. This might be via a parameter like index.php?pageId=7 or through URL rewriting (/about-us)
Step 2: With knowledge about the requested page record, we can run a multitude of database queries and produce a large array from matching database columns. For instance:
$PageContents = array("ID" => 7,
"Title" => "About Us",
"URL" => "/about-us/",
"Content" => "Our company was...",
"Created" => "2005-01-05 12:45",
"Created_nice" => "12:45, 5th of January, 2005"
"Author" => array("ID" => 10,
"Name" => "John Smith",
"Email" => "[email protected]",
Step 3: Substitute values in the array with those in the template file.While this approach is simple, it doesn't scale. As a project grows, the number of database queries increase, and include more complex joins and tables that have far more columns and rows. Additionally, values in the array often need to be processed by PHP. For example, making dates human readable (like Created_nice shown above). You might add secondary menus, pull in a featured news story from another page, and provide content from other sources. The result is a large nested array that takes a significant amount of computation and memory to produce.
In other words, this architecture aims to create an array of every piece of conceivable data, in the event it might be needed in the template. Many systems, including versions of SilverStripe CMS prior to 2.0 used this architecture, and took several seconds to render a page because of this problem.
In response, the only thing you can do is focus on optimising other areas of the process: caching database calls, caching HTML snippets, even pre-publishing content for a different, simpler system to deliver, and so on. Optimising other parts of the system take the heat off templating, but the trade-off there is that it can make the overall system complex, fragile, and bloated. However, even today, these techniques appear to be the mainstream approach.
Rethinking a template engine
When we remodelled our SilverStripe CMS 2.0 code to be PHP5-only, and heavily object-oriented, we had a rare opportunity to entirely rethink how the placeholders in templates are populated, and how they relate to your application.
The SilverStripe CMS design philosophy has always argued for very simple templates. That doesn't mean, however, that you will have a simple set of placeholders in your templates. Rather than anticipate what data you need, when we redesigned our templating system, we would read the template, and when we identified a placeholder, like $Content, we would go fetch it. In other words, we flipped the process around.
Instead of using an array to populate the placeholder, we used PHP objects. To be precise, for the $Content placeholder, we would call a method named Content(). If a method by that name exists and returns a value, this return value is substituted in place of the phrase $Content in a template.
Now, you can build some sophisticated optimisations around this, but the main advantage is that the template rendering time increases only slightly for great amounts of added complexity.
Let's look at how this works for the blog article page you're viewing right now.
Step 1: SilverStripe determines basic information, such as the type of page, from the current URL.
Step 2: You are visiting a Page object, which has "fields" like ID, Title, Content, and so on. However, the type of page is a Blog page, (a subclass of Page). The Blog page therefore inherits ID, Title, Content, etc, but is extended by additional "fields". The blog on silverstripe.org extends the system with fields for Author, Date, and Tags.
Step 3: SilverStripe creates an object (an instance of the BlogPage class). This is initially populated by raw data from the database in one sweep. The alternative, cherry-picking individual values from the database, means wasting time doing multiple queries. But, no PHP transformations are done to this data yet.
Step 4: This object now contains methods like Title(), Content(), Author(), and Tags(). These can be called inside your PHP code, but it also means $Title, $Content, $Author and $Tags can be used in your template.
Step 5: When reading a template and noticing placeholders like $Title and $Content, it will call the corresponding method on the BlogPage object. This method will often grab cached or new database data, and perform some logic on it if needed (e.g. convert "2010-11-29" to "Monday, 29th November"). The important difference is that the method is called on demand, and so this prevents unnecessary processing and memory use. Otherwise you might end up making hundreds of un-needed method calls.
Step 6: If you add custom functionality to the blog, such as adding an author's avatar, (AuthorImage), you do this by writing a new method in PHP onto the Blog class. Let's imagine that this method goes off and in realtime calls a Flickr webservice. When you add $AuthorImage to a template, it will run the AuthorImage() method when and only when it needs it for this particular page view.
The excellent side-effect about this templating architecture is that you can traverse the objects in your application design. If the author of the blog post is stored in a separate Author table, but is related by an AuthorID on the Blog post, you could access the Author's first name by calling BlogPost->Author->FirstName(). This is available in your template, too. If you want to access multiple items relating to the BlogPost (e.g. related posts), you can access those as you might expect, too. For instance:
My blog post is called $Title and what I have written is $Content.
I wrote it $Date.Ago. <!-- Ago() method displays date as "3 days ago" -->
Thanks, $Author.FirstName. <!-- Name stored in another class and table -->
PS. You should check out other relevant posts:
<% control RelatedPosts %> <!-- Other blog posts in a many:many relation -->
<li>$Title, written $Date.Ago</li>
<% end_control %>
Now, recall that a SilverStripe CMS-powered system is very much defined by its model. The Object-relational mapping architecture means that every piece of content - blog posts, members, pages, images uploaded in the CMS, etc - are all related to each other. Whether you're dealing with a template, or you are writing custom PHP code to power it, you are accessing the same hierarchy and set of data. If SilverStripe CMS used a conventional templating language, you would not get the memory and performance benefit, and cleanly accessing the identical data structure would be complex, slow, and fragile.
There are some downsides to this approach. Specifically, it makes it harder to abstract the view role to support other templating syntaxes and systems like Smarty. We think the gains are worth the tradeoffs, however.