Table of Contents

Request for Comments: SplClassLoader

Introduction

PHP 5 introduced the concept of autoloading classes.

However, many different projects adopted their own approach for class loader. In the mid of 2009, several lead developers started an initiative called PHP Standards Group (aka. Framework Interoperability Group), which is mainly focused on interoperability between different Open Source projects.

After an extensive discussions, group came with the primary interoperability between them, which is formally named as PHP Standards Recommendation #0 (or PSR-0).

PSR-0 focus on a flexible autoloader that could be used or shared between projects, structuring and organizing Object Oriented code. So any project that decides to be PSR-0 compliant just need to follow the defines rules defined in the next topic.

Rules

The following rules must be implemented in order to make a project be compliant to PSR-0:

Extended Rules for Proposal

Together with PSR-0 rules, for SplClassLoader we would also include a few valid rules that must be implemented, that addicts to original PSR-0 rules, not affecting any of the projects participants, but also improving the flexibility of autoloader:

Flexibility of a Rule

Even though PSR-0 is strict, PHP cannot force php extension. Ideally, the file extension is customizable, but default approach would be the defined one in PSR-0.

Initial missing support

PHP does not provide a single approach for any OO based autoloader, so initially a new interface is required, for anyone interested to implement their own Autoloader. From now on, let's name it as SplAutoloader. The purpose of this interface is to bring the minimum contract that any interested to autoload their resources should follow. Here is the proposed implementation:

/**
 * SplAutoloader defines the contract that any OO based autoloader must follow.
 *
 * @author Guilherme Blanco <guilhermeblanco@php.net>
 */
interface SplAutoloader
{
    /**
     * Defines autoloader to work silently if resource is not found.
     *
     * @const
     */
    const MODE_SILENT = 0;
 
    /**
     * Defines autoloader to work normally (requiring an un-existent resource).
     *
     * @const
     */
    const MODE_NORMAL = 1;
 
    /**
     * Defines autoloader to work in debug mode, loading file and validating requested resource.
     *
     * @const
     */
    const MODE_DEBUG = 2;
 
    /**
     * Define the autoloader work mode.
     *
     * @param integer $mode Autoloader work mode.
     */
    public function setMode($mode);
 
    /**
     * Add a new resource lookup path.
     *
     * @param string $resourceName Resource name, namespace or prefix.
     * @param mixed $resourcePath Resource single path or multiple paths (array).
     */
    public function add($resourceName, $resourcePath = null);
 
    /**
     * Load a resource through provided resource name.
     *
     * @param string $resourceName Resource name.
     */
    public function load($resourceName);
 
    /**
     * Register this as an autoloader instance.
     *
     * @param boolean Whether to prepend the autoloader or not in autoloader's list.
     */
    public function register($prepend = false);
 
    /**
     * Unregister this autoloader instance.
     *
     */
    public function unregister();
}

Examples of Namespace Resolution

The standards we set here should be the lowest common denominator for painless autoloader interoperability. You can test that you are following these standards by utilizing this sample SplClassLoader implementation which is able to load PHP 5.3 classes.

Examples of usage

Autoloading Doctrine 2:

$classLoader = new \SplClassLoader(); // $mode is "normal"
$classLoader->add('Doctrine', array(
    '/path/to/doctrine-common', '/path/to/doctrine-dbal', '/path/to/doctrine-orm'
));
$classLoader->register(true); // Autoloader is prepended

Autoloading PEAR1:

$classLoader = new \SplClassLoader();
$classLoader->setMode(\SplClassLoader::MODE_SILENT);
$classLoader->setIncludePathLookup(true);
$classLoader->add('PEAR');
$classLoader->register();

Autoloading in debug mode:

$classLoader = new \SplClassLoader();
$classLoader->setMode(\SplClassLoader::MODE_NORMAL | \SplClassLoader::MODE_DEBUG);
$classLoader->add('Symfony', '/path/to/symfony');
$classLoader->add('Zend', '/path/to/zf');
$classLoader->register();

Example of simplest implementation

