SilverStripe – Making work extremely easy for your content editors

Fell deeds awaken...

The life of a programmer can be hard, but can be made much less laborious, at least the labor of minutia, with well designed frameworks like the one I am showcasing here: SilverStripe. The following is the main nuts and bolts of the programming to create a page type. It defines the schema, its restrictions, helper functions, the data entry forms where the content editors are supposed to put their value types, and if all is done to spec, all you need is a well formed template and some CSS to prettify it and you not only have a webpage. You have a webpage that the content editors can add to again and again with out ever having to call the web developer each time they need to add another page. While that may seem bad on the developer’s income, in reality, the happiness of the company will pay for itself and the lack of headaches the developer gets to enjoy for not being called out to create a mindless HTML page are more than enough reason. For those of you who can’t read the programming and the entire point of the article, here is the TL;DR, finished product:

Here then is the nuts and bolts of the programming of an engineering project page. I’m just going to show you and let you read through yourself. If you want to get into how the particulars work you will have to head on over to SilverStripe yourself.

 * Defines the ProjectPage page type
class ProjectPage extends Page {
    public static $db = array(
		'Location'	=> 'Varchar',
		'Architect'	=> 'Varchar',
		'FootageLabel1'	=> 'Varchar',
		'FootageAmt1'	=> 'Decimal',
		'FootageLabel2'	=> 'Varchar',
		'FootageAmt2'	=> 'Decimal',
		'FootageLabel3'	=> 'Varchar',
		'FootageAmt3'	=> 'Decimal',
		'EstimatedCost'	=> 'Currency(30)',
		'OptionLabel'	=> 'Varchar',
		'OptionValue'	=> 'Varchar',
		'Materials'	=> 'HTMLText',
		'Highlights'	=> 'HTMLText',
		'OptionalTextLabel' => 'Varchar',
		'OptionalText'	=> 'HTMLText'

    public static $has_one = array(
		'Photo1'		=> 'Image',
		'Photo2'		=> 'Image',
		'Photo3'		=> 'Image',
		'Photo4'		=> 'Image',
		'Photo5'		=> 'Image',
		'Photo6'		=> 'Image',
		'Photo7'		=> 'Image',
		'Photo8'		=> 'Image',
		'Photo9'		=> 'Image'

	//static $icon = "framework/docs/en/tutorials/_images/treeicons/news-file.gif";
	public function getCMSFields() 
	    $fields = parent::getCMSFields();

	    $fields->addFieldToTab('Root.Main', $locationField = new TextField('Location','Location of Project'));
		//$locationField->setConfig('showcalendar', true);

	    $fields->addFieldToTab('Root.Main', new TextField('Architect','Architect Name'));
		$fields->addFieldToTab('Root.Main', new TextField('FootageLabel1','Square Footage Label 1'));
		$fields->addFieldToTab('Root.Main', new NumericField('FootageAmt1','Square Footage Amount 1'));
		$fields->addFieldToTab('Root.Main', new TextField('FootageLabel2','Square Footage Label 2'));
		$fields->addFieldToTab('Root.Main', new NumericField('FootageAmt2','Square Footage Amount 2'));
		$fields->addFieldToTab('Root.Main', new TextField('FootageLabel3','Square Footage Label 3'));
		$fields->addFieldToTab('Root.Main', new NumericField('FootageAmt3','Square Footage Amount 3'));
		$fields->addFieldToTab('Root.Main', new CurrencyField('EstimatedCost','Estimated Cost'));
		$fields->addFieldToTab('Root.Main', new TextField('OptionLabel','Option Label'));
		$fields->addFieldToTab('Root.Main', new TextField('OptionValue','Option Value'));
		$fields->addFieldToTab('Root.Main', new HTMLEditorField('Materials','Materials Used'));
		$fields->addFieldToTab('Root.Main', new HTMLEditorField('Highlights','Project Highlights'));
		$fields->addFieldToTab('Root.Main', new TextField('OptionalTextLabel','Optional Text Label'));
		$fields->addFieldToTab('Root.Main', new HTMLEditorField('OptionalText','Optional Text'));
		$fields->addFieldToTab("Root.Images", new UploadField('Photo1', 'First Photo'));
		$fields->addFieldToTab("Root.Images", new UploadField('Photo2', 'Second Photo'));
		$fields->addFieldToTab("Root.Images", new UploadField('Photo3', 'Third Photo'));
		$fields->addFieldToTab("Root.Images", new UploadField('Photo4', 'Fourth Photo'));
		$fields->addFieldToTab("Root.Images", new UploadField('Photo5', 'Fifth Photo'));
		$fields->addFieldToTab("Root.Images", new UploadField('Photo6', 'Sixth Photo'));
		$fields->addFieldToTab("Root.Images", new UploadField('Photo7', 'Seventh Photo'));
		$fields->addFieldToTab("Root.Images", new UploadField('Photo8', 'Eighth Photo'));
		$fields->addFieldToTab("Root.Images", new UploadField('Photo9', 'Ninth Photo'));

	    return $fields;
	function teaserHighlights(){
		return strip_tags($this->Highlights);
	public function validFootageTotal(){
		$total = $this->getFootageTotal();
		return $total>0 && ($this->FootageAmt2>0 || $this->FootageAmt3>0) ? true : false;
	public function getFootageTotal(){
		$FootageTotal = $this->FootageAmt1 ? $this->FootageAmt1 : 0;
		$FootageTotal += $this->FootageAmt2 ? $this->FootageAmt2 : 0;
		$FootageTotal += $this->FootageAmt3 ? $this->FootageAmt3 : 0;
		return number_format($FootageTotal);
	public function validFootageAmt($num){
		$valid = false;
			case 1:
				if($this->FootageAmt1 > 0) $valid = true;
			case 2:
				if($this->FootageAmt2 > 0) $valid = true;
			case 3:
				if($this->FootageAmt3 > 0) $valid = true;
		return $valid;
	public function outputMarginNumber(){
		$cnt = 0;
		if($this->FootageAmt1 > 0 || $this->FootageAmt2 > 0 || $this->FootageAmt3 > 0){
		if($this->FootageAmt1 > 0) $cnt++;
		if($this->FootageAmt2 > 0) $cnt++;
		if($this->FootageAmt3 > 0) $cnt++;
		if($this->OptionLabel) $cnt++;
		if($this->OptionValue) $cnt++;
		return $cnt;
	public function outputFootageAmt($num){
		$output = 0;
			case 1:
				$output = number_format($this->FootageAmt1);
			case 2:
				$output = number_format($this->FootageAmt2);
			case 3:
				$output = number_format($this->FootageAmt3);
		return $output;
	public function validEstimatedCost(){
		$ec = $this->EstimatedCost;
		return $ec > 0 ? true : false;
	public function outputEstimatedCost(){
		$ec = $this->EstimatedCost;
		$ec = str_replace('$','',$ec);
		$ec = preg_replace("/\.00$/","",$ec);
		return '$'.number_format($ec);
	function backLink(){
		$backLink = preg_replace('/[^\/]+\/$/','',$_SERVER["REQUEST_URI"]);
		return '« Back to '.ucwords(str_replace('-',' ',$m[1])).'';
class ProjectPage_Controller extends Page_Controller {


And because of the previous programming this data entry is all the content editors need to do to create a lovely Portfolio Project page. Head on over to if you don’t believe me.

Data Entry is all it takes

Data Entry is all it takes

Of course you will need a template to place all that data in, and fortunately there is still a lot of logic available to you to account for missing pieces of data of multiple pieces of data, basically all the exceptions one could think of. There is an include feature at the end that allows you to include sub-templates, which come in very handy for, in this case, a secondary menu that is used all over the place. I ended up using this same framework to create the mobile version of the site too and it’s working beautifully.




<% if OptionLabel && OptionValue %>


<% end_if %> <% if validFootageAmt(1) || validFootageAmt(2) || validFootageAmt(3) %>

<% if FootageLabel1 %> <% end_if %> <% if validFootageAmt(1) %>


<% end_if %> <% if FootageLabel2 %> <% end_if %> <% if validFootageAmt(2) %>


<% end_if %> <% if FootageLabel3 %> <% end_if %> <% if validFootageAmt(3) %>


<% end_if %> <% if validFootageTotal() %>


<% end_if %>
<% end_if %> <% if validEstimatedCost() %>


<% end_if %>

<% if Materials %>


<% end_if %> <% if Highlights %>


<% end_if %> <% if OptionalText %>


<% end_if %>

<% include SideBar %>

And now, as promised, is the sub-template. Notice the loop logic for building out the navigation menu (and HTML5 for the attentive). There is so much to this framework I still find new things in it. Of course, part of that has to do with the upgrade that came out a few months ago allowing me to do this mobile job in SilverStripe in the first place.

Just look how it comes together with a little help from a JS plugin. And did I mention that it resizes images automatically as it needs to. It keeps dozens of copies of the same image for whatever it needs. Pretty impressive. Just make sure disk space isn’t a worry. Here is the final page, sharp, clean, and it can be put together by any office-worker than can do data-entry and image-uploads.