Skip to end of metadata
Go to start of metadata

<ac:macro ac:name="toc"><ac:parameter ac:name="maxLevel">3</ac:parameter></ac:macro>
<h2>Overview</h2>

<p>The current ZF autoloader strategy is quite simple, and based on PEAR standards: each class has a 1:1 relationship to the filesystem, and classes must be found on the <code>include_path</code>, with the following autoloader implementation covering all classes:</p>

<ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
function autoload($class)
{
$filename = str_replace('_', DIRECTORY_SEPARATOR, $class) . '.php';
include $filename;
}
]]></ac:plain-text-body></ac:macro>

<p>This implementation is straight-forward. However, it has drawbacks:</p>

<ol>
<li>It is surprisingly difficult to educate users about the {{include_path|| and its proper usage.
<ol>
<li>In many ways, it would be much easier and simpler to be able to load the autoloader class by either the absolute path or a path relative to the calling script, and simply fire it off.</li>
</ol>
</li>
<li>The deeper the ZF library is within the <code>include_path</code>, the slower autoloading becomes.</li>
<li>It does not address systems where 1:1 relationships do not exist.</li>
<li>Even on systems using opcode caches, stat calls tend to pile up due to the need to search the <code>include_path</code>, making this a slow solution.</li>
<li>It does not easily address architectures where components may be installed under individual trees.</li>
</ol>

<p>I hereby propose some alternatives for ZF2.</p>

<h3>Changes to standard autoloader</h3>

<p>To address points 1, 1.a, 2, 4, and 5, some relatively simple changes may be made. </p>

<p>First, the autoloader can be altered to contain all functionality it needs to function; this will make it possible to simply require it by absolute path or a path relative to the calling script, and immediately start autoloading.</p>

<p>Second, the autoloader should have the ability to accept pairs of namespaces and paths. As an example, if <code>/opt/lib/ZendFramework-2.0.0/library/Zend</code> contains the ZF library, you would do one of the following operations:</p>

<ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
require_once
'/opt/lib/ZendFramework-2.0.0/library/Zend/Loader/StandardAutoloader.php';

/** DURING INSTANTIATION **/
$loader = new \Zend\Loader\StandardAutoloader(array(
// absolute directory
'Zend' => '/opt/lib/ZendFramework-2.0.0/library/Zend',
));

$loader = new \Zend\Loader\StandardAutoloader(array(
// relative directory
'Zend' => _DIR_ . '../lib/ZendFramework-2.0.0/library/Zend',
));

/** AFTER INSTANTIATION **/
$loader = new \Zend\Loader\StandardAutoloader();

// the path can be absolute or relative below:
$loader->registerNamespace('Zend', '/opt/lib/ZendFramework-2.0.0/library/Zend');

/** TO START AUTOLOADING */
$loader->register();
]]></ac:plain-text-body></ac:macro>

<p>These simple changes have a huge impact on ease-of-use, configurability, and performance.</p>

<p>Finally, the approach will still allow wildcard usage (i.e., specifying no prefix and/or no path). This will allow it to act as a fallback autoloader if desired. </p>

<p>This approach offers performance gains of 20% over the ZF1 autoloader with no opcode cache, and ~40% gains with opcode caching, but only when used with explicit namespace/path pairs.</p>

<ul>
<li>See the <a href="http://github.com/symfony/symfony/blob/master/src/Symfony/Framework/UniversalClassLoader.php">Symfony2 UniversalAutoloader implementation</a> for a similar example</li>
<li>See the <a href="http://gist.github.com/221634">SplClassLoader implementation</a> as another example</li>
</ul>

<h3>Class Map Autoloading</h3>

<p>A number of other projects utilize "class maps", which are simply hashes of class/file pairs. The file may either be an absolute path, or a path relative to the directory in which the map is defined.</p>

<p>Examples of such solutions include:</p>

