Skip to end of metadata
Go to start of metadata

<h2>History and issues</h2>
<p>There are three basic types of plugin architectures in ZF</p>
<ul>
<li><strong>Chains</strong> – typically, you attach one or more "plugins" to an object, and at specified "hooks", each plugin is run. This is really simply a pubsub system. Examples include validators, filters, decorators, etc.</li>
<li><strong>Helpers</strong> – basically, you attach one or more plugins to an object, and then that object calls these on-demand, by name. Examples are action and view helpers. These typically follow the strategy pattern when invoked.</li>
<li><strong>Adapters</strong> – in this case, plugins are specified by name or configuration, and a builder then instantiates the given adapter with the given options. Examples include form display groups, subforms, and elements; database adapters; translation adapters; etc.</li>
</ul>

<p>In many cases, there are combinations of patterns. For instance, with validators in form elements, we use a builder factory to create the appropriate validator object, and it is then attached to a chain. Similarly, with helpers, a "short" name is resolved to a fully qualified classname, an object instantiated, and then the appropriate method invoked.</p>

<p>Unfortunately, we have a few hurdles and issues with these, particularly when it comes to consistency.</p>
<ul>
<li>While many components use the PluginLoader to resolve short names to class names, there are case inconsistencies that occur between components.</li>
<li>The various helpers do not follow the same paradigms when it comes to the strategy pattern: action helpers use a "direct" method; view helpers use a method named after the helper class's short name; validators use "isValid()", filters use "filter()".</li>
<li>The PluginLoader is incredibly inefficient, as it requires <code>Zend_Loader::isReadable()</code> lookups for each prefix path registered – this is particularly noticeable when you have multiple paths, and the match exists several paths into the stack.</li>
<li>No common paradigm for adapter constructors and/or configuration. This is the primary use case for a "unified constructor": to make adapters consistent across components.</li>
<li>No common paradigm for passing information to factories.</li>
</ul>

<h2>Recommendations</h2>
<h3>Plugin name resolution</h3>
<ul>
<li>Use namespaces only. Plugin lookups should only consider the namespace, and attempt to autoload from those namespaces. E.g.:
<ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
$loader->registerNamespace('my\validators');
$class = $loader->load('foo'); // my\validators\Foo
]]></ac:plain-text-body></ac:macro>
<ul>
<li>Use un/registerNamespace() instead of addPrefixPath()</li>
<li>Use class_exists() only; no actual lookups</li>
</ul>
</li>
<li>Use lowercase-dash-separated-names. Plugin lookups should translate such names to MixedCase. E.g.:
<ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
$loader->registerNamespace('my\validators');
$class = $loader->load('foo-bar'); // my\validators\FooBar
]]></ac:plain-text-body></ac:macro></li>
<li>Plugin lookups should ignore underscores. If an underscore is found in the "short name" provided, it will be ignored; resolution to subdirectories will not be allowed.</li>
</ul>

<h3>Use __invoke() for helpers</h3>
<ul>
<li>Helpers should use <code>__invoke()</code> in all cases. This makes usage of helpers consistent, as well as simple:
<ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
$helper = new \my\foo\Helper();
echo $helper();
]]></ac:plain-text-body></ac:macro></li>
<li>In cases where there is an existing interface, or where defining a method * makes better semantic sense, <code>__invoke()</code> should proxy to that method:
<ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
interface Validator
{
public function isValid($value, $context = null);
public function __invoke($value, $context = null);
}

class FooValidator implements Validator
{
public function isValid($value, $context = null)

Unknown macro: { // ... }

public function __invoke($value, $context = null)

Unknown macro: { return $this->isValid($value, $context); }

}
]]></ac:plain-text-body></ac:macro></li>
</ul>

<h3>"Configurable" interface for all adapters</h3>
<ul>
<li>All adapters should implement a "Configurable" interface, which allows passing<br />
configuration options.
<ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
interface ConfigurableInterface
{
public function setOptions($options);
}

class Adapter implements ConfigurableInterface
{
public function setOptions($options)

Unknown macro: { // ... set object state ... }

}
]]></ac:plain-text-body></ac:macro></li>
<li>Factories will instantiate an adapter, and then pass it options.
<ul>
<li>As such, the adapter constructor typically should not be defined.</li>
<li>All factories should accept options as either an array or Config object; the options should include a "type" key and a "params" key; the former will be the adapter type, the latter the options with which to configure it.
<ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
class ValidatorFactory
{
public static function factory($options)
{
if ($options instance of \zend\Config)

Unknown macro: { $options = $options->toArray(); }


if (!is_array($options))

Unknown macro: { throw new InvalidArgumentException(); }

$adapter = new $options['type'];
$adapter->setOptions($options['params']);
return $adapter;
}
}
]]></ac:plain-text-body></ac:macro></li>
<li>In most cases, direct instantiation of the adapter should be allowed.</li>
</ul>
</li>
<li>Adapter factories should have an attached plugin loader, and allow attaching a different one, for purposes of plugin resolution.</li>
</ul>

