DataObjects as Pages - Part 1: Keeping it Simple

In this two part tutorial, I am going to show you how to display DataObjects as if they were pages. This is often very useful for times when you have lots of items that don't warrant full site tree objects, but do need to be displayed on their own on the site. In this first part displaying Staff Members we are going to keep it simple, using a Data Object Manager to manage our Staff Members and referring to their ID in the URL. Later in part 2, we will see how to use ModelAdmin to manage a Search Engine friendly product catalogue.

Note. We will be using the DataObjectManager module in this tutorial, which you should install if you don't already have it. You can also use a ComplextTableField in place of the DataObjectManger if you prefer not to use any external modules.

Preperation

We will be creating 4 files in this tutorial, StaffMember.php, StaffPage.php, StaffPage.ss and StaffPage_show.ss. Lets create these files and their initial code (you can also get them from the STARTING_FILES folder in the SourceFiles Zip attached to this page.

The StaffMember DataObject

Now that we have our basic staff member DataObject, lets's add some meat to it. A few db fields & relations, the summary_fields static, CMS fields for the popup and a function to generate our thumbnail in the DataObjectManger (see this post for more info on DOM/CTF thumbnail generation).

It's all pretty standard here, the only things to note are that we are adding a has_one relation to the StaffPage page type and that we define $summary_fields and use the function name getCMSFields() (not getCMSFields_forPopup()). This is so that when we define our DOM in the next step, we don't need to supply these as arguments as DOM knows to look for these automatically.

The StaffPage Page Type

Now that we have our DataObject, we need a page that allows us to create them. We will be doing that through the StaffPage which will have a has_many relation to StaffMember. Remember that StaffMember has a has_one relation to StaffPage, Meaning that each StaffMember can only be accociated to a single StaffPage while each StaffPage can have lots of StaffMembers accociated with it. If this is confusing don't worry, you can leave understanding relations to another day :)

Again, this stuff is pretty standard. Notice that our DOM definition on line 13-17 has far fewer arguments than many examples you will see. That is because as mentioned we used $summary_fields and getCMSFields() in our StaffMember DataObject so the DOM will pick these up automatically.

Here are some pics of what your StaffPage should look like in the CMS:

Now that we can create StaffMember objects, lets get a basic template together to display them in a list.

You may have noticed something strange here. As we loop through the <% control StaffMembers %> we are using $Link as if each individual StaffMember were a page. Of course this is works fine when using SiteTree objects, as their links are generated automatically, however our StaffMembers are DataObjects and so don't have a URL at all and we don't yet have a way of displaying each staff member on it's own, nevermind linking to it! We will be adding a way to generate the correct link in a bit, but before I confuse the matter too much, let's create a way to view our StaffMembers as actual pages.

The StaffMember Detail Page

Here is where things start to get interesting. Up to now we have been doing pretty standard stuff, but now we are going to use SilverStripe to create 'virtual' pages on the front end of our site for each StaffMember. To do this we are going to use URL paramaters. These are values passed to the Controller as an array via the URL. They are stuctured /action/ID/OtherID so the URL www.mysite.com/somepage/somefunction/value/anothervalue would create the following array: ('Action' => 'somefunction', 'ID' => 'value', 'OtherID' => 'anothervalue'). It would also try to run the function somefunction() if it existed in the controller and had been added to the $allowed_actions static array. Although not a requirement, it is good practice to use the $allowed_actions to prevent unauthorized function calls.

So in our particluar example the URL will look something like this www.mysite.com/staff/show/23. where show is our action which calls the show() function on our pages controller and 23 is the ID of the StaffMember object we want to view. This is of course not Search Engine friendly but it is quicker and simpler than using a URL segment. We will be delving into a more complex example using proper URL segments in part 2.

Now there's quite a lot to take in here so let's go through it carefully. First thing we do is define our show function as allowed in the $allowed_actions static array so that SilverStripe lets us use it to run the show() function. Then we create a function getStaffMember() to fetch the current StaffMember object from theID in the URL, if there is one. So the first thing we do here is fetch all of the URL parameters. These will be returned as an associative array of 2 items (with keys of 'Action' and 'ID') which is fetched by calling $this->getURLParams(). Of these 2 values we actually only need the ID which represents the ID of the StaffMember we want to display, as the action has already been used to call the function we are in. Next we check whether the ID is numeric and if it is we get the StaffMember using a DataObject::get_by_id(). We check that the value is numeric to prevent SS erroring by passing a non-numeric value to the get_by_id() function. Generally it is a really good idea to do checks like this (and/or use Convert::raw2sql($Value)) anyway to ensure you are passing in a sanitized value just to be on the safe side (see this post for more info on sanitization).

Now we create the actual show() function which will display our StaffMember DataObject as a page. We first test to see if there is a StaffMember to show by trying to assing the result of our getStaffMember() function to $StaffMember. If this fails then we pass the user to a 404 page, otherwise we then define an array with our $StaffMember object in it. This array is then returned using the customise(). This is really the heart of this whole process. What we are doing here is saying when you render the page again, include our custom $Data in the template. Although we are only defining a single value here, you can add as many as you like, for example setting 'Title' or 'Content' to something other than that in StaffPage, which is effectively still the page we are on.

Note. We could have easily combined the getStaffMember() function into the show() function, but as you will see later it is very useful to be able to fetch the current StaffMember for use in other functions, in this instance we will be using it in our custom Breadcrumbs trail.

The clever bit in all this comes with the temeplate that SilverStripe uses. Because we have called a URL action SilverStripe knows to look for a template that follows the convention PageType_action. In this case our page type is StaffPage and our action is show, so SilverStripe will look for the template StaffPage_show.ss, which should now look like this:

As you can see, we use <% control StaffMember %> to access the 'StaffMember' value from our $Data array. Once inside StaffMember we have access to all the attributes of that Object.

So now we have a page that displays a list of our StaffMembers and we have a way to access each StaffMember on their own page using the URL www.mysite.com/{staffpage}/show/{ID}. What we now need to do is go back to our StaffMember and create a function Link() which generates a Link to that StaffMembers corresponding detail page so that our StaffPage.ss template starts correctly linking to them.

All we do here is grab the related StaffPage then construct the link by joining the StaffPage's Link to 'show/' and finally the ID of the current StaffMember. So now when $Link is called from within the <% control staffMembers %> loop it will correctly output the link to that StaffMember! Your page will look something like this (with a bit of extra styling, included in the source files):

The Finishing Touches : Menus and BreadCrumbs

At this point you have a way to display DataObjects as pages and link to them. However there are a couple of things that are missing that make them less useful, namely the ability to include our DataObjects in the SideMenu as well as being able to correctly output breadcrumbs which include our StaffMembers.

Sidbear Menu

So firstly lets make it so that our DataObjects show up in the Sidebar menu of the Blackcandy theme. This is actually easier than you would imagine. All you need to think about is what the Sidebar menu is doing: 1st it grabs the children of the current page by calling Children(), then it lists these children by referring to each of their $MenuTitle's and $Links'. We already have a $Link function on our StaffMember, so to allow our them to be shown like this we simply have to tell the StaffPage to return all of it's StaffMembers when SilverStripe asks it for its children() and then we need to ensure that our StaffMember returns something useful when SilverStripe asks it for it's $MenuTitle. Here's what we do:

Inside the StaffPage class in StaffPage.php add the following function

This function simply returns all of the StaffMembers attached to that StaffPage when SilverStripe asks for it's children. We also add in an onBeforeDelete() function. This is to prevent Silvertripe deleting the attached StaffMembers when we unpublish the page which would normally happen because it expects the objects returned in Children() to be SiteTree obejcts and therefore should be removed from the Live site too. The difference here is that our StaffMember objects are not versioned, so it ends up deleting them completely!

Then we just add these two functions to our StaffMember class in StaffMember.php:

class StaffMember extends DataObject
{
.
.
.
//Return the Name as a menu title
public function MenuTitle()
{
return $this->Name;
}
//Ensure that the DO shows up in menu (unclear whether this is needed or not)
function canView()
{
return $this->StaffPage()->canView();
}
}

The first function MenuTitle() just returns the StaffMembers name when SilverStripe asks for it's $MenuTitle.The second function canView() is to fix an issue some people were having whereby non logged in users could not see the items in the menu. All it does is return true or false based on the parent StaffPage() access permissions, so if the StaffPage() is allowed to be viewed by anyone, so will this StaffMember object. This is no necessarily needed, but fixes the issue if you are having it. That's it! Your StaffMembers will now be listed and linked from your Sidebar menu. However there is one little detail still missing: the current page is not highlighted. To do this we just need to add a function to our StaffMember class which returns 'current' if we are viewing it. Here is what it would look like, placed in the StaffMember class inside StaffMember.php

class StaffMember extends DataObject
{
.
.
.
public function LinkingMode()
{
//Check that we have a controller to work with and that it is a StaffPage
if(Controller::CurrentPage() && Controller::CurrentPage()->ClassName == 'StaffPage')
{
//check that the action is 'show' and that we have a StaffMember to work with
if(Controller::CurrentPage()->getAction() == 'show' && $StaffMember = Controller::CurrentPage()->getStaffMember())
{
//If the current StaffMember is the same as this return 'current' class
return ($StaffMember->ID == $this->ID) ? 'current' : 'link';
}
}
}
}

So what we are doing here is first checking that we have a page controller to work with and that it belongs to a StaffPage class. Then we check that there is an action 'show' being called on that page using the function getAction() and that we can assign the current StaffMember. It's important to check these as only when these conditions are all true do we actually want to perform the comparison in the next line. This comparison returns 'current' if the currently viewing StaffMember (assigned to $StaffMember) is the same as this staff member (i.e. the instance of StaffMember that the function is currently running from).

Note. Strangely it doesn't seem possible to assign Controller::CurrentPage() to a variable like $Controller, which is why we are calling it so many times.

Breadcrumbs

Finally we are going to ensure that when the breadcrumbs are drawn on our StaffPage_show, they correctly include the StaffMember and link to the StaffPage. Add this function to your StaffPage class in StaffPage.php:

This function looks quite complex but is actually pretty strait forward. We grab the default Breadcrumbs, then if our getStaffMember() function returns a StaffMember (i.e. we are on a detail page) we break it up into an array of each section, make the last item into a link to the main StaffPage (it was just a string previously as SilverStripe thought that was the page we were on) then finally add another item onto the array with the value of the staff members name. Then we implode it all using the Breadcumb delimiter (&raquo; by default) and return it to the template. The clever thing about this function is that if we are simply just on the StaffPage rather than the detail page, then it just returns the default Breadcrumbs. However because these Breadcrumbs are being rendered from the main StaffPage, if we are on Level 1, then they won't show up due to the <% if Level(2) %> in the Breadcrumbs.ss include, so we need to insert the code directly into our StaffPage_show.ss template like so:

Just wanted to note that $allowed_actions isn't required as far as I know even in 2.4. I've got a couple of sites running on 2.4 and I've never used $allowed_actions myself and the methods are still reachable through the URL.

It's a great practice to use it though and I think it should be required. This would make it alot safer to put getters in the controller and know that noone can visit the URL by mistake.

The reason I ask, is the above method strikes me as a much better way of handling news articles for the News section of a website, as it is much easier to view and manage the articles in a DataObjectManager/ComplexTableField than as a long list of page titles in the site tree.

In part 2 of this tutorial, we will be using ModelAdmin and proper URLSegments to build a product catalogue, but this would be a far better way to implement a news section that this.

However if you did want to add an HTML field to a DO you can use the SimpleTinyMCEField that comes with the DataObjectManager module. It is specifically made to run in the DOM popup and works really well.

One question. In Admin mode a new Tab has been added with the label "Staff Members" (note the space withing the 2 words). But I don't see the label definition in the code? Does SS create a tab label on the fly using CamelCase?

vromepiet05/10/2010 7:29pm (4 years ago)

Thanks Aram for this great article. Is surely helps me into better understanding how to develop with Silverstripe (I'm still quite new to it). I'm already looking forward to part 2.

Thank you for quick answer.. I have a new question...
In Show action a sidebar widget is included and a list of children is shown. But children are shown only when I'm logged in. An anonymous user cannot see on the Sidebar a list of other staff members. Maybe we need to activate any security flag or permissions?

It looks as though you can only have a limited number of URL params? Is this correct?

http://domain.com/staff/show/ID/OtherID to add in additional parameters I would need to use a GET request string...

i.e.
Director::urlParam('ID')
Director::urlParam('OtherID')

Can you confirm this or is it just my bad coding that is causing this to fail...

What I need is:
http://domain.com/staff/show/ID1/ID2/ID3, but it would be good to be able to go to a decent level like 5 or 6. Sometime you want to pass information and other times you want to add elements for URL friendliness...

@Aram/Dario - That's really weird, I've never had to define a canView() method just to make DataObjects visible for non logged in users. I can still see the staff listing when using the old code without the canView(), what SS versions are you guys running?

@Dario, Aram - It's even more bizzarre that I'm running 2.4.2 (the same as Dario) and not experiencing the issue. I'll download 2.4.1 and see if I can replicate this in some way. If it's a bug it would be nice if we could track it down and get it squished.

@Mr Squatch - Thanks for catching that, I have moved it back to the controller. The reason this got half changed was that to be completely 'correct' the Breadcrumbs function should actually be in the Model, as we are overriding the same function in the SiteTree class. However, we then lose access to our getStaffMember() function, which requires URL paramaters, not accessible from the model.

So inorder to make it all work, it is back in the controller, but I am going to investigate how to do this to ensure cpnsistency with core classes. To be honest I am not entirely sure why it is in the model, as in my view it as a controller function.....

@ Dendeff - Yea that looks like a DOM error, probably caused by 2.4.3, I know there have been problems recently. I suspect UC will fix it shortly. Otherwise try it in 2.4.2 and see if you still get the error?

Aram

Nicolas21/01/2011 7:52am (4 years ago)

Thank you very much for this well written, educative tutorial with a real-life purpose. If part 2 is as good as this one, it'll answer the questions I actually came here for in an instant :-)

Small detail: In the Link()-function on the StaffMember Class I had to append a slash to 'show' for the link to work.

Hi Aram, thanks again for your nice tutorials. I'm a noob at the whole silverstripe-ajax-thingy. (Yes, I#ve read the ajax-recipe) So i thougt i learn this topic by 'playing' with this tutorial-code.My Goal is to show the StaffMember in a ajaxpopup instead to show a virtual page. It is'nt that heard to use a lightboxclone. But i wonder, where to put the Director::is_ajax-code. Or better which part of your code is need to be changed. I guess it is the function show() in StaffPage.php, right? Can you give me some guidance, please. Thanks pipifix

Hi Aram, its me again. It seems im a total beginner. I just simply want to sort the staff by the names of the staffmembers. Like my question above i dont know, where to put: return $StaffMembers->sort('Title',ASC);.
In your tutorial the Staffmembers are rendern in order of creation (ID).
Thanks again. Thomas

If you want a custom order for your staffmembers, you need to write your own function to return them in that order. So if you look at the code under 'The StaffPage listing Template' you'll see we simply <% control StaffMembers %> to loop over theStaffMembers attached to the page.

So instead change this to <% control OrderedStaffMembers %> and then add a function like this to your StaffPage_Controller:

the only reason you can't do that in the template (<% control StaffMembers(Null, 'FirstName ASC') %> is because the parser is a little limited. This will be improved for SS3 so hopefully this sort of workaround won't be needed.

Unfortunately I don't have a lot of time right now to look into the other question, but I know that in the past I would either use a hidden div and load that into the modal window or just load a page into the modal window with a stripped out template (e.g. http://www.severndeanery.nhs.uk/contacts/staff-contacts/). I'll let you know if I get some time to have a look :)

Cool trick, if you don't want the /show/{number_here}, add the following lines to your page-controller:
[code]
public static $url_handlers = array(
'$ID!' => 'show',
);
[/code]
This way, when you go to news/2, Silverstripe processes it as going to news/show/2

Cleans up your URL.

What I'd like to see is the slugged-URL. Have you got anything for that too?

Ah, nevermind, I had a control Children in the menu... that kinda did it...

ModernDegree09/04/2011 3:10am (4 years ago)

Is it possible to use [code]static $permissions = array( 'view' );[/code] instead of the parent's canView function? I've it in the past to solve the guest user read issue.

ModernDegree09/04/2011 3:10am (4 years ago)

Great post, btw

Gordon Anderson18/04/2011 5:30am (4 years ago)

Thanks for all the hints. However I am stuck on a couple of things:

1) I am trying to gracefully degrade the imagegallery plugin by unclecheese to be non JS friendly. I now have an ImageGalleryPage showing ImageGalleryAlbums correctly in the menu (ImageGalleryAlbums being DataObjects, ImageGalleryPage extending Page). However when viewing a single phone (class is ImageGalleryItem) the menu disappears, I assume due to the hierarchy being DataObject -> DataObject instead of Page -> DataObject.

