Plugins play an important role in Drupal 8, and understanding how the entire plugin system works will help us better understand how, when, where, and why we use plugins.
It is my hypothesis that if you understand the mechanics of the plugin system you'll write better plugins. This article is my attempt at unraveling the Drupal 8 plugin system in order to provide better understanding for those of us that are affected by it. The article starts out by deconstructing the various components that make up the plugin system and then provides an example of how you can make use of this design pattern in your own modules.
Plugins Are a Design Pattern
Plugins are a general reusable solution to a recurring problem within a given context. The plugin system isn't a finished chunk of code that can just be transferred directly into your own code, rather it's a description or template for solving a specific problem that can be used in many different situations. Plugins also include some generalized utility code that demonstrates a pattern. This is meant to assist the developer who can use this utility code as a starting point instead of having to rewrite the boiler plate pieces each time. In software engineering we call this a design pattern. This one just happens to be specific to Drupal.
If you want to implement a new plugin type in your module, there's a few things you'll need to understand:
- How plugin discovery works
- How to instantiate an instance of a given plugin
- Best practices for making it easy for others to implement plugins of your type
- How to include your new plugin manager in the Drupal services container
The Ice Cream Shop
I like to use the metaphor of an ice cream shop as way to illustrate the plugin system and all it's components. If you're the proprietor of an ice cream shop you'll likely face the following challenges as part of doing business.
- Obtaining or making a large enough variety of flavors to keep customers coming back.
- Appropriately pricing a scoop of ice cream, and perhaps varying the price per flavor depending on the cost of raw materials.
- Training employees how to deliver a scoop of ice cream to a customer.
The ideal business plan would then need to facilitate the possibility of partnering with other ice cream makers in an effort to offer more flavors. Perhaps adjusting the cost of a scoop of ice cream depending on the flavor being served. Employees should be able to serve a scoop of ice cream without having to worry about what flavor it is, they should be able to just scoop it. And of course you're going to need a freezer that's well organized so that you and your employees can efficiently find the flavor a customer is asking for.
Your ice cream shop needs a system that operates much like Drupal 8's plugin system.
What is a Plugin Manager?
A plugin manager describes how plugins of a given type will be located, instantiated, and generally what they’ll do. By creating a new plugin manager you're also creating a new plugin type, which other modules can implement in order to provide a new unit of functionality to compliment any existing plugins of the defined type. You can think of the plugin manager as being the ice cream shop itself, not just simple structure, but a business made up of many people, machines, spreadsheets, and ice cream flavors. All of which combine together to form the consumers idea of what an ice cream shop is. A place where I walk up to the window, ask for a scoop of basil flavored ice cream and with no need to understand the inner workings of the business be presented with an ice cream cone that I can consume.
All plugin managers are implementations of the PluginManagerInterface
, which extends the DiscoveryInterface
, FactoryInterface
and, MapperInterface
. Forming the basis for any new plugin manager. While a custom plugin manager could implement all the methods of the component interfaces it's more likely for plugin managers to proxy the method invocations to the respective components, and directly implement only the additional functionality needed by the specific plugable system. Core already provides various implementations of these component interfaces, like AnnotatedClassDiscovery
for example, and when writing your own custom plugin manager you're more likely to make use of these existing components and just wire them together with information about your specific plugin type and a few minor tweaks to provide the functionality that makes your plugable system unique.
The common way of doing so is by extending the DefaultPluginManager
class and then either just using the defaults or setting the DefaultPluginManager::discovery
and DefaultPluginManager::factory
properties to use one of the existing components and letting the DefaultPluginManager take care of proxying to those classes.
It sounds complicated, and honestly the system is complicated. There's a lot of moving parts. But once you know the pattern, and how to make use of existing boiler plate code, implementing a new plugin manager is actually pretty straight forward.
Let's take a look at the components that are already available to us, and then we can tie a few of them together and create a custom plugin manager.
Plugin Discovery
As the owner of the ice cream shop it's important to be able to inventory all the ice cream flavors in your cooler at any given time. It doesn't really matter where the flavor came from, or exactly what ingredients it contains, or even if it's any good. You simply need to generate a list of all the flavors currently in your shop so you can write them up on the chalkboard and inform your customers what flavors they can choose from.
In addition to being able to list all the flavors you've currently got you also need to tell both the delivery girl, and your intern who is hard at work making more vanilla, where to put any new flavors so you can find them tomorrow. So you write up a detailed set of instructions for how, where to place the buckets of ice cream so that you'll discover them next time you take inventory and get them on the chalkboard as well.
One of the main responsibilities of the plugin manager is to locate—or discover—any implementations of the plugin type that it's responsible for. Consider block plugins for example. Any module that's enabled can provide a new block to Drupal in the form of a plugin. The BlockPluginManager
needs to know which modules are enabled and where to find any block plugins those modules provide in order to do things like add them to the list of blocks that can be placed via the block administration screen.
At its most basic, this works because the BlockPluginManager
has defined a pattern that any module can follow that includes where to place the PHP code that implements the block plugin and where to find meta-data about any block plugin. As a module developer you simply follow that pattern and your block plugins will be located by the BlockPluginManager
.
Here are the plugin discovery mechanisms that core supports. And while you could certainly write your own, don't underestimate the additional features like caching that you get for free when using one these existing components.
Annotated Plugin Discovery
Provided by the AnnotatedClassDiscovery
class, this is the most commonly used discovery component in core and the recommended method to use when creating a new plugin manager. Plugins are discovered by using the PSR-4 standard to locate a file that contains the definition of the plugin in the form of a class, and annotations in the @docblock for the class to provide meta-data to the plugin manager. Developers wishing to use annotated class discovery need only provide a PHP sub-namespace and class that extends the Drupal\Components\Annotations\Plugin class and defines the information that should be entered into the annotation for a plugin of this type.
The following example looks for any class in the Drupal{module}\Plugin\Block namespace with an annotation that follows the form provided by Drupal\block\Annotation\Block. Anything it finds it assumes the class is a block plugin instance.
MyPluginManager::discovery = new AnnotatedClassDiscovery(‘Plugin/Block’, $namespaces, 'Drupal\block\Annotation\Block');
Hook Based Plugin Discovery
Provided by the HookDiscovery
class, hook based discovery is similar to the Drupal 7 info hook pattern in which all PHP functions that follow a specific naming convention are called and expected to return an associative array that tells the system where to find plugins of the given type. Hook based discovery is less common and is primarily used for legacy support.
The following example looks for any {module name}_block_info()
functions, calls them, and aggregates their return value into a list of block plugins. That list can then be used to locate any individual plugin instance.
MyPluginManager::discovery = new HookDiscovery($this->moduleHandler, 'block_info');
YAML Based Plugin Discovery
Provided by the YamlDiscovery
class, YAML discovery uses a .yml file with a special name located in the root directory of any enabled module to get a list of plugins of the given type provided by that module as well as meta-data for those plugins. YAML discovery is currently used primarily for the menu system and the definition of things like menu items and contextual links where the plugin itself can be instantiated and used simply from the meta-data provided in the YAML file and no custom PHP based processing is necessary at the individual module level.
The following example would look for a file named {module}.blocks.yml in the root directory of any enabled modules and use the information contained in the .yml file to generate a list of plugins of the given type and their meta-data.
MyPluginManager::discovery = new YamlDiscovery('blocks', $module_handler->getModuleDirectories());
Static Plugin Discovery
Provided by the StaticDiscovery
class, Static discovery allows plugin definitions to be manually registered rather than dynamically discovered like the other three methods. This requires any module that wishes to implement a plugin of the given type to manually register their provided plugin instance with the plugin manager. Static discovery is currently only used for tests and you'll rarely if ever use this mechanism in your own module. It's kind of like if the customer showed up at your shop, and handed you a bucket of cherry ice cream, and then asked if you had any cherry ice cream.
Example:
MyPluginManager::discovery = new StaticDiscovery();
Discovery Decorators
And just in case you didn't have enough options for plugin discovery already there are also discovery decorators. Decorator classes are used to wrap one of the above discovery classes with another class that implements all the same methods but provides some additional level of processing before or after that provided by the base discovery class. The classic example from Drupal 7 is alter hooks. Give me a list of all the plugins of type X and then let any module that would like to manipulate the complete list have a chance to do so.
The following example would use {module}block_info()
functions to discover a list of available plugins, and then immediately call {module}
block_info_alter()
functions and pass in the collection of defined plugins as an argument allowing any module implementing the alter hook to manipulate the list. (Very Drupal 7, eh?)
MyPluginManager::discovery = new InfoHookDecorator(new HookDiscovery('block_info'), 'block_info_alter');
The somewhat more complicated to understand, but equally useful, decorator is the DerivativeDiscoveryDecorator
. I find that this one is easiest to explain with a use case. Drupal allows an administrator to create any number of menus. Since this is user generated configuration, Drupal can't ship with a known list of all menus that a particular site might contain. Drupal also provides a block for every defined menu that can be placed via the block UI. Blocks, are plugins. If the list of possible menus is variable and we need a block for each that means we need a way to define in code with an annotation of an instance of a block plugin so that the block plugin manager can find it. Furthermore, that block plugin needs to be able to create derivative instances of itself: one for each configured menu. This system allows for a variable number of plugins of a given type based on application state or configuration.
Example usage:
MyPluginManager::discovery = new DerivativeDiscoveryDecorator(new HookDiscovery('block_info'));
For a more complete example of using the DerivativeDiscoveryDecorator
take a look at how the core system module provides an individual block for each configured menu with Drupal\system\Plugin\Derivative\SystemMenuBlock
and Drupal\system\Plugin\Block\SystemMenuBlock
.
Plugin Factories
Once you've gotten the names of all your ice cream flavors written on the board in large, vibrant, hand-drawn letters, your customers are likely to start requesting those flavors by name. "I'll take a scoop of vanilla", they'll say. Or, "A scoop of almond and a second of buttermint if you would please.". Some scoops will come right out of the bucket, and others will require being garnished with a sprinkle of salt before they can be served. But as far as the customer is concerned, they've identified the flavor they want and now it's your job to provide them with a cone and scoop of the requested flavor post-haste, by whatever means necessary.
A plugin manager needs to be able to respond to requests for a plugin with a given ID by returning an instance of the instantiated plugin. When I request the 'system_powered_by_block' plugin from the block manager I expect that it will return an instance of the Drupal\system\Plugin\Block\SystemPoweredByBlock
class and that I can get right down to displaying the content of that block without having to bother loading any files or classes or knowing anything about how to instantiate the SystemPoweredByBlock
. This is the domain of the MyPluginManager::factory
: a system for generically requesting an instance of a plugin without having to know anything about how that plugin type is handled. Just like with plugin discovery, Drupal core provides a handful of useful implementations of the FactoryInterface
that we can use in our own plugin manager to instantiate plugin instances.
It really boils down to this: what class should I instantiate and what arguments should I use when doing so?
Factories provide the plugin manager with a MyPluginManager::createInstance()
method which proxies the request to the selected factory class, which, in turn, can instantiate and return the specified plugin instance.
This a list of the plugin factory components that core provides. You can always write your own, but these should serve most use cases and provide some baked-in goodness like caching to help speed up the system. In order to describe how these work we'll take a look at what happens for each factory type when you call the plugin managers MyPluginManager::createInstance()
method.
The Default Factory
The simplest factory is provided by the Drupal\Component\Plugin\Factory\DefaultFactory
class, which simply answers the questions: What class? What arguments? The discovery component takes care of locating the plugin and providing the manager with a class name. The default factory then maps a plugin ID to a class, instantiates a copy of that class with a set of common arguments, and returns it. The arguments consist of any additional configuration passed to the createInstance()
method, the ID of the plugin being instantiated, and the definition of the plugin as returned by the discovery mechanism. In most cases this would be the annotation for your plugin.
Example:
return new $plugin_class($configuration, $plugin_id, $plugin_definition);
The Container Factory
The Drupal\Core\Plugin\Factory\ContainerFactory
class extends the DefaultFactory
class and as such does everything that it does with one minor addition: it injects a copy of the Drupal services container by calling the plugin's ::create()
method and performing constructor injection. This is useful if plugins managed by your new plugin manager need to have access to the services container in order to do whatever it is they do.
The container factory is also smart enough to recognize when a particular instance of a plugin doesn't use the container injection pattern and falls back to instantiating the plugin just like the DefaultFactory
. This is nice because then each individual instance of a plugin can decide on it's own whether it needs the service container or not. It also make the ContainerFactory
probably the best choice if you can't decide.
This is the default, and recommended factory for most use-cases.
Example:
return $plugin_class::create(\Drupal::getContainer(), $configuration, $plugin_id, $plugin_definition);
The Reflection Factory
If you need a bit more flexibility and want to allow plugins handled by your plugin manager to be able to allow for a variable or unknown set arguments in their constructor you can use the Drupal\Component\Plugin\Factory\ReflectionFactory
class. This factory will perform some introspection on the class that it's about to instantiate and derive the list of arguments to pass to the constructor based on the method signature of the constructor itself.
If you have class with a constructor like construct($configuration, $plugin_id, $definition, $ponies, $pizza)
and another like construct($configuration, $plugin_id, $definition, $ponies, $pizza, $tacos)
the ReflectionFactory will know that the second example has an additional argument of $tacos
and ensure that argument is present when instantiating the class.
To be honest I'm not exactly clear on the use case for this, or when it would be useful. If anyone has thoughts I would love to hear them.
Example:
$arguments = $this->getInstanceArguments($reflector, $plugin_id, $plugin_definition, $configuration);
$instance = $reflector->newInstanceArgs($arguments);
return $instance
Creating Your Own Factory
Although it's likely not very useful as a component for your custom plugin manager the Drupal\Core\Field\WidgetFactory
class is a good example of creating a custom factory in order to allow for the use of additional configuration parameters in the instantiation of a plugin. Field widgets need to know some additional information about the field instance and as such the widget plugin constructors all take a field definition and field settings parameter in addition to the common configuration, plugin_id, and plugin_definition arguments. This is great example to follow if your plugins require additional settings or configuration to be passed in when they are instantiated.
Plugin Mappers
Sometimes you'll get an ice cream connoisseur who comes into your shop and rather than read the board and ask you for a flavor by name they'll instead ask for, "A scoop of your best flavor please.". Now, having spent many years working with ice cream you know that everyone's definition of best is different, and that it depends more on the person, their individual tastes, the time of day, and even what they had for dinner prior to coming into your shop. So you ask a few questions in return, and then based on their answers to those questions you discern that for this particular individual a scoop of eggplant parmesan shortbread ice cream will hit the spot. You locate the flavor, scoop it into a cone, and hand the cone over the waiting customer. "Trust me, this is the one you want."
Mappers provide an additional layer of abstraction around plugin factories. Similar to decorators for discovery, they allow for extra processing to take place prior to creating an instance of a plugin. Mappers are used when the plugin manager needs to provide a generic way respond to requests for get me a plugin, but I don't know which one and need you to figure that out based on application state or configuration. Mappers provide the plugin manager with a ::getInstance()
method that can return the correct plugin based on additional logic.
Example use cases for mappers include the cache plugin manager. As a developer I simply want to request an instance of a cache plugin so I can cache some data. The manager however needs to allow for the possibility of alternate caching mechanisms such as memcache. So when I call its ::getInstance()
method the manager will first look up in my application's configuration which cache backend I have specified for a given cache bin and determine the plugin ID of the plugin that needs to be loaded and pass that along to the factory. Ultimately returning and instance of the the appropriate cache plugin.
Another example is servicing a request to a REST server where I want to use a plugin to convert an array of data into the appropriate format and return it. The type of data that's going to be returned should be derived from the HTTP requests headers. If the request is asking for application/json the manager should know to get an instance of the JSON response plugin, if, however, the incoming request specified "application/xml" the manager would need to return an instance of the XML response plugin.
Because the PluginManagerInterface
extends the MapperInterface
the presence of a ::getInstance()
method is already expected. Should you want to provide a custom mapper for your plugin manager you can simply override this method with your custom logic in MyManager::getInstance()
.
Putting It All Together
That's a lot of theory. But what does this all look like in action? Well, thanks to the Drupal\Core\Plugin\DefaultPluginManager
, there's actually very little code involved in creating a basic plugin manager.
You can grab a copy of the sample code on GitHub if you want to follow along or just review it in your editor of choice.
Start with a basic info file. Believe it or not we don't even need a .module file here.
icecream.info.yml
name: Ice Cream
description: An Ice Cream Shop!
core: 8.x
package: Examples
type: module
Next, create your custom plugin manager.
icecream/src/IcecreamManager.php
<?php
/**
* @file
* Contains IcecreamManager.
*/
namespace Drupal\icecream;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
/**
* Icecream plugin manager.
*/
class IcecreamManager extends DefaultPluginManager {
/**
* Constructs an IcecreamManager object.
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations,
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to invoke the alter hook with.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct('Plugin/Flavor', $namespaces, $module_handler, 'Drupal\icecream\FlavorInterface', 'Drupal\icecream\Annotation\Flavor');
$this->alterInfo('icecream_flavors_info');
$this->setCacheBackend($cache_backend, 'icecream_flavors');
}
}
The important parts here are that we are extending the DefaultPluginManager
, and then in our class' constructor delegating to the Drupal\Core\Plugin\DefaultPluginManager::_constructor()
, which takes care of a lot of the heavy lifting for us. The DefaultPluginManager
assumes that you're using Annotations for discovery in conjunction with the ContainerDerivativeDiscoveryDecorator
, the ContainerFactory
for instantiating instances of your plugin, and that you like speed and want to use caching for things like discovery.
This is the most important line of code in our plugin manager: parent::__construct('Plugin/Flavor', $namespaces, $module_handler, 'Drupal\icecream\FlavorInterface', 'Drupal\icecream\Annotation\Flavor');
In this line, we're defining a new Flavor plugin type, instances of which are in the {Vendor}\Plugin\Flavor
namespace. Or more verbosely: Drupal\{module}\Plugin\Flavor
. These plugins are all implementations of the Drupal\icecream\FlavorInterface
, and they use annotations for meta-data defined by and documented in Drupal\icecream\Annotation\Flavor
.
If you take a look at the code inside the DefaultPluginManager::_constructor()
method you'll notice that it's assigning the $this->factory
and $this->discovery
variables. Want to use HookDiscovery
in your plugin manager instead? You can skip the call to parent::__constructor()
and manually assign the factory and discovery properties to an instance of any of the classes discussed earlier in this article. It's that easy.
With that in place you could instantiate an instance of IcecreamManager
and call the IcecreamManager::getDefinitions()
method. It would go out and dutifully retrieve any Flavor plugins defined by all the enabled modules. But there's still quite a bit more we can do to make our new plugin type easier to use, discover, and for other developers to understand when it comes time for them to implement Flavor plugins including:
- Provide a PHP interface to better define what Flavor plugin can do.
- Provide a base class for new Flavor plugins to extend.
- If using annotation based discovery (which you probably should be) define an annotation class that documents the properties metadata properties of a Flavor plugin.
Define Your Plugin Types Annotation
In our IcecreamManager::__construct()
code we declared that flavor plugins are going to use Annotations for discovery so we'll need to provide a reference for what information those Annotations will contain. In this line: parent::__construct('Plugin/Flavor', $namespaces, $module_handler, 'Drupal\icecream\Annotation\Flavor');
we declared that the class that documents our Annotations is Drupal\icecream\Annotation\Flavor
, so let's make sure there's some code that defines this class.
Our Annotation class should implement the Drupal\Component\Annotation\AnnotationInterface
and should do so by extending Drupal\Component\Annotation\Plugin
, which does most of the work for us. In our class we can then define one or more public properties that effectively document the information collected in the Annotations associated with an instance of the Flavor plugin type. Note that the @Annotation
in the @docblock for our class is required and should be the last line in the @docblock. It's used for discovery of Annotation definitions.
icecream/src/Annotation/Flavor.php
<?php
/**
* @file
* Contains \Drupal\icecream\Annotation\Flavor.
*/
namespace Drupal\icecream\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a flavor item annotation object.
*
* Plugin Namespace: Plugin\icecream\flavor
*
* @see \Drupal\icecream\Plugin\IcecreamManager
* @see plugin_api
*
* @Annotation
*/
class Flavor extends Plugin {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The name of the flavor.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $name;
/**
* The price of one scoop of the flavor in dollars.
*
* @var float
*/
public $price;
}
This class would correlate to an Annotation like the following:
/**
* Some comment ...
*
* @Flavor(
* id = "vanilla",
* name = @Translation("Vanilla"),
* price = 1.75
* )
*/
The @Flavor
annotation name corresponds with the name of our class Drupal\icecream\Annotation\Flavor
and each of the keys in the annotation corresponds with one of the properties that we defined and documented in our Annotation class. The Annotation parser will read in the Annotation in the @docblock and ensure that the data it contains conforms to the format provided in the Flavor
class. The documentation in this class will also serve as a reference for any module developer that wants to provide a Flavor plugin and has questions about what values to use in the the plugin instance's annotations.
The Plugin Interface
If you're going to start working with 3rd party vendors like they brewery down the street to create new ice cream flavors it's important that you've defined what exactly you're going to need to know about a flavor. That way no one is left in the dark, you know how to price it, your provider knows where to put it in the cooler, your employees know how to scoop it, and your customers can enjoy it.
Although not strictly required, defining a PHP interface for you new plugin type, should be considered an essential step. The interface defines the methods that any code interacting with any plugin of your new plugin type can call, and be assured are there. This is a critical part of the process because it allows a developer to ask any plugin manager factory for an instance of a plugin handled by that plugin manager and without knowing any other details about the specific instance that was returned be able to make use of it. For example: $plugin_instance->doSomethingAwesome();
In our case we want to ensure that anytime you've got an instance of a Flavor plugin you can perform one of the following operations, $flavor->getName()
, $flavor->getPrice()
, and $flavor->slogan()
. We don't really care that much how you determine the name of your flavor, or how you calculate the cost as long as when I call the $flavor->getPrice()
method I get the cost of that particular flavor. So let's create a FlavorInterface
class. All Flavor plugins should implement this interface.
icecream/src/FlavorInterface.php
<?php
/**
* @file
* Provides Drupal\icecream\FlavorInterface
*/
namespace Drupal\icecream;
use Drupal\Component\Plugin\PluginInspectionInterface;
/**
* Defines an interface for ice cream flavor plugins.
*/
interface FlavorInterface extends PluginInspectionInterface {
/**
* Return the name of the ice cream flavor.
*
* @return string
*/
public function getName();
/**
* Return the price per scoop of the ice cream flavor.
*
* @return float
*/
public function getPrice();
/**
* A slogan for the ice cream flavor.
*
* @return string
*/
public function slogan();
}
Note that we're extending the Drupal\Component\Plugin\PluginInspectionInterface
, while not strictly required this is considered to be a best practice as it ensures some level of baseline consistency across all plugins regardless of type. For example, all plugins should have a $plugin::getPluginId()
method.
Plugin Base Classes
Ready to really increase your ability to collaborate with others on new ice cream flavors? What if you provided your partners with a standard vanilla ice cream mixture and they could focus on adding the ingredients that they specialize in. If every flavor has the same base of eggs, cream, and sugar, why not leverage economies of scale and let each ice cream scientist focus on just the parts that are unique to their flavor. You'll certainly be able to crank through and test a whole lot of new flavors at a faster rate.
Another not strictly speaking required, but generally good idea is to provide a base class for your plugin type that anyone wishing to provide an instance of your plugin type can extend. Most plugins will contain a fair amount of boiler plate code that is the same for almost all plugins of that type, things like getPluginId()
, and getName()
are likely pretty universal across all Flavor plugins – just read the data from the provided annotation and return it. By providing a base class we can eliminate the need for code duplication and make it even easier for other module developers to provide new Flavor plugins.
In order to facilitate this best practice core provides the Drupal\Component\Plugin\PluginBase
that we can extend to create our own plugin type specific base class. The PluginBase
class also implements the PluginInspectionInterface
mentioned earlier, and the DerivativeInspectionInterface
, eliminating all sorts of boiler plate code that we would otherwise have to put in our FlavorBase
class.
icecream/src/FlavorBase.php
<?php
/**
* @file
* Provides Drupal\icecream\FlavorBase.
*/
namespace Drupal\icecream;
use Drupal\Component\Plugin\PluginBase;
class FlavorBase extends PluginBase implements FlavorInterface {
public function getName() {
return $this->pluginDefinition['name'];
}
public function getPrice() {
return $this->pluginDefinition['price'];
}
public function slogan() {
return t('Best flavor ever.');
}
}
Our Drupal\icecream\FlavorBase
class implements the FlavorInterface
we defined earlier and provides the requisite methods. In most cases we can just read things like the flavor name and price directly from the Annotation provided with the plugin instance. If that's generically true we can do that in our base class and save module developers the hassle of having to write getName
and getPrice
methods that all do the same thing. Of course with OOP inheritance should they need to, a module developer could overwrite any of these methods with one that performs whatever logic is required for their specific plugin instance. Providing this base class will significantly reduce the amount of code someone needs to write in order to provide a new ice cream flavor. And that's a good thing because the more flavors the better.
Provide A Flavor Plugin
And finally all that hard work of organizing, planning, documenting, and training your employees has paid off. Your ice cream shop is seeing record profits and you're ready to really open the flood gate of new flavors. Who says you can't have a whole new set of flavors every week?
You probably went through the effort of creating a new plugin type because you had some functionality that you wanted to encapsulate into a plugin. With all of the above in place we can provide a new Flavor plugin by creating a new class in the Drupal\{module}\Plugin\Flavor
namespace with an annotation that conforms to Drupal\icecream\Annotation\Flavor
. Whenever someone calls the IcecreamManager::getDefinitions()
method our flavor instance will be loaded and available for use.
Here's an example flavor plugin:
icecream/src/Plugin/Flavor/Vanilla.php
<?php
/**
* @file
* Contains \Drupal\icecream\Plugin\Flavor\Vanilla.
*/
namespace Drupal\icecream\Plugin\Flavor;
use Drupal\icecream\FlavorBase;
/**
* Provides a 'vanilla' flavor.
*
* @Flavor(
* id = "vanilla",
* name = @Translation("Vanilla"),
* price = 1.75
* )
*/
class Vanilla extends FlavorBase {}
Seriously, that's it. Vanilla is so simple.
Of course that's probably the simplest possible case, what about something a little more complicated. In this case we can override the FlavorBase::getPrice()
method and do something a bit more complicated, as long as we return a float value that represents the price of our flavor the IcecreamManager won't care how we decide what that value is.
icecream/src/Plugin/Flavor/Chocolate.php
<?php
/**
* @file
* Contains \Drupal\icecream\Plugin\Flavor\Chocolate.
*/
namespace Drupal\icecream\Plugin\Flavor;
use Drupal\icecream\FlavorBase;
/**
* Provides a 'chocolate' flavor.
*
* @Flavor(
* id = "chocolate",
* name = @Translation("Chocolate"),
* price = 1.75
* )
*/
class Chocolate extends FlavorBase {
public function getPrice() {
// Insert code to query the international chocolate database for the current
// going rate of cocoa and then determine the price per scoop of Chocolate
// ice cream, or simply return the default price if we can't determine one.
//
// Magic ...
//
return $price;
}
}
Recap
With all of these things in place we're now declaring a new plugin type, documenting it, and making it as easy as possible for other developers to provide Flavor plugins. Here's the pieces we've built:
- A plugin manager
Drupal\icecream\IcecreamManager
that declares how we'll discover and instantiate Flavor plugin instances. - An Annotation class that defines the key/value pairs that can be used in Flavor plugin annotations.
- An interface,
Drupal\icecream\FlavorInterface
, that defines what we can expect to be able to do with a Flavor plugin. - A base class,
Drupal\icecream\FlavorBase
, that makes it easier for others to create Flavor plugins by handling any generic functionality.
Once again, here's the example code.
That's it, that's defining a plugin type in Drupal 8. There are a lot of moving pieces and it's important to understand how they all fit together in order to leverage the system, but at the end of the day there's actually not very much code required thanks to the various base classes provided by Drupal core.