- In Drupal, we use hooks as our primary tool for allowing modules to customize nearly any behavior of Drupal core or other modules. It's a very simple yet powerful implementation of the Observer pattern and implicit invocation architecture. For example, any module can add to what happens early in a page request by implementing hook_init() or modify any form by implementing hook_form_alter().
- However, not every kind of extensibility is best modeled by "module A broadcasts an event and all modules react". Sometimes what we want is "module A wants some specific chosen thingie or a specific sequence of thingies to perform a well-defined operation, but for which exact thingie(s) is/are used to be configurable". Any module should be able to extend the system by providing additional thingies that can be chosen from, but at any given time, only the actually chosen one(s) should be invoked. For example, the Image module lets you create image styles by choosing from a set of image effects (scale, crop, rotate, desaturate, etc.). Any module can add more effects to the system. However, a particular image style is configured as a particular sequence of effects, and when it's time to generate an image of that style, the Image module wants to invoke just that particular sequence of effects.
- In core currently, we have at least a dozen such examples, and each one is implemented slightly differently. For example:
- The Aggregator module allows any module to implement one (and only one) "feed fetcher" by implementing hook_aggregator_fetch(). However, this is unlike common hooks like hook_init() or hook_form_alter(), because the Aggregator module doesn't passively broadcast an 'aggregator_fetch' event for all modules to react to; it actively keeps track of which fetcher the administrator configured to be the active one and only "invokes the hook" on that one module.
- The Block module allows any module to implement one or more blocks via hook_block_info(). It provides an admin/structure/block page allowing the administrator to select which blocks to enable and in which regions to display them. When it's time to render a region, for each block added to that region, it invokes the hook_block_view() "hook". Like with hook_aggregator_fetch(), this "hook" is not invoked on all modules, only the module that "owns" that block.
- The Image module allows any module to implement one or more image effects via hook_image_effect_info(), and provides a UI for defining image styles as a sequence of one or more effects. When generating an image in a particular style, for each effect in that style, it invokes the 'effect callback' function included as part of the definition of the effect, rather than a "hook". This has the advantage of not calling a non-hook a hook, but it also results in there being no documentation for the signature and expectations of this callback.
- The Filter module allows any module to implement one or more text filters via hook_filter_info(), and provides a UI for defining text formats as a sequence of one or more filters. When outputting text in a particular format, for each filter in that format, it invokes the 'process callback' function included as part of the definition of the filter. This is like the Image module example, but goes a step further by recommending a naming convention for this function and documenting it as a pseduo-hook.
- The actions system allows any module to implement one or more actions via hook_action_info() and for invoking the action, calls neither a pseudo-hook nor a callback function added to the action definition, but rather a magically named function based on the action id.
- The cache system includes a cache() function for getting back the implementation object (e.g., one for a database backend or one for a memcache backend) configured for the requested cache bin, and then the receiver of that object invokes well defined and well documented interface methods on that object. However, there is no API function for getting either the full list of possible cache bins or the full list of available backend implementations, and consequently, no UI for configuring what to use: the configuration must be managed manually in a settings.php variable.
- Just as there are inconsistencies in how "pluggable thingies" are invoked, there are inconsistencies in how they are discovered by the module providing the configuration UI. In many cases, an info hook is called, but sometimes it's followed by an alter hook and sometimes not. Sometimes the result is cached, sometimes not. Other details vary from use case to use case unnecessarily, and if two systems happen to share the same approach, they do it via duplicated rather than actually common code.
A plugin system that provides an API to make all these things consistent where they can be, and customizable where they need to be.
The initial patch has been committed. Need to open follow ups for converting systems to use it.
User interface changes
None by this issue.
hook_aggregator_fetch_info() has changed. This needs a change notice.
Original report by neclimdul
Plugin Documentation [in progress]
"The goal of the plugin system is to get everyone into the same chapter, not necessarily onto the same page." --David Strauss
So we’ve had the concept of “plugins” as swappable implementations of groups of code for a long time in Drupal. In Drupal Core have a variable_get/include replacement system used for things like cache backends and contrib has things like ctools, views and some other bespoke and clever systems. In Austin last year a lot of people sat down and distilled the concepts down into some core ideas and an architecture(summary). Now that we have a rough implementation its time to start discussing how we polish this off for core.
What does the architecture look like?
First it's important to understand the the slogan a bit. The idea is to provide concepts and tools for best practices. We want developers to be able to open up “plugin” code and conceptually be able to understand and discuss what’s going on even if the implementation is different from the defaults provided in core. Hence in the same chapter, not on the same page.
There are a some of important conceptual definitions that are useful as building blocks. These are the more general concepts that most developer would interact with.
Plugin discovery is the idea of discovering Plugin Implementation definitions. It's possibly the most important concept because it distinguishes plugins from just using an interface and a factory. It is core to plugins because it provides the tools for building user interfaces and its definitions are the bridge that get us from configuration to implementation.
So in your UI you use getPluginDefinition() to fetch definitions to drive your UI with things like human readable names. When saving from you UI you store the appropriate key bundled with the configured values. Then at runtime you can then call createInstance($key, $options); and get back your configured instance.
Discovery as an implementation is in itself “pluggable.” It is generally very closely connected to the Plugin Type but can be shared and replaced as needed as long as the interface is respected. While we can implement a common method for core, there are cases where the restrictions around a plugin force it to be implemented in unique ways and having the type able to choose a different discovery implementation allows this with minimal effort. An example would be layouts defining a plugin in a yaml file and listing template, js and css files. A PSR-0 class with annotations or a hook would not make sense and would be unnecessarily difficult for template developers.
Previously we’ve used info hooks and for very high level system this could be an options but ideally we’d like a better method. Currently we’ve implemented a “static” crud based implementation that could be useful for the earliest most limited Drupal bootstraps and there is a annotation based method in the works that will hopefully be the default and common implementation.
Conceptually a plugin type is the grouping for all Plugin Implementations that satisfy a particular purpose. Practically it is a class that provides the methods for interacting with those plugins. All Plugin Implementations are the members of exactly one plugin type and should be interchangeable with other implementations of the same type(they share an interface). The common example of the concept is the "Cache storage system" is a plugin type. "Database cache", "Memcache cache", etc. are plugins that conform to that type.
Plugin Type objects are the place where the high level business logic of the type is implemented. Definition defaults are defined, discovery and factory implementations are defined and any specific edge cases are dealt with.
A Plugin Implementation, or simply "Plugin", is a self-contained, encapsulated concept. It represents a implementation of a plugin type. Functionally and most importantly it is a definition that defines how the plugin is implemented. It will generally contain a human readable name for UI usage and and other relevant metadata. It will also have a associated class but this may be a default implementation that acts on the definition metadata.
The normal case of a plugin might be a block where the entire implementation is contained in a unique class and the metadata contains the Admin title and some information like cacheability and form defaults.
A Plugin Instance is a particular configured Plugin Implementation. It generally exists as a bundling of configuration with a Plugin Implementation. Functionally it literally resolved to an instantiated object but conceptually can exist as a triple of the Plugin Type, Plugin Implementation and configuration.
This is arguably the most confusing concept to grasp. People coming from CTools know this concept as “child” plugins. They’re plugins that share implementation in every way but are defined by some more dynamic system. If you were to accept blocks as plugins, each block like “Powered by Drupal” would be a normal plugin and custom blocks would be derivative plugins. Derivatives are important because, since every sub implementation is listed in the same list as the other plugins, they are able to be provided in the UI as unique items. This is best demostrated by sdboyer in his comment on this issue. In the end is becomes a way of preconfiguring plugins in a very specific way.
This Plugin Mapper is a front-facing object that is the main point of contact for the entire system. Its core job is interfacing with Discovery and proxying best practices around using a factory. Its a PHP Interface that can be replaced with different discovery methods depending on the use case of the plugin.
Plugin Factories exist per Plugin Type and have a simple task of instantiating plugin instances. There is a workflow implied by the mapper interface by design but more elaborate layered system like maybe views are welcome to use the interface in more clever ways.
So we took these concepts out of the sprint and refined them some with some more prototypes. So here’s where we’ve arrived and here’s some code! Lets open this discussion up.
PASSED: [[SimpleTest]]: [MySQL] 37,292 pass(es). View
PASSED: [[SimpleTest]]: [MySQL] 37,284 pass(es). View
PASSED: [[SimpleTest]]: [MySQL] 37,285 pass(es). View
PASSED: [[SimpleTest]]: [MySQL] 37,284 pass(es). View
PASSED: [[SimpleTest]]: [MySQL] 37,082 pass(es). View