A little piece of procedural code configures the chain (a PageController):
Code: Select all
include('../config.php');
include(APP_LIB . 'shared/request_gateways/EmptyRequest.php');
$application_controller =& new Cor(new EmptyRequest);
$application_controller->addHandler('path/to/class_file.php', 'ClassName');
// etc
$application_controller->execute();
There would usually be a specific Request class per PageController (each request type normally expects a different GPC parameter list). The EmptyRequest class just asserts that there should be no user supplied parameters.
Virtually every http request has a range of possible pages which might be served in response to the request. The idea here is to have one handler per page - although you could possibly cheat a bit and deal with a range of http error status codes in the final handler.
You've got the picture pretty accurately. However the first handler is normally a BadRequestSyntax class. This wants to create a 400 page and might not do much more than check $request->hasValidSyntax(). Bad request syntax might be a hacking attempt though so you could possibly also attempt to profile the attacker (as best you can..), log details, email the webmaster etc. This must come before any code which needs to access GPC: if the request is missing a required parameter, has invalid values, or alien vars, all bets are off.
Authentication might or might not have its own handler. If you want to display a login page with failed authentication it needs its own handler dedicated to creating the page. At other times, failed authentication might affect only the presentation logic ie a home page displays a login box - and so it doesn't need a handler of its own.
Same deal with authorisation: depends on the specific request and what you want to do with it.
There might also be a DbUnavailable handler if, when you can't connect, you want to display an error page (which you probably do). This would be near the start of the chain, possibly right at the start if BadRequestyntax needs to make db queries.
The rest proceeds as you have outlined, above.
That seems to me enough to make a ChainOfResponsibility very useful. It's quite a powerful pattern which makes a good fit for an application controller. Loosely-coupled, chained handlers are very flexible. You can easily pop authentication/authorisation handlers in and out for example. For those wizzard moments, throw a few more handlers into the mix.
I think a CoR framework promotes nice, cohesive classes. Each handler object is only concerned with doing its own job.
The CoR pattern is less useful if you can't identify loosely-coupled classes for the chain, or if there are very many objects in the chain in which case the "switch/case" logic gets to be inefficient.
I'd better post the class - plus test of course