<ul>
<li><a href="http://incubator.apache.org/zetacomponents/documentation/install">ezComponents/`ZetaComponents</a></li>
<li><a href="http://github.com/theseer/Autoload">Arne Blankert's "Autoloader" component</a></li>
</ul>

<p>The basic approach involves creating a script that performs static analysis over a tree to find classfiles and generate the classmap. From there, two approaches are possible:</p>

<ul>
<li>A self-registering class map</li>
<li>Files that simply return the map, to be consumed by an autoloader</li>
</ul>

<p>The first approach offers the easiest solution from an end-users point of view:</p>

<ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
include _DIR_ . '/../library/Zend/_autoloader.php';
// start using classes...
]]></ac:plain-text-body></ac:macro>

<p>However, it can potentially lead to a plethora of such autoloaders, which can potentially lead to slowdown by <code>spl_autoload</code>. Additionally, it doesn't allow for extension or merging classmap lists (which would allow for userland rewrites of classes).</p>

<p>The approach suggested is two-fold:</p>

<ul>
<li>A script that can create self-registering class maps</li>
<li>Another script for creating simple classmaps (as PHP arrays) that can then be used by an autoloader class</li>
</ul>

<p>The latter approach allows usage such as the following:</p>

<ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
require_once 'Zend/Loader/ClassMapAutoloader.php';

// empty instantiation
$loader = new Zend\Loader\ClassMapAutoloader();

// or with a list of classmaps:
$loader = new Zend\Loader\ClassMapAutoloader(array(
_DIR_ . '/../library/Zend/',
_DIR_ . '/../library/Phly/',
));

// Explicit location:
$loader->registerMap(_DIR_ . '/../library/Zend/.classmap.php');

// Implicit location (look for .classmap.php file in that directory)
$loader->registerMap(_DIR_ . '/../library/Zend/');

// Merge another classmap:
$loader->registerMap(_DIR_ . '/../library/Phly/Mustache/');

// Finally, register with the autoloader
$loader->register();
]]></ac:plain-text-body></ac:macro>

<p>This latter approach makes it easy to configure the autoloader with multiple classmaps.</p>

<p>A build tool would be created to allow generating class maps at build/deployment time, and this tool would be hooked into the package creation process for the project itself, so that ZF will ship with a class map by default.</p>

<p>Either approach offers performance gains of 25% over the ZF1 autoloader with no opcode cache, and 150% with an opcode cache. As such, these approaches offer the best performance possible short of preloading.</p>

<h3>Autoloader Factory</h3>

<p>With multiple autoloading strategies available, setting up and configuring autoloading becomes potentially more difficult. We propose an autoloader factory to simplify the process, and tie in with <code>Zend_Application</code>. The factory would take as configuration items:</p>

<ul>
<li>Autoloader class to use</li>
<li>Any options to pass to that autoloader</li>
</ul>

<p>As an example:</p>

<ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
require_once 'Zend/Loader/AutoloaderFactory.php';
$options = array(
'Zend\Loader\StandardAutoloader' => array(
// Fallback include_path autoloader
array('' => null),
),
'Zend\Loader\ClassMapAutoloader' => array(
// ClassMap autoloading for libraries and application
_DIR_ . '/../library/Zend/',
_DIR_ . '/../library/Phly/',
_DIR_ . '/../application/',
),
);
$loaders = Zend\Loader\AutoloaderFactory::factory($options);
]]></ac:plain-text-body></ac:macro>

<p>As you can see, it would simply return an array or object containing the various autoloaders created, and implicitly register each with <code>spl_autoload</code>.</p>

<h2>Theory of Operation</h2>

<p>The overview contains a number of implementation ideas already. In this section, we will discuss more of the nuts and bolts associated with each approach.</p>

<h3>Autoloader interface</h3>

<p>All autoloaders <strong>WILL</strong>:</p>

<ul>
<li>Take configuration options via the constructor and optionally a "configuration" method (name to be determined)</li>
<li>Provide an "autoload($class)" method, which will perform the logic necessary to attempt to load a class file</li>
<li>Supply a "register()" method which, when invoked, will register the autoloader with <code>spl_autoload_register()</code></li>
</ul>

<p>Autoloaders <strong>WILL NOT</strong>:</p>

<ul>
<li>Act as singletons</li>
</ul>

<h4>Proposed Interface</h4>

<ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
namespace Zend\Loader;

interface Autoloadable
{
public function __construct($options = null);

public function setOptions($options);

public function autoload($class);

public function register();
}
]]></ac:plain-text-body></ac:macro>

<h3>Standard Autoloader</h3>

<p>The standard autoloader <strong>WILL</strong>:</p>

<ul>
<li>Autoload PSR-0-compliant classes</li>
<li>Allow specifying Namespace/Path pairs; when specified, it will look in the provided path, and only that path, to load any class with that namespace. Multiple pairs may be provided.</li>
<li>Allow specifying Vendor Prefix/Path pairs; these will work just like Namespace/Path pairs, but will check for vendor prefixes instead.</li>
<li>Allow specifying either a namespace or vendor prefix <strong>without</strong> a path; in this case, the <code>include_path</code> will be used.</li>
<li>Allow specifying no namespace with a path; in this case, it will act as a fallback autoloader, searching for classfiles on the given path.</li>
<li>Allow specifying no namespace and no path; in this case, it will act as a fallback autoloader, searching on the <code>include_path</code>.</li>
</ul>

<p>The standard autoloader <strong>WILL NOT</strong>:</p>

<ul>
<li>Act as a fallback autoloader by default. You will need to specifically configure it as such.</li>
</ul>

<h4>Proposed Implementation</h4>

<ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
namespace Zend\Loader;

require_once _DIR_ . '/Autoloadable.php';

class StandardAutoloader implements Autoloadable
{
public function __construct($options = null);
public function setOptions($options);
public function registerNamespace($namespace, $path = '');
public function registerPrefix($prefix, $path = '');
public function autoload($class);
public function register($prefix, $path = '');
}
]]></ac:plain-text-body></ac:macro>

<ul>
<li>See the <a href="https://github.com/weierophinney/zf2/blob/autoloading/library/Zend/Loader/StandardAutoloader.php">PSR-0 autoloader implementation</a> on github as an example.</li>
</ul>

<h3>Class Map Autoloader</h3>

<p>The classmap autoloader <strong>WILL</strong>:</p>

<ul>
<li>Allow registering <strong>EITHER</strong> files returning class maps <strong>OR</strong> array class maps.</li>
<li>Registering multiple class maps will merge them; class maps registered later will override those previously merged when conflicts occur.</li>
<li>Class map definitions <strong>MUST</strong> define the file to use an absolute path; by preference, <code><em>DIR</em></code> will be used for portability.</li>
</ul>

<h4>Proposed Implementation</h4>

<ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
namespace Zend\Loader;

require_once _DIR_ . '/Autoloadable.php';

class ClassMapAutoloader implements Autoloadable
{
public function __construct($options = null);
public function setOptions($options);
public function registerMap($map);
public function autoload($class);
public function register($prefix, $path = '');
}
]]></ac:plain-text-body></ac:macro>

<ul>
<li>See the <a href="http://github.com/weierophinney/zf2/blob/autoloading/library/Zend/Loader/ClassMapAutoloader.php">class mapautoloader implementation</a> on github as an example.</li>
</ul>

<h4>Additional Tools</h4>

<p>Additionally, with the classmap autoloader, two tools will be provided:</p>

<ol>
<li>A tool for generating the classmaps. By default, this will create files named ".classmap.php", using <code><em>DIR</em></code> to prefix the paths.</li>
<li>A tool for generating self-registering class maps. By default, this will create a file named "_autoload.php", using the namespace in the current directory (or provided by a switch). Within the file, it will register a closure with <code>spl_autoload_register()</code>, and use the class map within it to perform lookups.</li>
</ol>

<p>These tools may be provided either as <code>Zend_Tool</code> providers, or as standalone scripts.</p>

<ul>
<li>See the <a href="http://github.com/weierophinney/zf2/blob/autoloading/library/Zend/File/ClassFileLocater.php">class file locater implementation</a> on github for an example of how to find class files.</li>
<li>See the <a href="http://github.com/weierophinney/zf2/blob/autoloading/bin/zfal.php">class map generator</a> on github for an example of a script for generating class files.</li>
<li>See the <a href="http://github.com/weierophinney/zf2/blob/autoloading/bin/zfals.php">autoloader generator</a> on github for an example of a script for generating self-registering class map autoloaders.</li>
</ul>

<h3>Autoloader Factory</h3>

<p>The autoloader factory <strong>WILL</strong>:</p>

<ul>
<li>Allow specifying autoloader class/option pairs to configure and register</li>
<li>Allow specifying the class file where the autoloader class may be found</li>
<li>Register all configured autoloaders with <code>spl_autoload</code></li>
<li>Return a list of autoloader class/instance pairs</li>
</ul>

<h4>Proposed Implementation</h4>

<ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
namespace Zend\Loader;

class AutoloaderFactory
{
public static method factory($options);
}
]]></ac:plain-text-body></ac:macro>

Labels:
None
Enter labels to add to this page:
Please wait 
Looking for a label? Just start typing.
  1. Sep 13, 2010

    <p>The include file "_autoload.php" registers a closure directly with spl_autoload_register but after this it's very hard to unregister this clusure if you have more closures registered this way.</p>

    <p>-> Is it possible to return the closure?</p>
    <ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
    // _autoload.php
    $_closure = function($class) use ($_map)
    // ...
    };
    if (!spl_autoload_register($_closure)) {
    return false; // or exception
    }
    return $_closure;

    // application
    $closure = include $includeFile; // register autoload
    spl_autoload_unregister($closure); // unregister autoload
    ]]></ac:plain-text-body></ac:macro>

    1. Sep 13, 2010

      <p>Absolutely, it'd be possible to return the closure. The question is: why?</p>

      <p>Yes, I understand the only way to unregister a callback from the spl_autoload registry is by handle – but how often do you really do this in PHP? The only time I've done so myself is unit testing – and then only to test autoloaders. I'm thinking the <a href="http://c2.com/cgi/wiki?YouArentGonnaNeedIt">YAGNI</a> principle applies here.</p>

      1. Sep 14, 2010

        <p>I looked for the following:</p>

        <p>The script generating the autoloader file is designed for ZF but if I understand it right it's usable on other libraries/components. There it could be that you have a big library with hundreds of class files but you only need a small extract on a specific manner. If you can unregister the big library the memory for a big $map-array can be free.</p>

        1. Sep 14, 2010

          <p>First: yes, you can use the utility on any library. It simply does static analysis, looking for classes and interfaces under that tree in order to generate the map.</p>

          <p>Based on your scenario, though, I'd argue that you should create autoloaders or class maps for the invididual components in the library you're pulling from, instead of using one that pulls in the entire library's map.</p>

  2. Sep 14, 2010

    <p>One small comment, since I love the rest. The Autoloader factory contains:</p>

    <ac:macro ac:name="code"><ac:plain-text-body><![CDATA[$options = array(
    'Zend\Loader\StandardAutoloader' => array(
    // Fallback include_path autoloader
    array('' => null),
    ),]]></ac:plain-text-body></ac:macro>

    <p>It's early days, but will the all-empty array reference be strictly required? It's loose wiring type stuff like this that can cause moments of API confusion.</p>

    1. Sep 14, 2010

      <p>Yes, that can likely be omitted.</p>

  3. Sep 14, 2010

    <ac:macro ac:name="note"><ac:rich-text-body><p><strong>Community Review Team Recommendation</strong></p>

    <p>The CR-Team recommends accepting this proposal as-is.</p>
    </ac:rich-text-body></ac:macro>

  4. May 13, 2011

    <p>How about allowing the standard autoloader to cache file vs. class mappings as they are resolved? </p>

    <p>Depending on the cache performance this would possibly make it as fast as the class map autoloader when a previously used class is loaded. </p>

    <p>It would, like the Zend_Db_Table metadata cache, require that the cache is cleared when code is updated. It could be used without caching while developing and with cachen when in production.</p>