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>Plugin loading is an important aspect of Zend Framework. One key way to extend and expand the capabilities of many Zend Framework components is via plugins – be they adapters, helpers, filters, validators, decorators, etc. These plugins typically utilize <code>Zend_Loader_PluginLoader</code> to resolve plugin names to actual classes (and the associated class file). </p>

<p>There are a number of problems related to plugin loading, however:</p>

<ul>
<li>Not all components offering plugin capabilities utilize the plugin loader. This leads to inconsistent APIs, and added maintenance.</li>
<li>The current solution relies heavily on class prefix/path pairs, and utilizes path stacks. While this offers extensive flexibility, it introduces several issues:
<ul>
<li>It's relatively difficult to educate users about how the plugin loader works, particularly when the same prefix is used with multiple paths.</li>
<li>Users are torn about what the correct behavior should be when the same path is added multiple times, for a given prefix.</li>
<li>The solution requires many stat calls (basically, <code>is_readable()</code> calls).</li>
<li>For each prefix, it needs to loop through each path, until it finds a matching plugin. Matching immediately is a best-case scenario. Even then, the solution does not make good use of the realpath cache or opcode caching. As such, the current solution is an enormous performance bottleneck within the framework.</li>
</ul>
</li>
<li>Classes using the plugin loader each have to build functionality for registering and caching loaded plugins themselves, since the plugin loader actually only locates and loads the class itself. This leads to much code duplication, as well as increased maintenance.</li>
<li>One persistent issue is case sensitivity; if a plugin name is referenced using incorrect case:
<ul>
<li>If the class managing plugins does so in a case sensitive manner, it simply can't load the plugin, but the error message is ambiguous: does the plugin not exist, or was it simply mis-spelled?</li>
<li>Other classes managing plugins are not case sensitive... making the above error even harder to diagnose, if you are not aware whether the current component is case sensitive or not.</li>
</ul>
</li>
</ul>

<p>When profiling a Zend Framework application, we've determined that under a typical application, sometimes upwards of 40% of execution time is spent resolving and loading plugins. A key priority of ZF2 must be to simplify and optimize plugin loading. </p>

<h2>Theory of Operation</h2>

<h3>Interfaces for plugin loading</h3>

<p>When it comes down to brass tacks, classes <em>using</em> plugin loading really only use a handful of methods from a plugin loader:</p>

<ul>
<li><code>load($plugin)</code></li>
<li><code>isLoaded($plugin)</code></li>
<li><code>getClassName($plugin)</code></li>
</ul>

<p>The details of how a plugin loader is configured are relevant only to the code configuring plugin loading, not the object consuming it. As such, we recommend creating a "<code>ShortNameLocater</code>" interface with the above methods, which will be consumed by objects needing to load plugin classes.</p>

<p>Additional interfaces can be created to describe specific capabilities of given plugin loaders – examples include "<code>PrefixPathMapper</code>", "<code>PluginClassMapper</code>", etc. </p>

<h3>Alias Autoloading</h3>

<p>The single fastest approach to plugin loading is to use an aliasing plugin loader. Similar to the class map autoloader proposal, this establishes a short name "alias" that points to the class it represents. This provides several key benefits:</p>

<ul>
<li>Simpler to explain.</li>
<li>Makes it possible to easily make plugin names case insensitive, leading to easier normalization and documentation.</li>
<li>Simpler to track when and where a "plugin" is pointed to a new class.</li>
<li>Can benefit from autoloading improvements:
<ul>
<li>No stat calls <strong>ever</strong> if the class is found in the opcode cache!</li>
</ul>
</li>
<li>Simpler execution path.
<ul>
<li>The current plugin loader takes around 15 steps before it is able to load a class, and spends a plurality of execution time in the plugin loader and autoloader.</li>
<li>Using an aliasing plugin loader and class map autoloader showed results in only 4 steps to load a class, and a minority of execution time spent resolving classes.</li>
<li>Performance gains of up to 5x current plugin loading strategies!</li>
</ul>
</li>
</ul>

<p>Additionally, it allows us to pre-generate explicit plugin maps, and for end users to create their own and/or extend existing plugin maps. This makes the code more explicit, as well as capable of benefiting from static analysis and opcode caching.</p>

<p>We recommend creating a "<code>PluginClassMapper</code> interface with the following definition:</p>

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

interface PluginClassMapper
{
public function registerPlugin($shortName, $className);
public function unregisterPlugin($shortName);
public function getRegisteredPlugins();
}
]]></ac:plain-text-body></ac:macro>

<p>A class <code>PluginClassLoader</code> would implement both this interface, as well as <code>ShortNameLocater</code>. Internally, plugin "short names" will be normalized to lowercase. </p>

<h4>Static Loader Registry</h4>

<p>In many instances, a developer may want to provide global overrides for short name resolution. As an example, she may desire to override the "url" view helper with her own class throughout the framework. </p>

<p>As an alternative to defining a one-off class extension for this, one potential way this could be done is through a static registry. Its operation might resemble this:</p>

<ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
// Single registration:
Zend\View\HelperLoader::registerGlobalPlugin('url', 'Foo\Helper\Url');

// Multiple registration:
Zend\View\HelperLoader::registerGlobalPlugins(array(
'url' => 'Foo\Helper\Url',
));
]]></ac:plain-text-body></ac:macro>

<p>Any plugins registered this way would override those defined in the given plugin class loader, but not override any specified via the constructor, or via registerPlugin().</p>

