There are three basic types of plugin architectures in ZF
- Chains – 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.
- Helpers – 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.
- Adapters – 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.
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.
Unfortunately, we have a few hurdles and issues with these, particularly when it comes to consistency.
- While many components use the PluginLoader to resolve short names to class names, there are case inconsistencies that occur between components.
- 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()".
- The PluginLoader is incredibly inefficient, as it requires Zend_Loader::isReadable() lookups for each prefix path registered – this is particularly noticeable when you have multiple paths, and the match exists several paths into the stack.
- 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.
- No common paradigm for passing information to factories.
- Use namespaces only. Plugin lookups should only consider the namespace, and attempt to autoload from those namespaces. E.g.:
- Use un/registerNamespace() instead of addPrefixPath()
- Use class_exists() only; no actual lookups
- Use lowercase-dash-separated-names. Plugin lookups should translate such names to MixedCase. E.g.:
- 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.
- Helpers should use __invoke() in all cases. This makes usage of helpers consistent, as well as simple:
- In cases where there is an existing interface, or where defining a method * makes better semantic sense, __invoke() should proxy to that method:
- All adapters should implement a "Configurable" interface, which allows passing
- Factories will instantiate an adapter, and then pass it options.
- As such, the adapter constructor typically should not be defined.
- 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.
- In most cases, direct instantiation of the adapter should be allowed.
- Adapter factories should have an attached plugin loader, and allow attaching a different one, for purposes of plugin resolution.
- Reference implementation
- All chains should extend one of the classes in the phly\pubsub component (to be renamed and added to ZF). These chains include:
- Provider: subscribe to one or more topics and either notify all subscribers or filter a value through all subscribers to a topic
- FilterChain: like Provider, but no topics; good for classes that have a single plugin flex-point.
- Build default chains where applicable: form decorators, etc.
- Allow extending chains and attaching subclasses to objects consuming them
- Corollary: do not proxy to chains, but instead simply attach chains
- Chains should act as plugin loaders or attach plugin loaders to ensure name resolution works consistently.