View Source

<h2>History and issues</h2>
<p>There are three basic types of plugin architectures in ZF</p>
<ul>
<li><strong>Chains</strong> &ndash; typically, you attach one or more &quot;plugins&quot; to an object, and at specified &quot;hooks&quot;, each plugin is run. This is really simply a pubsub system. Examples include validators, filters, decorators, etc.</li>
<li><strong>Helpers</strong> &ndash; 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> &ndash; 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 &quot;short&quot; 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 &quot;direct&quot; method; view helpers use a method named after the helper class's short name; validators use &quot;isValid()&quot;, filters use &quot;filter()&quot;.</li>
<li>The PluginLoader is incredibly inefficient, as it requires <code>Zend_Loader::isReadable()</code> lookups for each prefix path registered &ndash; 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 &quot;unified constructor&quot;: 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 &quot;short name&quot; 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)
{
// ...
}

public function __invoke($value, $context = null)
{
return $this->isValid($value, $context);
}
}
]]></ac:plain-text-body></ac:macro></li>
</ul>


<h3>&quot;Configurable&quot; interface for all adapters</h3>
<ul>
<li>All adapters should implement a &quot;Configurable&quot; 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)
{
// ... 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 &quot;type&quot; key and a &quot;params&quot; 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) {
$options = $options->toArray();
}
if (!is_array($options)) {
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>