Page 1 of 1

Alternative autoloader for PSR-0 proposal

Posted: Sun May 30, 2010 8:16 am
by Zyxist
The code below is an alternative to SplClassLoader which loads the classes following the naming convention described in http://groups.google.com/group/php-stan ... l-proposal . I wrote it because I found out that the original autoloader implementation is quite slow for a bigger number of libraries (vendors). Instead of registering different autoloader instance for each class that forces us to perform lots of unnecessary string operation when matching class names, I have one instance for all the classes and the paths are kept in an array:

Code: Select all

<?php
/**
 * Aggregate autoloader that implements the technical interoperability
 * standards for PHP 5.3 namespaces and class names.
 *
 * http://groups.google.com/group/php-standards/web/final-proposal
 *
 * @author Tomasz Jędrzejewski <http://www.zyxist.com/en/>
 */
class AggregateAutoloader
{
	/**
	 * The list of available libraries.
	 * @var array
	 */
	private $_libraries = array();
	
	/**
	 * The loaded file extension
	 * @var string
	 */
	private $_fileExtension = '.php';
	
	/**
	 * The namespace separator
	 * @var string
	 */
	private $_namespaceSeparator = '\\';

	/**
	 * Registers a new library to match.
	 *
	 * @param string $library The library name to add.
	 * @param string $path The path to the library.
	 */
	public function addLibrary($library, $path)
	{
		if(isset($this->_libraries[(string)$library]))
		{
			throw new Exception('Library '.$library.' is already added.');
		}
		if($path[strlen($path) - 1] != DIRECTORY_SEPARATOR)
		{
			$path .= DIRECTORY_SEPARATOR;
		}
		$this->_libraries[(string)$library] = $path;
	} // end addLibrary();
	
	/**
	 * Checks if the specified library is available.
	 *
	 * @param string $library The library name to check.
	 */
	public function hasLibrary($library)
	{
		return isset($this->_libraries[(string)$library]);
	} // end hasLibrary();
	
	/**
	 * Removes a recognized library.
	 *
	 * @param string $library The library name to remove.
	 */
	public function removeLibrary($library)
	{
		if(!isset($this->_libraries[(string)$library]))
		{
			throw new Exception('Library '.$library.' is not available.');
		}
		unset($this->_libraries[(string)$library]);
	} // end removeLibrary();

	/**
	 * Sets the namespace separator used by classes in the namespace of this class loader.
	 * 
	 * @param string $sep The separator to use.
	 */
	public function setNamespaceSeparator($sep)
	{
		$this->_namespaceSeparator = $sep;
	} // end setNamespaceSeparator();

	/**
	 * Gets the namespace seperator used by classes in the namespace of this class loader.
	 *
	 * @return string
	 */
	public function getNamespaceSeparator()
	{
		return $this->_namespaceSeparator;
	} // end getNamespaceSeparator();

	/**
	 * Sets the file extension of class files in the namespace of this class loader.
	 * 
	 * @param string $fileExtension
	 */
	public function setFileExtension($fileExtension)
	{
		$this->_fileExtension = $fileExtension;
	} // end setFileExtension();

	/**
	 * Gets the file extension of class files in the namespace of this class loader.
	 *
	 * @return string $fileExtension
	 */
	public function getFileExtension()
	{
		return $this->_fileExtension;
	} // end getFileExtension();

	/**
	 * Installs this class loader on the SPL autoload stack.
	 */
	public function register()
	{
		spl_autoload_register(array($this, 'loadClass'));
	} // end register();

	/**
	 * Uninstalls this class loader from the SPL autoloader stack.
	*/
	public function unregister()
	{
		spl_autoload_unregister(array($this, 'loadClass'));
	} // end unregister();
	
