Adding Extension Methods to PHP
If you've ever used the .NET Framework version 3.5 of later, you'll probably have encountered extension methods. Microsoft describes extension methods as:
Extension methods enable you to "add" methods to existing types without creating a new derived type, recompiling, or otherwise modifying the original type. Extension methods are a special kind of static method, but they are called as if they were instance methods on the extended type. For client code written in C# and Visual Basic, there is no apparent difference between calling an extension method and the methods that are actually defined in a type.
In this article, I will show you how to write a base class that will allow you to add methods to any PHP class that inherits from it at runtime. You will be able to call these methods transparently, without any special syntax.
For those unfamiliar with .NET, I'll explain extension methods in a PHP context. For example, consider the following code for a limited .NET-style string class in PHP:
final class NetString {
private $value;
public function __construct($str) {
$this->value = $str;
}
public function toUpper() {
return new NetString(strtoupper($this->value));
}
public function string() {
return $this->value;
}
}
$str = new NetString("foo");
$upper = $str->toUpper()->string();
// $upper now contains "FOO"
What if, at some point in the future, we wanted to add a toLower
method and we didn't have access to the source code? The first thing that probably comes to mind is to use inheritance. Unfortuantely, NetString
is a final class, so that's out of the question. So what else?
All we're left with is using a function or a static method. Wouldn't it be nice if we could do something like this?
NetString::addMethod("toLower", function($str) {
return new NetString(strtolower($str->string()));
});
$str = new NetString("foo");
$upper = $str->toUpper()->string();
// $upper now contains "FOO"
$lower = $upper->toLower()->string();
// $lower now contains "foo"
Starting with PHP 5.3, this is possible thanks to closures and some other new features. The rest of this article will be dedicated to explaining how to setup extension methods in PHP.
How It Works
In order to add extension methods to PHP, we need to do a couple of things. First off, since an extension method is basically a missing method, we need to find some way to catch any calls to missing methods. Fortunately, PHP provides a mechanism for doing this. It's a magic method called __call
.
What we need to do is make sure any class that we want to have extension methods have it's __call
method rerouted to a method dispatcher. This dispatcher will have to be static so all instances of a class can access it. In order to ensure that these calls are rerouted to the dispatcher, we'll create a common base class that does that automatically for us. We'll call this base class Extensible
.
class Extensible {
public function __call($name, $args) {
self::methodDispatcher($this, $name, $args);
}
public static function methodDispatcher(
$instance, $name, $args) {
}
}
Now that we have our missed calls rerouted to our dispatcher, we need two more things. We need a way to add methods to the object (addMethod
) and we need somewhere to store them to look them up with the dispatcher ($methodTable
).
class Extensible {
public function __call($name, $args) {
self::methodDispatcher($this, $name, $args);
}
private static $methodTable = array();
public static function methodDispatcher(
$instance, $name, $args) {
}
public static function addMethod($methodName, $method) {
}
}
We're almost there. How can we store a method so that we can uniquely identify it? The easiest way would be in an associative array indexed by class name. Each value in the that associative array would be another associative array holding all the extension methods (i.e. the closures) for that class.
class Extensible {
public function __call($name, $args) {
self::methodDispatcher($this, $name, $args);
}
private static $methodTable = array();
public static function methodDispatcher(
$instance, $name, $args) {
}
public static function addMethod($methodName, $method) {
$class = get_called_class();
$table =& self::$methodTable;
if (!array_key_exists($class, $table))
$table[$class] = array();
$table[$class][$methodName] = $method;
}
}
Now that we have all the extension methods stored, how do we invoke them? Well, if they're going to be useful at all, they'll need to have access to the instance they're attached to. Like .NET, we'll provide that automatically as the first argument. We also need to pass the arguments that were passed in the call.
class Extensible {
public function __call($name, $args) {
self::methodDispatcher($this, $name, $args);
}
private static $methodTable = array();
public static function methodDispatcher(
$instance, $name, $args) {
$table =& self::$methodTable;
$class = get_class($instance);
do {
if (array_key_exists($class, $table)
&& array_key_exists($name, $table[$class]))
break;
$class = get_parent_class($class);
} while ($class !== false);
if ($class === false)
throw new Exception("Method not found");
$func = $table[$class][$name];
array_unshift($args, $instance);
return call_user_func_array($func, $args);
}
public static function addMethod($methodName, $method) {
$class = get_called_class();
$table =& self::$methodTable;
if (!array_key_exists($class, $table))
$table[$class] = array();
$table[$class][$methodName] = $method;
}
}
And we're there. Now, in order to add extension methods to a class, we just have to ensure that inherits from our Extensible class. I'll admit this is kind of a crappy requirement, but it's the best we can do for now.
Here's an example:
final class Bug extends Extensible {
private $name;
private $arms;
public function __construct($name, $arms) {
$this->name = $name;
$this->arms = $arms;
}
public function getName() { return $this->name; }
public function getArms() { return $this->arms; }
}
Bug::addMethod("hug", function($bug, $otherBug) {
echo $bug->getName() . " hugs " . $otherBug->getName();
});
Bug::addMethod("punch", function($bug, $otherBug) {
echo $bug->getName()
. " punches " . $otherBug->getName()
. " with " . $bug->getArms() . " fists ";
});
$doug = new Bug("Doug", 10);
$fred = new Bug("Fred", 4);
$fred->hug($doug);
$doug->punch($fred);
Limitations
This implementation of extension methods in PHP isn't all puppy dogs and ice cream. It has its share of drawbacks:
- Classes must inherit from
Extensible
to be capable of having extension methods. This sucks, but you can get around it in existing classes by using composition. - Speed. Although I have not tested it myself, magic methods like
__call
are apparently quite slow. Ignores inheritance. This particular implementation does not follow the inheritance chain to see if any parent classes have extension methods for a given method call. This could be easily remedied with a bit of code.See the update below.
Source
The source code I have written in this article is public domain, and you are free to do what you will with it. Here is the source file.
Update
I've updated the code so that methodDispatcher
will search the inheritance chain for a suitable extension method. This behaviour is illustrated by this example:
class Foo extends Extensible { }
class Bar extends Foo { }
Foo::addMethod("customMethod", function($object) {
return "Foo";
});
$bar = new Bar();
$result = $bar->customMethod();
// $result contains the string "Foo"
6 comments