2) I've had to revert to URLs for the form /images/35 for the mean time just to get things working. Is it possible to keep the URL structure intact, e.g. /galleries/gordons-travels/bangkok/image/35 instead of /image/35 ?

Is there a way to make the fields only display when there is information in there?

I have adapted the code to make more fields, I have fields to enter peoples facebook, twitter, website url etc and want it to be displayed on the site using icons. At the moment, the icons are still displayed, even when there is no links specified..

Hi,
First of all thanks for your contribution, it is really important resource for me, I use this for almost my website especially charity organisation. With this tutorial I would like to know is there any possible way to hide the StaffMemer name from Menu title?

Hi Edward, yes it's just a case of not having extended the functionality to support the Site map. It's not too much work to do, have a look in the googlesitemaps module to see how to do it, basically you will need to extend that to add the DO to the results and adapt the xml template accordingly.

wow.. you must be online as much as me. Thanks for the info and speedy reply! :))

kBits25/08/2011 5:29am (4 years ago)

Hi Aram,

This is super cool thanks for the tutorial! I am using this code to create a wallpaper gallery to offer downloads to my visitors. I ran into a problem however: if I 'unpublish' the main page from the site, I lose all the data I uploaded to the page. So I tried with the demo and same thing, if you publish the page you lose all the Staff Members. Any ideas on what to do about this? Thanks!

