The overloading features in PHP5 allow you to intercept calls to unknown methods at runtime. I recently used this feature to rapidly develop a class wrapper for Xapian, and it’s interesting to see how the technique works and what it achieves.
In this article, I’ll explain how you can use __call to wrap a flat function-based PHP5 library into a set of classes with minimal effort. I’ll use Xapian as an example – it has a SWIG generated binding which flattens the library’s C++ class interface into a set of functions.
Using a wrapper we can turn code like this…
$enquire = new_Enquire($database); Enquire_set_query($enquire, $query); $matches = Enquire_get_mset($enquire, 0, 10);
…into code like this…
$enquire = new XapianEnquire($database) $enquire->set_query($query); $matches = $enquire->get_mset(0, 10);
This allows you to use the library in a similar way to others using different languages, aiding communication, but also allows you to realise all the usual benefits of OO software design, such as deriving from these classes to provide specialised functionality.
So far so good, but wait until you see how lightweight the implemention of that XapianEnquire wrapper class is…
class XapianEnquire extends XapianWrapper { }
That’s not a typo. That’s all you need. Instantiate one of those and you can call all the methods you would expect from the Xapian documentation. Try and call a method which doesn’t exist and it will throw an exception.
Voodoo, surely?
Well, __call is certainly voodoo. But before we look at the downsides, lets pick out a few techniques used…
Figuring out what C++ class a resource represents
The C++ objects created by the library are presented to PHP as resource variables. They are really just opaque tokens, you can’t do anything with them aside from pass them back to the library that created them.
However, given a resource variable you can call get_resource_type() to get a string telling you something about the resource – this can help you automatically map a resource to a suitable PHP class wrapper:
$res=new_Database($file); echo get_resource_type($res);
The code above would display _p_Xapian__Database
So given a resource, we can figure out that it represents a C++ “Database” object, and with a bit of string manipulation, map that onto our “XapianDatabase” class.
This means that we take a resource variable return value from a library call and wrap it in an appropriate class at run time. Nice.
Turning method calls into function calls
When we want to resolve a method call like XapianDatabase::add_document(), PHP notices XapianDatabase doesn’t have such a method, and will go up the inheritance chain looking for a class which does. Eventually it will wind up calling our XapianWrapper::__call method.
We can figure out a few things about what we need to do:
* the method name ‘add_document’ is passed to __call() along with an array of parameters.
* we can use get_class() to determine our classname (XapianDatabase)
* combining these two pieces of information, we know we need to call Database_get_document()
We also need to ensure the first parameter to Database_get_document is our resource variable. This won’t be in the parameter list we’ve been given, so we insert it at the start of the parameter array with array_unshift()
Now we can simply use call_user_func_array() to actually invoke the function.
Coercing parameters and return value types
A programmer using this wrapper is going to want pass wrapper object variables as parameters. For example, if I have a XapianQuery object in the variable $query, I’ll want to pass that to $enquire->set_query($query).
Trouble is, the library function Enquire_set_query() expects a resource variable – it wouldn’t know what to do with our object variable. So, we need to identify all the parameters which are objects with XapianWrapper as a parent class, and turn them into resource variables, not forgetting that sometimes you might be passing arrays of parameters too.
Thankfully, there’s a neat solution – array_walk_recursive() – we just use that to run a callback across every element of the parameter array – the callback checks to see if the parameter value is a XapianWrapper object, and if so, replaces it with it’s corresponding resource variable. Lovely.
In a similar vein, if a library function returns a resource, we don’t want to return that – we want to to return a class which wraps it. As mentioned earlier, this is easily done – we use get_resource_type() to figure out which php class to instantiate as a wrapper, and return an object variable instead.
Coping with inheritance
So far so good, but it would be all for nothing if you could not subclass the wrapper classes. Let’s say you wanted to subclass XapianWritableDatabase and called it MyWritableDatabase – the __call handler has to figure out that any Xapian library calls you want to make aren’t derived from the classname “MyWritableDatabase”, but from it’s parent class “XapianWritableDatabase”.
Furthermore, as the Xapian WritableDatabase inherits from Database, you need to be able to call XapianDatabase methods on a XapianWritableDatabase object.
This leads us to the final trick, where the __call method looks for a suitable library function matching the current class name, but if one is not found, uses the parent class name instead, ad infinitum, until it reaches the top of inheritance chain without finding a match. If this happens, the method call was bogus and an exception must be thrown.
Putting it all together
With all these pieces of the puzzle together, we can build a base class which allows use to define a real, working wrapper class simply by deriving from it with an appropriate name:
class XapianEnquire extends XapianWrapper { }
So here is that voodoo in full. Excuse the odd layout – line lengths are short so it remains readable in this page…
abstract class XapianWrapper { /** * The wrapped Xapian resource */ public $ptr=null; /** * Smart constructor will wrap a Xapian resource if a * suitable one is passed, otherwise will call the * relevant new(..) method passing all parameters * along with it */ public function __construct($ptr=null) { //we can do a variety of clever things here - //firstly, if the param is a resource and its //type matches the class we're wrapping, //we can use it as our ptr $create_resource=true; if (is_resource($ptr)) { $class=get_class($this); while (strpos($class,'Xapian')!==0) $class=get_parent_class($class); $xapian_class=substr($class,6); if (self::isXapian($ptr, $xapian_class)){ $create_resource=false; $this->ptr=$ptr; } } if ($create_resource) { //we weren't passed a Xapian resource, which //means we probably want to call new, so we //call it and pass all the parameters we //were passed... $args=func_get_args(); $this->ptr=call_user_method_array( 'new', $this, $args); } } /** * This handles most Xapian method calls by figuring * out which function should be called and amending * the parameters to suit it */ private function __call($m, $a) { //figure out function name //find the XapianXYZ parent class - from this we //determine which type of Xapian object is //wrapped. To do this we go up the inheritance //chain until we find a class starting with //'Xapian' $class=get_class($this); while (strpos($class,'Xapian')!==0) $class=get_parent_class($class); $xapian_class=substr($class,6); //now we have a xapian class, we can see if the //required method is mapped $func=($m!='new')? ($xapian_class.'_'.$m) : ($m.'_'.$xapian_class); while (!function_exists($func)) { //not available, but maybe in a parent? $class=get_parent_class($class); if (!empty($class) && ($class!='XapianClassWrapper')) { $xapian_class=substr($class,6); $func=($m!='new') ? ($xapian_class.'_'.$m) : ($m.'_'.$xapian_class); } else { //we've reached the top of the //inheritance chain, no joy... break; } } if (function_exists($func)) { //no point passing instances of this //class to xapian, but it's nice to do //from from the outside, so let's //coerce those into the right type array_walk_recursive($a, array($this, 'coerce_param')); //pass ptr as first param if function isn't a //"new" call if ($m!='new') array_unshift($a, $this->ptr); $rvalue=call_user_func_array($func, $a); if (is_resource($rvalue) && ($m!='new')) { //wrap in class? $xapianclass = self::getClassnameFromResource($rvalue); $wrapperclass='Xapian'.$xapianclass; if (class_exists($wrapperclass)) { //create a new instance of the //appropriate wrapper - $rvalue=new $wrapperclass($rvalue); } } return $rvalue; } else { throw new Exception( "Xapian $func not found"); } } //////////////////////////////////////////// // internal methods //////////////////////////////////////////// /** * array_walk callback which turns any XapianWrapper * object into a resource variable instead */ private function coerce_param(&$value, $index) { if (is_object($value) && is_a($value, 'XapianWrapper')) $value=$value->ptr; } /** * static method which given a result will tell * you if it is a Xapian resource and optionally * verify the expected type */ static function isXapian($res, $req_type=null) { $ok=is_resource($res); if ($ok) { //xapian resources look like this //_p_Xapian__Database $type=explode('_', get_resource_type($res)); $ok=(count($type)>=5) && ($type[2]=='Xapian') && ( ($req_type==null) || ($req_type==$type[4])); } return $ok; } /** * given Xapian resource, find class name */ static function getClassnameFromResource($res) { $type=explode('_', get_resource_type($res)); return $type[4]; } }
Well, you reached the end. Let’s round up…
…the good!
This technique allows very rapid wrapping of flattened OO-based libraries. In the case of Xapian, you can wrap 20 or so classes with 240 methods simply by deriving empty classes from the XapianWrapper.
…the bad!
Because the classes have no actual methods, there no possibility of introspection, and no machine generated documentation. Your IDE isn’t going to be able to offer any code completion hints either.
…and the ugly!
Because the wrapper is catering for all scenarios, and managing the inheritance chain itself, there is some overhead in each method call that would not be present in a hand-crafted wrapper. Depending on the library you’re wrapping, this may or may not be a big factor – it really depends on the proportion of the time spent inside the library calls.
Summary
I wrote this both to present what I think is a neat and rapid way to provide wrapper classes for OO-based function libraries, but I’ve hopefully also highlighted a few neat tricks and PHP functions which you might find useful in entirely different areas.
If you’ve found this useful, or have constructive feedback, please do leave a comment!