Quite often when you are working with legacy code you will come across a mess of globals. Every single method will make use of the same global instance of the database class for example. So where do you begin to work with this massive impediment?
Logging is a great way to see what methods and classes are being used by you application and where. To achieve this you would normally need to add a logging call to each and every method in the code base. Clearly this would be incredibly tedious and time consuming.
This is where a proxy object can be implemented to save time and centralise the logging functions. The basic idea of a proxy object is that it will be instantiated in place of the actual class and the proxy will delegate any calls through to the original class. For the purposes of this example the original class will be called Database and the proxy object will be called LazyLoadingProxy.
Based upon the diagram above we can work through a simple example to demonstrate this powerful technique. Initially the class lazy loading proxy is instantiated and assigned to the global variable $Database
.
<?php
$Database = new LazyLoadingProxy('Database', '/home/simon/DatabaseClass.php');
$Blog = new LazyLoadingProxy('Blog', '/var/www/classes/Blog.class.php5');
When the proxy is setup it is told the class name and then the exact path where it can find the class. The proxy will now lie in wait for a request to a method or class property of the proxied class (Database
).
<?php
echo $Database->getTable(); // echo the name of the table
If the underlying class is accessed the proxy will require_once()
Database.class.php and create a new instance of it on the fly. This is the lazy loading aspect of the process. In this way resources are not consumed until the Database class is really needed. Once instantiated the object is “cached” so that any future requests will reuse the same instance of the class.
So our proxy lazy load class is very simple like the following:
<?php
/**
* @author Simon Holywell <[email protected]>
*/
class LazyLoadingProxy {
/**
* Where the instance of the actual class is stored.
* @var $instance object
*/
private $instance = null;
/**
* The name of the class to load
* @var $class_name string
*/
private $class_name = null;
/**
* The path to the class to load
* @var $class_path string
*/
private $class_path = null;
/**
* Set the name of the class this LazyLoader should proxy
* at the time of instantiation
* @param $class_name string
*/
public function __construct($class_name, $class_path = null) {
$this->setClassName($class_name);
$this->setClassPath($class_path);
}
public function setClassName($class_name) {
if(null !== $class_name) {
$this->class_name = $class_name;
}
}
public function getClassName() {
return $this->class_name;
}
public function setClassPath($class_path) {
if(null !== $class_path) {
$this->class_path = $class_path;
}
}
public function getClassPath() {
return $this->class_path;
}
/**
* Get the instance of the class this LazyLoader is proxying.
* If the instance does not already exist then it is initialised.
* @return object An instance of the class this LazyLoader is proxying
*/
public function getInstance() {
if(null === $this->instance) {
$this->instance = $this->initInstance();
}
return $this->instance;
}
/**
* Load an instance of the class that is being proxied.
* @return object An instance of the class this LazyLoader is proxying
*/
private function initInstance() {
Logger::log('Loaded: ' . $class_name);
require_once($this->class_path);
$class_name = $this->class_name;
return new $class_name();
}
/**
* Magic Method to call functions on the class that is being proxied.
* @return mixed Whatever the requested method would normally return
*/
public function __call($name, $arguments) {
$instance = $this->getInstance();
Logger::log('Called: ' . $this->class_name . '->' . $name . '(' . print_r($arguments, true) . ');');
return call_user_func_array(
array($instance, $name),
$arguments
);
}
/**
* These are the standard PHP Magic Methods to access
* the class properties of the class that is being proxied.
*/
public function __get($name) {
Logger::log('Getting property: ' . $this->class_name . '->' . $name);
return $this->getInstance()->$name;
}
public function __set($name, $value) {
Logger::log('Setting property: ' . $this->class_name . '->' . $name);
$this->getInstance()->$name = $value;
}
public function __isset($name) {
Logger::log('Checking isset for property: ' . $this->class_name . '->' . $name);
return isset($this->getInstance()->$name);
}
public function __unset($name) {
Logger::log('Unsetting property: ' . $this->class_name . '->' . $name);
unset($this->getInstance()->$name);
}
}
To log the calls you can simply echo out the request in each magic method or you could make it more sophisticated with the help of FirePHP and FireBug. I use the latter and it is really handy to see where the legacy code is calling classes and methods that it should not be!
The only hiccup I have found with this system is that all tests using method_exists()
need to be changed to be is_callable()
. This is because the former appears to use PHPs introspection methods where as the latter attempts to call the method from what I can guess.
This design pattern also has another application in systems that make extensive use of global objects which are instantiated at bootstrap. It will save memory as only the classes that are actually referenced will be instantiated just in time.
I recently used this to make a large legacy application more efficient. In the bootstrap file it simply looped through all the files in a directory called classes and looked for files beginning with a certain prefix (for example SYSBlog.php or SYSSessions.php). When it found a file it would load it via require_once()
and then using eval()
it would instantiate the class. This would subsequently be used globally as you can see below.
<?php
$dir = 'classes/';
if (is_dir($dir)) {
if ($dh = opendir($dir)) {
while (($file = readdir($dh)) !== false) {
if($file != '.' && $file != '..' && !is_dir($dir.$file)){
require_once($dir.$file);
}
}
closedir($dh);
}
}
// SNIP
// some unrelated code such as DB connectors appeared here
// /SNIP
$classes = get_declared_classes();
foreach($classes as $class){
if(substr($class,0,3)=='SYS'){
$class2 = str_replace('SYS','',$class);
eval('$'.strtolower($class2).' = new '.$class.'();');
}
}
A few optimisations that could be applied to the aforementioned code would be to:
- make use of the glob() function in PHP, which allows you loop through files that match a pattern. In this case something like
glob('SYS*.php')
would have done the trick. - remove the use of eval() which could just as easily be implemented using variable variables. So in the example this should be implemented as
$$className = $className()
and if you need to access a property$$className->getMethod()
. - make use of the SPL autoload functions that PHP provides to call the classes when they are needed.
But lets just be honest with ourselves; the code is crap and needs to be rewritten, but what if we do not have the time? Then we can use the following (only slightly improved I know):
<?php
$dir = 'classes/';
if (is_dir($dir)) {
if ($dh = opendir($dir)) {
while (($file = readdir($dh)) !== false) {
if($file != '.' && $file != '..' && !is_dir($dir.$file)){
require_once($dir.$file);
}
}
closedir($dh);
}
}
// SNIP
// some unrelated code such as DB connectors appeared here
// /SNIP
$classes = get_declared_classes();
foreach($classes as $class){
if(substr($class,0,3)=='SYS'){
$class2 = str_replace('SYS','',$class);
eval('$'.strtolower($class2).' = new '.$class.'();');
}
}
Now the class will only be loaded into memory when it is actually being used. This makes sense when you think that a response to a poll would never need to use methods in the blog classes for example. I found this technique was saving 30-70% of memory depending on the request. This could be most easily seen when making ajax requests as they often only made use of one class to perform their actions.
In the actual project the code is slightly different in that it does use an autoloader called Überloader written by Jamie Matthews (a colleague at Mosaic), which is available over on his github account. With the autoloader I was able to dispense with all the require_once()
s and the need to pass the class path into the lazy loading proxy class.