A question though, how does the StaffMember get associated with its StaffPage?
I'm following the example closely, but my StaffMember's associated StaffPageID always comes up 0 since it never gets written, and then the Link() function comes up with an uninitialized page with no URLSegment.

What you need to do is put the form function on the CategoryPage_Controller and then pass in the ID of the DataObject you are on in a hidden field in the form itself. So you effectively call the form on the parent page, but you can work out which DO you were on in the submission controller by grabbing the value from the hidden field.

It worked !
I had to add "ContactForm" to the allowed actions as well, otherwise I ended on the 404.

Thank you very much, again :) !

Vincent_vega23/01/2012 9:01am (3 years ago)

I've created a page that lists all the staff etc on one page (based on this tutorial).

The staff page is all good but i would like each staff member to be listed under their role, basically like how Silverstripe have done it with their team page http://www.silverstripe.com/about-us/team/

So CEO, Development, Design etc etc

Can someone point me in the right direction on what documentation i have to read up on?

i simple want to provide a contactform on the staffdetailspage (staff_show.ss). but i dont get a clue how to stick togehter the userform/form and this fine piece of code. I guess i need to build the form in the StaffMember.php and not in the StaffPage.php. Or am i wrong? And how to pass the mailvariable to the form?

Can you give me a tip or point to a tutorial/snippet, please. Thanks a lot.

