Senior Developer
Safely extending Drupal 10 plugin classes without fear of constructor changes
From time to time you may find you need to extend another module's plugins to add new functionality.
You may also find you need to alter the signature of the constructor in order to inject additional dependencies.
However plugin constructors are considered internal in Drupal's BC policy.
So how do you safely do this without introducing the risk of breakage if things change.
In this article we'll show you a quick trick learned from Search API module to avoid this issue.
So let's consider a plugin constructor that has some arguments.
Here's the constructor and factory method for Migrate's SQL map plugin
/**
* Constructs an SQL object.
*
* Sets up the tables and builds the maps,
*
* @param array $configuration
* The configuration.
* @param string $plugin_id
* The plugin ID for the migration process to do.
* @param mixed $plugin_definition
* The configuration for the plugin.
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration to do.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EventDispatcherInterface $event_dispatcher) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->migration = $migration;
$this->eventDispatcher = $event_dispatcher;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$container->get('event_dispatcher')
);
}
As you can see, there are two additional dependencies injected beyond the standard plugin constructor arguments - the event dispatcher and the migration.
Now if you subclass this and extend the constructor and factory to inject additional arguments, should the base plugin change its constructor, you're going to be in trouble.
Instead, you can use this approach that Search API takes - leave the constructor as is (don't override it) and use setter injection for the new dependencies.
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
$instance = parent::create(
$container,
$configuration,
$plugin_id,
$plugin_definition,
$migration
);
$instance->setFooMeddler($container->get('foo.meddler'));
return $instance;
}
/**
* Sets foo meddler.
*/
public function setFooMeddler(FooMeddlerInterface $fooMeddler) {
$this->fooMeddler = $fooMeddler;
}
Because the signature of the parent create method is enforced by the public API of \Drupal\Core\Plugin\ContainerFactoryPluginInterface you're guaranteed that it won't change.
Thanks to Thomas Seidl for this pattern