Below is an example function to simply demonstrate how the above proposed standards are autoloaded.

function autoload($className)
{
    $className = ltrim($className, '\\');
    $fileName  = '';
    $namespace = '';
 
    if ($lastNsPos = strripos($className, '\\')) {
        $namespace = substr($className, 0, $lastNsPos);
        $className = substr($className, $lastNsPos + 1);
        $fileName  = str_replace('\\', DIRECTORY_SEPARATOR, $namespace) . DIRECTORY_SEPARATOR;
    }
 
    $fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . '.php';
 
    require $fileName;
}

SplClassLoader implementation

The following class is a sample SplClassLoader implementation that can load your classes if you follow the autoloader interoperability standards proposed above. It is the current recommended way to load PHP 5.3 classes that follow these standards.

/**
 * SplClassLoader implementation that implements the technical interoperability
 * standards for PHP 5.3 namespaces and class names.
 *
 * https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md
 *
 * Example usage:
 *
 *     $classLoader = new \SplClassLoader();
 *
 *     // Configure the SplClassLoader to act normally or silently
 *     $classLoader->setMode(\SplClassLoader::MODE_NORMAL);
 *
 *     // Add a namespace of classes
 *     $classLoader->add('Doctrine', array(
 *         '/path/to/doctrine-common', '/path/to/doctrine-dbal', '/path/to/doctrine-orm'
 *     ));
 *
 *     // Add a prefix
 *     $classLoader->add('Swift', '/path/to/swift');
 *
 *     // Add a prefix through PEAR1 convention, requiring include_path lookup
 *     $classLoader->add('PEAR');
 *
 *     // Allow to PHP use the include_path for file path lookup
 *     $classLoader->setIncludePathLookup(true);
 *
 *     // Possibility to change the default php file extension
 *     $classLoader->setFileExtension('.php');
 *
 *     // Register the autoloader, prepending it in the stack
 *     $classLoader->register(true);
 *
 * @author Guilherme Blanco <guilhermeblanco@php.net>
 * @author Jonathan H. Wage <jonwage@gmail.com>
 * @author Roman S. Borschel <roman@code-factory.org>
 * @author Matthew Weier O'Phinney <matthew@zend.com>
 * @author Kris Wallsmith <kris.wallsmith@gmail.com>
 * @author Fabien Potencier <fabien.potencier@symfony-project.org>
 */
class SplClassLoader implements SplAutoloader
{
    /**
     * @var string
     */
    private $fileExtension = '.php';
 
    /**
     * @var boolean
     */
    private $includePathLookup = false;
 
    /**
     * @var array
     */
    private $resources = array();
 
    /**
     * @var integer
     */
    private $mode = self::MODE_NORMAL;
 
    /**
     * {@inheritdoc}
     */
    public function setMode($mode)
    {
    	if ($mode & self::MODE_SILENT && $mode & self::MODE_NORMAL) {
    	    throw new \InvalidArgumentException(
    	        sprintf('Cannot have %s working normally and silently at the same time!', __CLASS__)
    	    );
    	}
 
        $this->mode = $mode;
    }
 
    /**
     * Define the file extension of resource files in the path of this class loader.
     *
     * @param string $fileExtension
     */
    public function setFileExtension($fileExtension)
    {
        $this->fileExtension = $fileExtension;
    }
 
    /**
     * Retrieve the file extension of resource files in the path of this class loader.
     *
     * @return string
     */
    public function getFileExtension()
    {
        return $this->fileExtension;
    }
 
    /**
     * Turns on searching the include for class files. Allows easy loading installed PEAR packages.
     *
     * @param boolean $includePathLookup
     */
    public function setIncludePathLookup($includePathLookup)
    {
        $this->includePathLookup = $includePathLookup;
    }
 
    /**
     * Gets the base include path for all class files in the namespace of this class loader.
     *
     * @return boolean
     */
    public function getIncludePathLookup()
    {
        return $this->includePathLookup;
    }
 
    /**
     * {@inheritdoc}
     */
    public function register($prepend = false)
    {
        spl_autoload_register(array($this, 'load'), true, $prepend);
    }
 
