Hierarchical Class Loading With Namespaces

A challenge that I often encounter when developing in PHP is the ability to instantiate a class from the highest point in a folder hierarchy where that class exists, but fall down the hierarchy when it is missing. Confused? That wasn't very clear at all! Let me illustrate with an example.

Imagine that I have a shared cms with a folder structure that looks like this:

What is often desirable is to be able to either over-ride or extend any part of a module from the Base modules with one from the Extensions. That is have the system use the classes in the Extensions folder before they get to the Base folder if they happen to exist and fall back on the originals if they do not. Simple you say?... just require the correct file(s) and extend or replace the correct classes and code in the correct class name. But what if you want this autoloaded (not having to require files everywhere!), with the ability to just code the module includes once without file_exists statements everywhere?

Well, I'm going to show one simple solution to this issue here, and becasue it is just that, a simple issue, I will throw in PHP namespaces just for fun (PHP >= 5.30) - this can be done without them! I'm not going to yap about them because I have a separate article on their use.

To just over-ride the classes we could do this in the __autoload function, so just instantiate a new class:

<?php
$inst = new module_3();
 
function __autoload($class)
{
	$module = str_replace('_', ' ', ucfirst($class));
	if(file_exists('Extensions/'.$module.'/index.php'))
	{
		require_once('Extensions/'.$module.'/index.php');
	}
	else
	{
		require_once('Base/'.$module.'/index.php');
	}
}

And job done. However, the class name in the __autoload function is always module_3, and you can't very well say "module_3 extends module_3", you need something like "extension_module_3 extends module_3", but now the __autoload function cannot handle this because it is still looking for the original class name module_3 and it will never load the extension...

Sadly namespacing didn't fix this either, despite my great hopes for an efficient solution and we are left with using a factory class to create our classes using reflection to bail us out (I know it is slow!). I will illustrate a solution using a very cut down example, 4 files:

File 1: /Base/Sub/Test.php

<?php 
namespace Base\Sub;
 
class Test
{
	public function run()
	{
		return 'Running: '.__METHOD__.'<br />';
	}
}

File 2: /Extension/Sub/Test.php

<?php 
namespace Extension\Sub;
 
class Test extends \Base\Sub\Test
{
	public function run()
	{
		$string = 'Running: '.__METHOD__.'<br />';
		$string .= 'Extending the parent class...<br />';
		$string .= parent::run();
 
		return $string;
	}
}

File 3: /index.php

<?php
use App\Load as L;
 
# Load in the autoloader
require_once('App'.DIRECTORY_SEPARATOR.'Load.php');
L::register();
 
# Test it
try
{
	$test = L::factory('Sub\Test');
	echo $test->run();
}
catch(Exception $e)
{
	echo $e->getMessage();
}
 
 
/* ASIDE - NEED TO MODIFY THE CLASSES FOR THESE TO RUN */
# Of course there are different scenarios (not shown directly):

# If the class accepted variables
$test = L::factory('Sub\Test', $variable, $another, $etc);
echo $test->run();
 
# If run was a static method
echo L::factory('Sub\Test::run');
 
# If run was a static method with variables
echo L::factory('Sub\Test::run', $variable, $another, $etc);

File 4: /App/Load.php

<?php 
namespace App;
 
class Load
{
	# Load classes in a hierarchical manner
	public static function factory()
	{
		# Get our arguments and class
		$args	= func_get_args();
		$class	= array_shift($args);
		$method	= '__construct';
 
		# Cater for static calls too
		$parts = explode('::', $class);
		if(isset($parts[1]))
		{
			$method = $parts[1];
			$class	= $parts[0];
		}
 
		# Find the path
		$path = str_replace('\\\', DIRECTORY_SEPARATOR, $class).'.php';
 
		# Move through our hierarchy of namespaces
		$namespaces	= array('Extension', 'Base');
 
		foreach($namespaces as $namespace)
		{
			$file = $namespace.DIRECTORY_SEPARATOR.$path;
			$name = $namespace.'\\\'.$class;
 
			if(file_exists($file))
			{
				include_once($file);
 
				try
				{
					$reflection = new \ReflectionClass($name);
					break;
				}
				catch(ReflectionException $e)
				{
					# Ignore this - we may have a file but an incorrect classname
				}
			}
		}
 
		if(is_null($reflection))
		{
			throw new \Exception('Class "'.$class.'" could not be found for loading'); 
		}
 
		$instance = null;
 
		# We are instantiating a standard class
		if($method == '__construct')
		{
			# We have arguments so we should have a constructor
			if(count($args) > 0 AND $reflection->hasMethod($method))
			{
				try
				{
					$instance = $reflection->newInstanceArgs($args);
				}
				catch(ReflectionException $e)
				{
					throw new \Exception($e->getMessage());
				}
			}
			else
			{
				# We have no arguments / constructor so we can just create a class
				$instance = new $name;
			}
		}
		else
		{
			# We are making a static call
			if(method_exists($name, $method))
			{
				return call_user_func_array($name.'::'.$method, $args);
			}
			else
			{
				throw new \Exception('Method "'.$method.'" does not exist in class "'.$class.'"');
			}
		}
 
		if(!is_object($instance))
		{
			throw new \Exception('Class "'.$class.'" could not be instantiated'); 
		}
 
		return $instance;
	}
 
	# Method to do the autoloading
	public static function autoload($class)
	{
		$path = str_replace('\\\', DIRECTORY_SEPARATOR, $class).'.php';
		require_once($path); 
	}
 
	# Let the system know that this class will handle the autoloading
	public static function register()
	{
		if(!function_exists('spl_autoload_register'))
		{	
			throw new \Exception('spl_autoload does not exist in this PHP installation');
		}
 
		spl_autoload_register(__NAMESPACE__.'\Load::autoload');
	}
}

So what is happening in a nutshell? Well, rather than creating a new class in one of the traditional ways you pass the class and module to the factory method in the Load class and let it instantiate your object for you. It registers itself as the autoloader which you would call once in your bootstrap file if this were used in production. Here I have aliased App\Load to 'L' for ease of typing and you can then use L::factory('module\class', $optional, $variables); or L::factory('module\class::method', $optional, $variables); to load all module classes in a codebase.