Last updated: 18/07/2010 4:21pm

New website!

Check out the new website at www.balbuss.com for new and updated articles on things SilverStripe.

Rules and Actions

Edited 18-07-2010
Rules and Actions are a means to disguise a querystring as a 'nice' url. So suppose you want a request to look like this:

http://www/mydomain.xxx/wines/selection/

but what you really mean is this:

http://www/mydomain.xxx/wines/?Action=selection

How do you get his to work? Suppose http://www/mydomain.xxx/wines/ is a 'real' page. Upon entering the URL above, SilverStripe will go looking for the childpage http://www/mydomain.xxx/wines/selection/ that doesn't exist. This will obviously result in a 'page not found'. So we need to tell SilverStripe how to correctly interprete this URL.

Rules

This is where rules come in. Using a rule, you can tell SilverStripe that a certain URL should be handled by a specific controller, at the same time defining some optional parameters that will be delivered in the shape of url-segments. So the following rule:

Director::addRules(100, array(  
'wines//$Action' => 'SomeWineController'
}

...will tell SilverStripe that any request for an URL like http://www/mydomain.xx/wines/selection/  should always be handled by the controller SomeWineController. This controller then has access to an 'Action' parameter with (in our case) the value 'selection', that can be retrieved within the controller using $this->urlParams['Action'].

Pages and rules

The following rule is default in SilverStripe - it has a 'lowish' priority 10 in a scale from 0 to 100 (find it in sapphire/_config.php):

Director::addRules(10, array( 
...
'$Controller//$Action/$ID/$OtherID' => '*',
...
));

It basically tells SilverStripe that any URL of the form www.mydomain.xxx/somecontroller/param1/param2/param3/ should be handed over to somecontroller, whereby the latter 3 folders are in fact (optional) parameters Action, ID and OtherID to the request.

So in fact the URL http://www/mydomain.nl/wines/selection/ should work out of the box - we don't need to write any special rule for it! Only - it doesn't right away: There's more to be done!

Note: there is something a bit confusing about this: in the URL above, 'whine' is the page's URL segment, not the name of a controller. But SilverStripe will interprete it as a reference to the Page_Controller, and hand things over to it. You could even try http://www/mydomain.nl/Page_Controller/ as an URL. This will also be handed over to the page_Controller and chances are you'll get a page without any page-specific content... 

Action

The first param isn't called $Action for nothing - it has a special meaning in SilverStripe. The parameter Action=selection will hand over all controll to a method named selection. So now you just have to create this 'selection()' on the Page_Controller, and all calls to http://www/mydomain.xxx/wines/selection/ will be automatically handled by this method. 

Each param from the URL can be accessed from the Page controller as:

  • $this->urlParams['Action']
  • $this->urlParams['ID']
  • $this->urlParams['OtherID']

Of course you can put any kind of string into a parameter, so ID doesn't necessarily have to be an ID... But it would make code much more readable if the parameters could have some more self-explanatory name. That's where creating your own rules becomes interesting. Copy the default rule above to mysite/_config, change the parameter names, and give it a priority higher then 10.

Director::addRules(20, array(  
'wines//$Action/$Color/$Country/$Region' => 'SomeWineController'
}

Watch out: do not add a trailing slash to the URL part of the rule: it might make the entire rule not work *)

You then need the new method to return some rendered results or you'll end up with a white screen... So either use it to redirect to some other page - or make it render the page itself. Something like:

function selection()
...
return $this->return $this->renderWith(array('myWinePage','Page'));
}

So now I can have the following types of url, where red(), white() and bubbly() are methods in the controller:

http://www.mydomain.xxx/wine/red/
http://www.mydomain.xxx/wine/white/
http://www.mydomain.xxx/wine/bubbly/

Default Action: index()

If you call the page by its 'real URL' without specifying an Action, the page will do what it would normally do. You can however influence this behaviour by using the default index() method: when no Action is specified, index() is always called - if it exists, that is.

Caution: don't mix up init() and index(). Init() will always be called before any action that renders the page. Index() is in fact just another action. So if you use index, make sure it renders some pagecontent for you as well...

Security: restricting actions

To prevent people from adding all kinds of 'foldernames' and calling methods you don't want called from the URL, there is the $allowed_actions static variable, that lets you restrict possible actions to just the ones you want. Al other attempts will just result in a 'page-not-found' warning. So add this to the designated controller class:

static $allowed_actions = array(
'selection',
...
); 

Be careful: when actions are added to the $allowed_actions array while no method (yet) exists for them, they will no longer trigger the page-not-found reaction. Adding such an action to the URL will also prevent index() from being triggered, so the page will be displayed 'as is'.

Security: sanitizing user input

All params are ultimately user input and need to be sanitized. So, for example, if you'd want only characters, digits, -, _ and spaces, you could do something like the following. Urlencoded params are, it seems, already decoded when they appear in the urlParams array, so the only thing left to do is strip unwanted characters:

$value = preg_replace('/[^a-zA-Z0-9\-_ ]*/', '', $this->urlParams['ID']);

URL encoding links (menu-items)

Links that are dynamically generated from within the website, need to be urlencoded, since they might contain spaces and other illegal stuff. Be sure to use urlencode().

Conflicting pagenames

What if a page named selection already exists? Once you create the selection() method in the wine page - or even add it to the $allowed_actions array - SilverStripe will prefer it above the real page. And if you try to create a page named 'selection' afterwards, when a method of that name already exists, SilverStripe will name its URLSegment 'selection-2'! So there is that protection.

Dynamic actions

In the example above, you need to either have 'wine' be a Page on which some actions red(), white() and bubbly() are defined, or have 'wine' be an action on yet another, and red, white and bubbly be params. Unfortunately you cannot have SilverStripe generate action methods on the fly, just because you want to add a new action to the URL.  So to generate winetypes dynamically, you need that extra urlsegment. 

So I thought of a very simple workaround, that would probably make some people nauseous, but I won't be responsible for any (security) issues that might arise from it :-) What I wanted to do is have the following URLs, where I can add new winetypes easily, without having to construct methods for them:

http://www.mydomain.xxx/wine/red/
http://www.mydomain.xxx/wine/white/
http://www.mydomain.xxx/wine/bubbly/

First of all: it cannot be completely dynamic unless you omit the $allowed_actions array - and you just need that bit of extra security! But you might just as well add it to your _config.php file, making it more accessible:

mysite/_config.php

  ProductPage_Controller::$allowed_actions = array(
'red', 'white', 'bubbly'
);

Next: all actions are handled by the Controllers (yep) handleAction() method, which you can override in your custom controller. You can now check if the action is part of the allowed_actions, and if so, do something with it. In this case, I have a simple 'showSelection() method that accepts any wine type, and returns the resulting selection:

/mysite/ProductPage.php

class SomeWineController extends Controller {
...


public function handleAction($request) {

// get the matching params in the rule that is used
$params = $request->latestParams();

$action = $params['Action'];
if (in_array($action, $this->stat('allowed_actions'))) {
return $this->showSelection($action);
}
else {
// default action handling
return parent::handleAction($request);
}
}



function showSelection($wineType) {
// render the page based on the type of product
}
..
}

Now the URL's above will all call the same action showSelection(...) on wine controller, and if I need to add a new variety I just add it to the allowed_actions, and away it goes. Works! :-)

*) Beware!

Do not add a trailing slash to the rule's URL part. SilverStripe will see this as an extra (empty!) parameter, and it will try to match it to the URL that is being parsed. The way it tries to do that (in HTTPRequest::match()) will always fail, and therefor the rule will never apply. This looks like - if not a bug - a design flaw, since that typo is so easily made...

 

10 Most recently updated pages

Post your comment

Comments

  • Thanks - this is brilliant!

    Posted by Ed Gossage, 23/11/2011 5:30pm (2 years ago)

  • Thank you, a wonderful and concise article.

    Posted by John Black, 15/11/2011 6:24pm (2 years ago)

  • That's correct - you cannot use a slash in the URL field (MetaData tab), In SS version 2.4.x however, nested url's are supported, where the url you mention will be created automatically if 'selection' is a child page of 'wines'.

    Posted by Martine, 21/06/2011 6:53pm (3 years ago)

  • Is it possible to have pages of type http://www/mydomain.xxx/wines/selection/which content is managed from the CMS? The CMS does not allow urls to be set this way, it replaces / with - in the url...

    Posted by Yasen, 21/06/2011 9:34am (3 years ago)

RSS feed for comments on this page | RSS feed for all comments