PHP Function of the Day (2011-9-20): spl_autoload_register

Today’s function is part of the Standard PHP library introduced with PHP 5.0. It replaces the magic __autoload function and allows multiple PHP modules to include their own autoload functions. If you attempt to use __autoload at the same time as [fphp]spl_autoload_register[/fphp] the latter will fail to work, so be careful.

[fphp]spl_autoload_register[/fphp] is one of a handful of functions that take a PHP object known as a callback. This can take the form of a function name

spl_autoload_register('myAutoload');

or an array with a class name and method,

spl_autoload_register(array('myClass', 'myLoader'));

or an array with a class instance and a method.

spl_autoload_register(array($obj, 'myLoader'));

and as of PHP 5.3 you can just pass a closure directly to it.


spl_autoload_register( function( $class ) {
  /* function body */
});

Autoload functions are to return true if they succeed in finding the class, false if they do not. Prior to PHP 5.3 exceptions thrown from them cannot be caught.

For an extended example here’s the autoloader of the Gazelle framework.


/**
 * Gazelle Autoload System.
 * @author Michael
 */
class Loader extends ReadOnlyArray implements FileLibrary {
		
	/**
	 * When pulled from cache we relink with the PHP autoload service.
	 */
	public function unserialize($data) {
		parent::unserialize($data);
		spl_autoload_register(array($this, 'find'), true);
	}
	
	/**
	 * As we go to cache unlink from the autoload service.
	 */
	public function serialize() {
		spl_autoload_unregister(array($this, 'find'));
		return parent::serialize();
	}
	
	/**
	 * Determine if a class is available for loading.
	 * 
	 * NOTE BENE -- This is NOT a replacement for class_exists.
	 * That function, if called, will hit this autoloader and
	 * run the find method below - loading the class if it is
	 * found unless false is passed as the second parameter, in
	 * which case none of the autoloaders will be invoked before
	 * class_exists returns its response. This method allows you
	 * to check if the class is *available* for loading, but
	 * doesn't load it. The core framework uses this as a sneaky
	 * way of setting an error flag - if the ErrorHandler class
	 * got loaded then there are errors, but if it wasn't then
	 * there where none - hence the load state of that class
	 * itself is a flag.
	 * 
	 * @implements FileLibrary
	 * @param string $class
	 */
	public function exists( $class ) {
		return $this->findClass($class, false);
	}
	
	/**
	 * Responder to the PHP Autoload service. While this can be called
	 * to load the class without using the autoload service,
	 * the only time this should be necessary is if several
	 * autoloaders are in use and their stack order is unknown;
	 * or more likely - by the test system.
	 * 
	 * @implements FileLibrary
	 * @param string $class
	 */
	public function find( $class ) {
		return $this->findClass($class, true);
	}
		
	/**
	 * Find a class and if instructed to do so load it.
	 * 
	 * @param $path // Path of class to load.
	 * @param $load // Do we actually load it? 
	 */
	protected function findClass( $path, $load ) {

		$haystack = $this->getArrayCopy();
		$needles = explode("\\\\", $path);
		
		do {			
			$needle = array_shift($needles);
			if (!isset($haystack[$needle])) {
				break;
			} else if (is_array($haystack[$needle])) {
				$haystack = $haystack[$needle];
				continue;
			} else if (file_exists($haystack[$needle])) {
				if ($load) {
					require_once ( $haystack[$needle] );
				}
				return true;
			}
		} while (count($needles) > 0);
		
		/* 
		 * We failed find the requested class. We're in trouble.
		 * First, return false if not loading or if we're not
		 * the only registered autoloader.
		 */
		if ( $load == false || count(spl_autoload_functions()) > 1) {
			return false;
		}
		
		// Uh oh... Well, is our FatalException object present, cause
		// we'd rather use that.
		if ( $this->exists("Gazelle\\\\FatalException")) {
			throw new FatalException("Unable to load requested class {$path}");
		}
		
		// No? Well core exception it is then.
		
		throw new \\Exception("Unable to load requested class {$path}");
	}
}

In addition to the comments above, the find and exists methods must be defined as part of the FileLibrary interface. The purpose of the interface is that any object in the system that says its a “FileLibrary” (that is, implements FileLibrary) must implement a find and exists method, and the returns of those methods should usually be the same (most file libraries return the path on the find method but the loader class simply returns ‘true’ on successful find).

Note the loader does not search the file system. Instead this is done by the “factory” class that creates the loader. As part of that process the factory scans the filesystem for available classes and prepares an array which is stored with this loader. The loader in turn gets stored into APC cache between page loads by the framework, but the factory (and its attendant code and execution overhead) are not cached because they aren’t needed again until the framework file system changes (which does not normally happen during production).