A PHP
issue that comes up
over and
over again is how the
static keyword doesn't know about inheritance. That is, code such as:
class Masons {
static $where = "World-wide";
static function show() {
print self::$where;
}
}
class Stonecutters extends Masons {
static $where = "Springfield";
}
Stonecutters::show();
prints
World-wide, not
Springfield because the
self inside
Masons::show() is bound at compile time to the
Masons class. This is different than how
$this works in instances, so it can be unexpected.
There are plenty of good reasons why PHP 5 works this way and it seems that in PHP 6 the
static keyword will be able to be used in place of
self to get the dynamic behavior a lot of folks are looking for (that is,
print static::$where; in
Masons::show() would cause
Stonecutters::show() to print
Springfield.
All well and good once PHP 6 is done.
In the meantime, I was noodling around with
runkit and came up with some glue that lets you do something like this:
class Model {
public static function find($class, $filters) {
// This method would actually do an SQL query or
// REST request to retrieve data
print "I'm looking for a $class with ";
$tmp = array();
foreach ($filters as $k => $v) { $tmp[] = "$k=$v"; }
print implode(', ', $tmp);
print "\n";
}
public static function findById($class, $id) {
return self::find($class, array('id' => $id));
}
}
class Monkey extends Model { }
class Elephant extends Model {
public static function findByTrunkColor($color) {
return self::find(array('color' => $color));
}
}
And then after the mysterious (for a few seconds) glue:
MethodHelper::fixStaticMethods('Model');
you can do:
Monkey::find(array('name' => 'George', 'is' => 'curious'));
// prints: I'm looking for a Monkey with name=George, is=curious
Elephant::findById(1274);
// prints: I'm looking for a Elephant with id=1274
Elephant::findByTrunkColor('grey');
// prints: I'm looking for a Elephant with color=grey
Monkey::findById('abe');
// prints: I'm looking for a Monkey with id=abe
(The innards of
MethodHelper after the jump...)
The glue is as follows:
class MethodHelper {
public static function fixStaticMethods($parentClass) {
foreach (get_declared_classes() as $class) {
// In each subclass of $parentClass,
if (is_subclass_of($class, $parentClass)) {
$rc = new ReflectionClass($class);
// Look for all the methods that are:
foreach ($rc->getMethods() as $m) {
if ($m->isStatic() && // static
$m->isPublic() && // public
// and inherited
($m->getDeclaringClass()->getName() != $class)) {
// and then redefine them so they call their parent method
// explicitly, passing the class name as a first argument
$body='
$args = func_get_args();
array_unshift($args, "'.$rc->getName().'");
return call_user_func_array(array("parent","'.$m->getName().'"),$args);
';
runkit_method_redefine($class, $m->getName(),
'',$body,RUNKIT_ACC_PUBLIC | RUNKIT_ACC_STATIC);
}
}
}
}
}
}
Each subclass of the class passed to
fixStaticMethods() gets tweaked, thanks to the Reflection API and runkit. Any
public static methods that aren't actually defined in the subclass, but just inherited from above get defined. And the body of the new definition just takes any provided arguments, tacks the current class name (of the subclass) onto the front of the list, and then calls the parent method with the new args. This way, the actual subclass name is available to the static method in the parent class.
A fun proof of concept, but not something (for efficiency reasons) you'd probably want to run on a busy production site.
Some other notes:
- That
RUNKIT_ACC_STATIC constant, which tells
runkit_method_redefine() to make the new method static, is not part of runkit CVS (yet) -- I sent the patch to Sara, but if you want to download it, you can find it at
http://www.sklar.com/files/runkit-static-method.diff if you're impatient. Everything will run if you leave that flag out, but you'll get "Non-static method should not be called statically" messages with
E_STRICT turned on.
- I am not sure whether I like the fact that the new methods have different signatures than their parents. I suppose another approach would be to maintain, as a class variable in the parent class, a stack of "current class name" values. And then the child can push its class name onto the stack before invoking the parent method, the parent method looks at the top of the stack to see what the current subclass is, and then the child method pops its class name off the stack after it's done calling the parent method.