
I’m in the process of working on the new version of my PHP Thumbnailer class, and came across a couple of interesting challenges. I’ve gotten a lot of great suggestions for features, and have wanted to add them, but at the same time don’t as I would prefer not to bloat the class with all sorts of functionality. So I started thinking about how I could provide certain functionality for people that want it, without either simply making it a part of the class (and making it more bloated as a result), or coming up with all sorts of extended classes to maintain and distribute. The other thing I don’t want to do is end up creating an app… I want this thing to be a small library. Finally, I want people to be able to integrate the library / classes into their existing apps as easily as possible. The solution (or paradigm I suppose) that jumped out was plugins. Ah, but how does one create plugins for classes? Rather, how do you dynamically add functions to PHP classes? Well, after some googling and tinkering, I think I’ve come up with the solution. Read on to see how it’s done…
Before we get started, let’s set some baseline assumptions. First, we’re not going to have one class… it’s just not possible. Second, all plugins will need to be classes themselves, and these classes will not have any member variables (you’ll see why in a bit). Outside of that we’re pretty much free to do things however we want. Let’s also assume that we’re creating a user object that other people will be able to augment with whatever functionality they’d like.
So, let’s create our base class. This will contain all the magic functionality that makes this work. What we’re essentially going to do is create a method for importing the new functions, and then one that will allow these functions to be called. Here’s what the code looks like:
<?php class BaseUser { private $imported; private $imported_functions; public function __construct() { $this->imported = array(); $this->imported_functions = array(); } protected function imports($object) { // the new object to import $new_import = new $object(); // the name of the new object (class name) $import_name = get_class($new_import); // the new functions to import $import_functions = get_class_methods($new_import); // add the object to the registry array_push($this->imported, array($import_name, $new_import)); // add teh methods to the registry foreach($import_functions as $key => $function_name) { $this->imported_functions[$function_name] = &$new_import; } } public function __call($method, $args) { // make sure the function exists if(array_key_exists($method, $this->imported_functions)) { $args[] = $this; // invoke the function return call_user_func_array(array($this->imported_functions[$method], $method), $args); } throw new Exception ('Call to undefined method/class function: ' . $method); } } ?>
So, what have we done? Well, we’ve created a function “imports”, that will allow new functionality to be imported and added to the class. Essentially, all we’re doing is looking at a provided object, storing its name in an array, and storing all of its functions in another array. The other half of the magic is the “__call” function. This is a magic method provided by PHP, that will get called any time a function that isn’t defined is invoked on the class. This is what actually invokes the so-called imported functions. We’ve also provided a nice way to pass “$this” along to any of these imported functions automatically, as they won’t technically be a part of this class.
Now that we’ve got the base class, we can create a sample user class that extends it:
<?php class User extends BaseUser { private $first_name; private $last_name; public function __construct() { parent::__construct(); $this->first_name = ''; $this->last_name = ''; } // getters and setters.... public function getFullName() { return $first_name . ' ' . $last_name; } } ?>
I’ve omitted a lot of functionality that you’d need / want (like the getters and setters) as they’re irrelevant to what we’re doing here. Anyway, let’s say these two classes are our user library, and this is all the functionality we’d like to provide. However, several users of the library want a function that will return the user’s name formatted as “last, first”. Well, we’ve provided a nice way for them to add their own functionality without making major changes to the class, or requiring any extra work on our part.
To take advantage of this, one would first need to create a class containing the functions we’d like to add the the User class. Let’s do that now, providing the desired functionality previously described:
<?php class UserFunctions { public function lastThenFirst(&$that) { return $that->last_name . ', ' . $that->first_name; } } ?>
One thing to note here… since this function is not technically merged into the base class, we need a way to refer to the base class. So, we’ve go the parameter “$that” in our function. This variable will basically be a reference to the parent / base class, allowing us to access all the members and manipulate them in the context of our new “imported” function. If this doesn’t quite make sense yet, don’t worry. It should become clearer when you see everything together.
Now, all we need to do is glue everything together. The easiest way to do this is to simply modify the User class constructor to now read:
public function __construct() { parent::__construct(); $this->first_name = ''; $this->last_name = ''; $this->imports('UserFunctions'); }
That’s it! Now, any time a user object is created, it will also have the new “lastThenFirst()” function. Here’s all the code together, with a little test at the end of it (note, I’ve added some default values to the User class, and changed private vars to public since I skipped getters / setters):
<?php class BaseUser { private $imported; private $imported_functions; public function __construct() { $this->imported = array(); $this->imported_functions = array(); } protected function imports($object) { // the new object to import $new_import = new $object(); // the name of the new object (class name) $import_name = get_class($new_import); // the new functions to import $import_functions = get_class_methods($new_import); // add the object to the registry array_push($this->imported, array($import_name, $new_import)); // add teh methods to the registry foreach($import_functions as $key => $function_name) { $this->imported_functions[$function_name] = &$new_import; } } public function __call($method, $args) { // make sure the function exists if(array_key_exists($method, $this->imported_functions)) { $args[] = $this; // invoke the function return call_user_func_array(array($this->imported_functions[$method], $method), $args); } throw new Exception ('Call to undefined method/class function: ' . $method); } } class User extends BaseUser { public $first_name; public $last_name; public function __construct() { parent::__construct(); $this->first_name = 'Ian'; $this->last_name = 'Selby'; $this->imports('UserFunctions'); } // getters and setters.... public function getFullName() { return $this->first_name . ' ' . $this->last_name; } } class UserFunctions { public function lastThenFirst(&$that) { return $that->last_name . ', ' . $that->first_name; } } $user = new User(); echo $user->getFullName() . '<br />'; echo $user->lastThenFirst() . '<br />'; ?>
Which happily outputs:
Ian Selby Selby, Ian
Before I close, one other note… you would probably want to take this one step further by adding some functionality to the base class to automatically load plugins, thus preventing the need to manually add $this->imports() to your class. My approach to this notion will be available when I release the next version of my Thumbnailer class in the near future, so look there for some ideas… or, drop me a comment!
Enjoy!