    /**
     * {@inheritdoc}
     */
    public function unregister()
    {
        spl_autoload_unregister(array($this, 'load'));
    }
 
    /**
     * {@inheritdoc}
     */
    public function add($resource, $resourcePath = null)
    {
        $this->resources[$resource] = (array) $resourcePath;
    }
 
    /**
     * {@inheritdoc}
     */
    public function load($resourceName)
    {
        $resourceAbsolutePath = $this->getResourceAbsolutePath($resourceName);
 
        switch (true) {
            case ($this->mode & self::MODE_SILENT):
                if ($resourceAbsolutePath !== false) {
                    require $resourceAbsolutePath;
                }
                break;
 
            case ($this->mode & self::MODE_NORMAL):
            default:
                require $resourceAbsolutePath;
                break;
        }
 
        if ($this->mode & self::MODE_DEBUG && ! $this->isResourceDeclared($resourceName)) {
            throw new \RuntimeException(
                sprintf('Autoloader expected resource "%s" to be declared in file "%s".', $resourceName, $resourceAbsolutePath)
            );
        }
    }
 
    /**
     * Transform resource name into its absolute resource path representation.
     *
     * @params string $resourceName
     *
     * @return string Resource absolute path.
     */
    private function getResourceAbsolutePath($resourceName)
    {
        $resourceRelativePath = $this->getResourceRelativePath($resourceName);
 
        foreach ($this->resources as $resource => $resourcesPath) {
            if (strpos($resourceName, $resource) !== 0) {
                continue;
            }
 
            foreach ($resourcesPath as $resourcePath) {
                $resourceAbsolutePath = $resourcePath . DIRECTORY_SEPARATOR . $resourceRelativePath;
 
                if (is_file($resourceAbsolutePath)) {
                    return $resourceAbsolutePath;
                }
            }
        }
 
        if ($this->includePathLookup && ($resourceAbsolutePath = stream_resolve_include_path($resourceRelativePath)) !== false) {
            return $resourceAbsolutePath;
        }
 
        return false;
    }
 
    /**
     * Transform resource name into its relative resource path representation.
     *
     * @params string $resourceName
     *
     * @return string Resource relative path.
     */
    private function getResourceRelativePath($resourceName)
    {
        // We always work with FQCN in this context
        $resourceName = ltrim($resourceName, '\\');
        $resourcePath = '';
 
        if (($lastNamespacePosition = strrpos($resourceName, '\\')) !== false) {
            // Namespaced resource name
            $resourceNamespace = substr($resourceName, 0, $lastNamespacePosition);
            $resourceName      = substr($resourceName, $lastNamespacePosition + 1);
            $resourcePath      =  str_replace('\\', DIRECTORY_SEPARATOR, $resourceNamespace) . DIRECTORY_SEPARATOR;
        }
 
        return $resourcePath . str_replace('_', DIRECTORY_SEPARATOR . $resourceName) . $this->fileExtension;
    }
 
    /**
     * Check if resource is declared in user space.
     *
     * @params string $resourceName
     *
     * @return boolean
     */
    private function isResourceDeclared($resourceName)
    {
    	return class_exists($resourceName, false) 
    	    || interface_exists($resourceName, false) 
    	    || (function_exists('trait_exists') && trait_exists($resourceName, false));
    }
}

If any interested wants to customize the public methods, like caching through APC to reduce I/O, it should be possible to extend SplClassLoader and overwrite the public methods.

Proposal and Patch

The final release version of PSR-0 is available at: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md

A C extension is already available for usage, and can be grabbed at http://github.com/metagoto/splclassloader

An extension to SPL has been created from the original C extension and a feature request has been filled for documentation purposes. The new SPL extension preliminary patch can be found at: https://gist.github.com/1310352. The provided patch is a minimum working version of SplClassLoader, so it may still require some updates to address minimum issues highlighted after a deep code review.

Main purpose of this proposal is to support both PEAR style directory organization and also Namespace directory organization.

Changelog

Comments