<h3>Chains</h3>
<ul>
<li><a href="http://github.com/weierophinney/phly/tree/HEAD/Phly_PubSub/">Reference implementation</a></li>
<li>All chains should extend one of the classes in the phly\pubsub component (to be renamed and added to ZF). These chains include:
<ul>
<li>Provider: subscribe to one or more topics and either notify all subscribers or filter a value through all subscribers to a topic</li>
<li>FilterChain: like Provider, but no topics; good for classes that have a single plugin flex-point.</li>
</ul>
</li>
<li>Build default chains where applicable: form decorators, etc.</li>
<li>Allow extending chains and attaching subclasses to objects consuming them
<ul>
<li>Corollary: do not proxy to chains, but instead simply attach chains</li>
</ul>
</li>
<li>Chains should act as plugin loaders or attach plugin loaders to ensure name resolution works consistently.</li>
</ul>

Labels:
None
Enter labels to add to this page:
Please wait 
Looking for a label? Just start typing.
  1. Nov 18, 2009

    <ac:macro ac:name="unmigrated-wiki-markup"><ac:plain-text-body><![CDATA[The interesting part is how do we get the collaborators to plugins: the unified constructor or setOptions() works but someone has to pass. This cannot be done at construction time.
    Imho if an Abstract Factory is used it should contain all the collaborators requested by helpers as user of the helpers does not care about what an helper need: they only want to use it. Configuration is not only strings: for instance the Url view helper needs a reference to the Router: http://giorgiosironi.blogspot.com/2009/11/how-to-eliminate-singletons-part-2.html
    Since providing factories for all the different use cases and for user defined plugins, a simple DI container is needed. Just a PluginFactory (or new version of the PluginLoader):
    class PluginFactory
    {
    public function __construct()

    Unknown macro: { //...unified constructor }

    public function setConfig(array|Zend_Config $config)
    {
    }
    public function setInjector(Injector $injector)
    {
    }
    public function setPluginLoader(PluginLoader $
    public function __get($pluginName)
    {
    if (!$this->_injectorReady)

    Unknown macro: { $this->_injector->addConfig($config) // only the first time $this->_injectorReady = true; }

    $className = $this->_pluginLoader->load($pluginName);
    return $this->_injector->newInstance($className);
    }
    }
    The Injector instance should be a copy of the main Injector which has set up the application object graph (creating the router, the fsm, etc.).]]></ac:plain-text-body></ac:macro>

    1. Nov 18, 2009

      <p>Some aspects of DI will not be an issue if the proposed MVC is adopted.</p>

      <p>The View object will receive the Event object – which will have access to anything registered via the bootstrap. This will include the EventManager, Router, and Dispatcher. As such, if the view object simply injects itself and/or the Event into its helpers, service location will be fait accompli.</p>

  2. Feb 18, 2010

    <p>What are the semantics of filter chains? For example, are they ordered? Would all fail if one threw an exception, or would the filter chain continue with all other filters succeeding, or- it they are ordered- would only those filters that preceded the failing filter succeed?</p>

    <p>,Wil</p>

    1. Feb 18, 2010

      <p>Yes, filter chains are ordered, and act as a FIFO stack.</p>

      <p>If a particular filter throws an exception, the current implementation does not handle it; it's up to the code invoking the filter chain to handle exceptions (or not). Typically, your filters should not throw exceptions, and if they do, I would consider this, well, exceptional behavior. <ac:emoticon ac:name="wink" /></p>

      <p>As for filter failing, there can be several approaches, and all are valid depending on the intended use case. Validation often will require a "break on failure" flag, which means that as soon as one fails, any remaining filters are skipped. A typical filter chain, on the other hand, passes the return value of one filter directly to the next in the chain – and as a result, there's really no concept of a "failure" per se; so long as no exception is thrown, all subscribers are run.</p>

      <p>One way to do a break on failure is with a "filterUntil" or "filterUnless" method, which checks for specific types of return values, and stops once such a value is found. My current implementation has a "publishUntil" method that parallels the "publish" method, and this is available both in the generic pubsub implementation as well as the filter chain implementation. As soon as the condition is met, then no more subscribers are notified.</p>