Hi...
Methinks people's confidence in me is about to take a bit of a pasting
. What follows is anything but magic. It is brute force eval() and doesn't entirely work anyway.
The idea is that decorators can be generated from the reflection API. This is rather limited in PHP4 and I don't yet have a working PHP5 box so I am rather restricted here. As a result parameters are not passed by reference without some extra work.
Usage is...
PHP Code:
Decorator::generate('MyClass');
class MySpecificDecorator extends MyClassDecorator {
function MySpecificDecorator(&$wrapped) {
$this->MyClassDecorator($wrapped);
}
function theOnlyOneToChange($a) {
...
$result = &parent::theOnlyOneToChange($a);
...
return $result;
}
}
The idea is that you only have to override the methods you want to change, the other methods are chained automatically by the do-nothing generated decorator. You have to also explicitely chain those where you must pass references, but for simple classes it could ease the drudge.
To fix the reference issue in the generator, the only thing needed is the parameter count of the method. In PHP4 this means creating some kind of reflection API by parsing the PHP through the tokeniser. If the code is generated from some kind of make file, this might not be so bad.
Anyway, the code...
PHP Code:
class Decorator {
function generate($class, $decorator = false) {
if (! $decorator) {
$decorator = $class . 'Decorator';
}
if (class_exists($decorator)) {
return false;
}
eval(Decorator::generateCode($class, $decorator));
return true;
}
function generateCode($class, $decorator) {
$code = "class $decorator {\n";
$code .= " function $decorator(&\$wrapped) {\n";
$code .= " \$this->_wrapped = &\$wrapped;\n";
$code .= " }\n";
foreach (get_class_methods($class) as $method) {
$code .= " function &$method() {\n";
$code .= " \$parameters = func_get_args();\n";
$code .= " return Decorator::_invoke(\$this->_wrapped, '$method', \$parameters);\n";
$code .= " }\n";
}
$code .= " function &getDecorated() {\n";
$code .= " return \$this->_wrapped;\n";
$code .= " }\n";
$code .= "}\n";
return $code;
}
function &_invoke(&$object, $method, &$parameters) {
$aliases = array();
for ($i = 0; $i < count($parameters); $i++) {
$aliases[] = "\$parameters[$i]";
}
$code = '$result = &$object->' . $method . '(' . implode(', ', $aliases) . ');';
eval($code);
return $result;
}
}
And also a test case for hacking...
PHP Code:
if (! defined('SIMPLE_TEST')) {
define('SIMPLE_TEST', 'simpletest/');
}
require_once(SIMPLE_TEST . 'unit_tester.php');
require_once(SIMPLE_TEST . 'mock_objects.php');
require_once(SIMPLE_TEST . 'reporter.php');
require_once('decorator.php');
class Dummy {
function Dummy() {
}
function a() {
}
}
Decorator::generate('Dummy');
Mock::generate('Dummy');
class TestOfDecorator extends UnitTestCase {
function TestOfDecorator() {
$this->UnitTestCase('Decorator');
}
function testSimpleCallChaining() {
$wrapped = &new MockDummy($this);
$wrapped->setReturnValue('a', 'Out');
$wrapped->expectOnce('a');
$decorator = &new DummyDecorator($wrapped);
$this->assertEqual($decorator->a(), 'Out');
$wrapped->tally();
}
function testCallWithParameters() {
$wrapped = &new MockDummy($this);
$wrapped->setReturnValue('a', 'Out');
$wrapped->expectOnce('a', array(1, 2, 3));
$decorator = &new DummyDecorator($wrapped);
$this->assertEqual($decorator->a(1, 2, 3), 'Out');
$wrapped->tally();
}
function testReturnByReference() {
$thing = &new Dummy();
$wrapped = &new MockDummy($this);
$wrapped->setReturnReference('a', $thing);
$decorator = &new DummyDecorator($wrapped);
$this->assertReference($decorator->a(), $thing);
}
function testAccessToDecoratedObject() {
$wrapped = &new Dummy();
$decorator = &new DummyDecorator($wrapped);
$this->assertReference($decorator->getDecorated(), $wrapped);
}
}
$test = &new TestOfDecorator();
$test->run(new HtmlReporter());
yours, Marcus
Bookmarks