Unlike the traditional GoF CoR, chained ojects don't call their successors. The key point however is the logical switch/case behaviour where only one handler is ever active.
This isn't really meant to be a finished version. I can't decide whether to just dump all that error stuff. It also needs a new name - SwitchCase is kind of confusing - might suggest alphabetical case-changing.
Code: Select all
/*
A minor variation on ChainOfResponsibility.
A chain "manager" (lazy) loads objects rather than each object loading
the next in turn. Otherwise the same logical behaviour: only one handler
is ever active, ie the first which decides it can handle the event.
*/
class SwitchCase
{
var $no_handlers_error = 'Who ate all the handlers?';
var $cannot_handle_error = 'Unable to process the event.';
var $_object_meta_data = array();
var $_error_level;
var $_handler_parameters;
/*
param (mixed) $handler_parameters - all handlers are isntantiated with the same parameters
param (mixed) $error_level - pass boolean false to turn error-triggering off
*/
function SwitchCase(&$handler_parameters, $error_level = E_USER_ERROR)
{
$this->_handler_parameters =& $handler_parameters;
$this->_error_level = $error_level;
}
/*
param (string) $class_name
param (string) $script pipe-separated list if parents to include (include order left to right)
*/
function addHandler($class_name, $script)
{
$this->_object_meta_data[] = array($class_name, $script);
}
/*
return (mixed)
*/
function execute()
{
$i = 0;
while( !is_null($handler_meta = array_shift($this->_object_meta_data))) {
$this->_handler =& $this->_Handler($handler_meta);
if($this->_handler->try()) {
return get_class($this->_handler);
}
++$i;
}
if($i == 0) {
if($this->_error_level !== false) {
trigger_error($this->no_handlers_error, $this->_error_level);
}
return null;
}
if($this->_error_level !== false) {
trigger_error($this->cannot_handle_error, $this->_error_level);
}
return false;
}
function &_Handler($handler_meta)
{
$files = explode('|', $handler_meta[1]);
foreach($files as $path) {
require_once($path);
}
$class = $handler_meta[0];
return new $class($this->_handler_parameters);
}
}
The test:
Code: Select all
require_once(APERI_LIB . 'control/SwitchCase.php');
require_once(APERI_LIB . 'filesystem/MakeBranch.php');
require_once(APERI_LIB . 'filesystem/DeleteBranch.php');
class TestOfSwitchCase extends UnitTestCase
{
var $_handler_parameters = 'foo';
function TestOfSwitchCase()
{
$this->UnitTestCase();
}
function setUp()
{
// create some handler classes with hard-coded return values
$meta = array();
$meta[] = array('truehandler_foo', true);
$meta[] = array('truehandler_bar', true);
$meta[] = array('nullhandler', null);
$this->_makeHandlers($meta);
}
function tearDown()
{
new DeleteBranch(TEST_TMP);
}
function testWithNoHandlers()
{
$cor =& new SwitchCase($this->_handler_parameters);
$this->assertIdentical($cor->execute(), null);
$this->assertError($cor->no_handlers_error);
}
function testWithNoActiveHandlers()
{
$cor =& new SwitchCase($this->_handler_parameters);
$cor->addHandler('nullhandler', TEST_TMP . 'nullhandler.php');
$this->assertIdentical($cor->execute(), false);
$this->assertError($cor->cannot_handle_error);
}
function testSingleActiveHandler()
{
$cor =& new SwitchCase($this->_handler_parameters);
$cor->addHandler('truehandler_foo', TEST_TMP . 'truehandler_foo.php');
$this->assertEqual($cor->execute(), 'truehandler_foo');
}
function testMixedChain()
{
$cor =& new SwitchCase($this->_handler_parameters);
$cor->addHandler('nullhandler', TEST_TMP . 'nullhandler.php');
$cor->addHandler('truehandler_bar', TEST_TMP . 'truehandler_bar.php');
$cor->addHandler('truehandler_foo', TEST_TMP . 'truehandler_foo.php');
$this->assertEqual($cor->execute(), 'truehandler_bar');
}
function testParameterReferenceIsPassedToHandlers()
{
$cor =& new SwitchCase($this->_handler_parameters);
$cor->addHandler('truehandler_foo', TEST_TMP . 'truehandler_foo.php');
$cor->execute();
$this->assertReference($cor->_handler->_parameters, $this->_handler_parameters);
}
function testPipeSeparatedScriptArgIncludesParentClasses()
{
$meta = array();
$meta[] = array('parent', null);
$meta[] = array('child', true, ' extends parent');
$this->_makeHandlers($meta);
$cor =& new SwitchCase($this->_handler_parameters);
$cor->addHandler('child', TEST_TMP . 'parent.php|' . TEST_TMP . 'child.php');
$this->assertEqual($cor->execute(), 'child');
}
function testWithNoHandlersAndErrorsToggledOff()
{
$cor =& new SwitchCase($this->_handler_parameters, false);
$this->assertIdentical($cor->execute(), null);
$this->assertNoErrors();
}
function testWithNoActiveHandlersAndErrorsToggledOff()
{
$cor =& new SwitchCase($this->_handler_parameters, false);
$cor->addHandler('nullhandler', TEST_TMP . 'nullhandler.php');
$this->assertIdentical($cor->execute(), false);
$this->assertNoErrors();
}
#
# file gen could possibly be made clearer..
#
function _makeHandlers($list)
{
new MakeBranch(TEST_TMP);
foreach($list as $handler_meta) {
$handle = fopen(TEST_TMP . $handler_meta[0] . '.php', 'wb');
$contents = call_user_func_array(array($this, '_handlerTemplate'), $handler_meta);
fwrite($handle, $contents);
fclose($handle);
}
}
function _handlerTemplate($name, $active, $extends = '')
{
if($active) {
$return_value = 'true';
} else {
$return_value = 'null';
}
$tpl = '';
$tpl .= "<?php\n";
$tpl .= "class " . $name . $extends . "\n";
$tpl .= "{\n";
$tpl .= "\tfunction " . $name . "(&\$parameters)\n";
$tpl .= "\t{\n";
$tpl .= "\t\t\$this->_parameters =& \$parameters;\n";
$tpl .= "\t}\n";
$tpl .= "\tfunction try()\n";
$tpl .= "\t{\n";
$tpl .= "\t\treturn " . $return_value . ";\n";
$tpl .= "\t}\n";
$tpl .= "}\n";
$tpl .= "?>\n";
return $tpl;
}
}