<h3>Plugin Broker</h3>

<p>To consolidate logic around not only plugin class loading, but instantiation and registry, we recommend creating separate capabilities within a plugin broker. The plugin broker will be responsible for:</p>

<ul>
<li>Loading plugin classes (via an attached plugin loader)</li>
<li>Instantiation of plugin, with provided options</li>
<li>Maintaining a registry of loaded and instantiated plugins</li>
</ul>

<p>We recommend creating both a "<code>Broker</code>" interface, and a generic "<code>PluginBroker</code>" class. The latter may be extended to provide lazy-loading options for the plugin loader and registry. This will allow us to provide pre-seeded plugin brokers for each component.</p>

<p><code>Broker</code> will be defined similar to the following:</p>

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

interface Broker
{
public function load($plugin, array $options = null);
public function register($name, $plugin);
public function unregister($name);
public function setRegistry(\Zend\Registry $registry);
public function getRegistry();
public function setClassLoader(ShortNameLocater $loader);
public function getClassLoader();
}
]]></ac:plain-text-body></ac:macro>

<p>Potentially, we may allow attaching multiple <code>ShortNameLocater</code> instances in a stack within the Broker. This would allow mixing and matching approaches for plugin class resolution.</p>

<h3>Prefix Path Loader</h3>

<p>The current <code>PluginLoader</code> would be renamed to <code>PrefixPathLoader</code>, and will implement both <code>ShortNameLocater</code> as well as a new interface, <code>PrefixPathMapper</code>. Internally, it will be modified to utilize <code>SplStack</code> for the path lists, and will internalize logic from the current <code>Zend_Loader::loadFile()</code> and <code>Zend_Loader::isReadable()</code>. </p>

<p>Within ZF2, however, it will no longer be used, and will only be documented as a plugin loading option within the manual.</p>

<h2>Example Use Cases</h2>

<p>An example has been created in my <a href="http://github.com/weierophinney/zf2/tree/pluginloading">pluginloading branch on github</a>, under <code>library/Zend/View/HelperBroker.php</code>. Usage would be similar to the following:</p>

<ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
$broker = new Zend\View\HelperBroker();
$loader = $broker->getPluginLoader();
$loader->register('foo', 'My\Component\Foo');
$loader->register('url', 'My\Component\Url');

$fooHelper = $broker->load('foo'); // instance of My\Component\Foo
$formHelper = $broker->load('form'); // instance of Zend\View\Helper\Form
$urlHelper = $broker->load('url'); // instance of My\Component\Url
]]></ac:plain-text-body></ac:macro>

<p>In the above, the <code>url</code> helper is originally registered to <code>Zend\View\Helper\Url</code>; however, the user overrides this by registering a different class for that helper name.</p>

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

    <p>5x performance increase sounds exciting!<br />
    Will PrefixPathLoader see any performance increases as well?</p>

    1. Sep 17, 2010

      <p>We will likely add some caching abilities to the prefix-path-based loader to ensure that plugins already requested do not need to be resolved again. However, even this is problematic, as new paths may be added at any time, which would invalidate the cache. As such, I wouldn't expect to see too many improvements to performance in it.</p>

  2. Sep 17, 2010

    <p>Statically override aliases is very nice, additionally i think we could do more! <ac:emoticon ac:name="wink" /></p>

    <p>Like in current zend_cache_manager zf1, the new zend\loader could provide a manager class (not abstract) and handle instances of them for others components, like zend_registry. My goal is avoid coding the same code every time we have to manage plugins (like nowadays when using zend_config).</p>

    <p>Once a component has a plugin loader registered, users can request for plugin register from zend\loader and not from its component. Maybe the external component could provide a key, like we do in bootstrap when registering a item in zend_registry.</p>

    1. Sep 24, 2010

      <p>What you are talking about is very close of a container as in the inversion of control approach which is, i think, a different matter than the loader strategy.</p>

      <p>But talking of that, I think it would be great to abolish the use of the registry and natively provide a lightweight and flexible dependency container used by the core of the framework.</p>

      <p>A container is used internally to dynamically instanciate dependancies at runtime but also allows the developper to access statically to any instance at any time like you described it. And if the framework himself is based on it and uses that mecanism, it's very powerful and very flexible.</p>

      1. Sep 28, 2010

        <p>A container instance (like a Factory) for each set of related plugins would be ideal.<br />
        For example, currently there are view helpers like the Url one which require the injection of the router (actually until now they grab it from the Front Controller Singleton instance). Allowing for plugin with a non-empty constructor is crucial.</p>

        1. Sep 28, 2010

          <p>Please look at the examples in my pluginloading branch. The PluginBroker is a container and factory, and allows passing zero or more arguments to the constructor of each object.</p>

          <p>Additionally, in the case of view helpers, the idea is that you would get the instance from the broker – and then you could configure it however you desire:</p>

          <ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
          $urlHelper = $broker->load('url');
          $urlHelper->setRouter($router);
          echo $urlHelper->simple('baz', 'bar', 'foo');

          // OR:
          $urlHelper = $broker->load('url', array($router));
          echo $urlHelper->simple('baz', 'bar', 'foo');
          ]]></ac:plain-text-body></ac:macro>

          <p>While the above code looks icky in terms of usage in a view script, the configuration portion is the important part – the controller can inject dependencies into helpers prior to passing to the view. This is actually quite clean, easy to debug and trace, easy to learn, and more flexible than the current solution.</p>