Hi Thomas, that is going to be pretty tricky to achieve. The best way to do this would be to build the form in the Controller rather than trying to use the UserForms module. You will need to create a hidden field with the ID of the staff member you are currently viewing so that when you process the form submission you know who to send it to.

Have a look at the Contact Form Tutorial to get you started: http://www.ssbits.com/newbies/2010/creating-a-simple-contact-form/

Elie Andraos10/04/2012 9:50am (3 years ago)

Hi Aram,
What if the staffmember should have many images ? like mulitple uploads, what should i do ?

Any tips on getting this to work with many_many files?
I'm using Uncle Cheese's KickAssets module which makes adding multiple files (photos) to each page in the CMS nice and easy... but I'd like to be able to show these on the front end on individual page if clicked. eg. /show/ID

But can't get this to work and I think it's because my dataobjects (Files) are many_many instead of has_many.

Ok ignore the above - I have this working using Files as pages instead of DataObjects (because I'm using Uncle Cheese's KickAssets) - the details are on this post:
http://www.silverstripe.org/general-questions/show/19748

Along with a couple of questions that I think experienced Silverstripers might be able to help me with.

Dredd10/05/2012 12:24pm (3 years ago)

You can move Breadcrumbs() into model if you replace $this->getStaffMember() with $this->CurrentPage()->getStaffMember() .