	/**
	 * Loads the given class or interface.
	 *
	 * @param string $className The name of the class to load.
	 * @return void
	 */
	public function loadClass($className)
	{
		$replacement = str_replace(array('_', $this->_namespaceSeparator), DIRECTORY_SEPARATOR, ltrim($className, $this->_namespaceSeparator));
		$id = strpos($replacement, DIRECTORY_SEPARATOR);
		if($id === false)
		{
			return false;
		}
		$match = substr($replacement, 0, $id);
		$path = substr($replacement, $id+1, strlen($replacement) - $id);
		
		if(!isset($this->_libraries[$match]))
		{
			return false;
		}
		require($this->_libraries[$match].$path.$this->_fileExtension);
		return true;
	} // end loadClass();
} // end AggregateAutoloader;
Sample use:

Code: Select all

$loader = new AggregateAutoloader;
$loader->addLibrary('Foo', './path/to/Foo/');
$loader->addLibrary('Bar', './path/to/Bar/');
$loader->addLibrary('Joe', './path/to/Joe/');
$loader->register();

Re: Alternative autoloader for PSR-0 proposal

Posted: Sun May 30, 2010 12:20 pm
by AbraCadaver
It would be interesting to see some benchmarks between the two, loading a large number of files.

Re: Alternative autoloader for PSR-0 proposal

Posted: Sun May 30, 2010 1:05 pm
by Zyxist
Here they are: http://media.zyxist.com/download/autoloadBench.tar.gz

I tested loading 200 classess grouped in 10 libraries:

* Manual (bare) class loading: 0.0366496 s
* AggregateAutoloader: 0.0423926 s
* SplClassLoader: 0.0501879 s

And loading 200 classes grouped in 1 library:

* Manual (bare) class loading: 0.0374809 s
* AggregateAutoloader: 0.0417581 s
* SplClassLoader: 0.0432021 s

For more libraries, AggregateAutoloader tends to be significantly faster, whereas in case of a single library the results are within a statistical error.

Re: Alternative autoloader for PSR-0 proposal

Posted: Sun May 30, 2010 6:22 pm
by Weirdan
Couple of notes here:
  1. Don't throw Exception, throw your own specific descendant exception class. This will allow client to perform selective catch.
  2. File extension is defined globally for entire autoloader, while in reality it could be different for each included library.
  3. '/' works as a separator on both windows and netware - no need to use platform-specific directory separator. In fact, should someone pass 'path/to/dir/' in a addLibrary() call on windows, it would add '\' to the path.
  4. PSR-0 says '_' is not translated into directory separator when used as a part of namespace, yet your autoloaded does just that.

Re: Alternative autoloader for PSR-0 proposal

Posted: Mon May 31, 2010 12:18 am
by Zyxist
Exceptions -> I know; if I was to deploy it somewhere, I would add some kind of specific exception, but just for testing it was unnecessary.

File extensions -> I know, but actually I haven't seen any library that used something else than .php for quite a long time. For some extra features, the autoloader could be extended to keep an array of settings for each library, not only with the path, but also file extension and other things, if needed.

Underscore -> hmm, actually you're right:
The underscore '_' has no special meaning in namespaces.
I'll fix it, as well as directory separator issues.

----
Edit:

OK, the improved version is available under the same link. The autoloading function has been rewritten:

Code: Select all

public function loadClass($className)
{
	$className = ltrim($className, $this->_namespaceSeparator);
	$match = strstr($className, $this->_namespaceSeparator, true);
		
	if(false !== $match || !isset($this->_libraries[$match]))
	{
		return false;
	}
	$rest = strrchr($className, $this->_namespaceSeparator);
	$replacement =
		str_replace($this->_namespaceSeparator, '/', substr($className, 0, strlen($className) - strlen($rest))).
		str_replace(array('_', $this->_namespaceSeparator), '/', $rest);
	require($this->_libraries[$match].$replacement.$this->_extensions[$match]);
	return true;
} // end loadClass();
Benchmark results for 10 libraries:

* Bare class loading: 0.0359044 s
* AggregateAutoloader: 0.0416718 s
* SplClassLoader: 0.0476685 s

Benchmark results for 1 library:

* Bare class loading: 0.0363744 s
* AggregateAutoloader: 0.0426363 s
* SplClassLoader: 0.0414774 s