Skip to main content

Safely extending Drupal 8 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.

by Lee Rowlands /

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

Tagged

Drupal 8, Plugins, OOP

Posted by Lee Rowlands
Senior Drupal Developer

Dated

Comments

Comment by dawehner

Dated

Nice!! Thank you for sharing it!

Comment by Matthieu Scarset

Dated

Cool! Thank you for sharing.

Comment by Hamza

Dated

Nice workaround. Quite helpful!

Comment by jhuhta

Dated

Nice that you documented this! The issue with changing constructors, BC policy and this excellent solution has been discussed at least in this core issue: https://www.drupal.org/node/2856625.

Comment by _benjy

Dated

IMO core should consider constructors a public API and not change them. Maybe core could use this pattern and save everyone else the hassle.

Comment by heddn

Dated

One thing I found when trying this recently is that it requires a fair amount more effort and code. And it isn't as obvious. So, if you are inheriting from something volatile, go ahead. Its worth it. But if it is something fairly stable, it didn't seem worth the extra effort to insulate the pain.

Comment by heddn

Dated

To clarify my last comment, the setter also has to be static as well. And then the variable that is set also needs to be static. And the trickle-down is pretty complicated.

Comment by Lee Rowlands

Dated

No, neither of them need to be static.

At the time you've called the parent ::create method, you're dealing with an instance.

Comment by joelpittet

Dated

This looks great but I ran into an issue trying to implement this today. Drupal constructor is considered @internal by the BC policy https://www.drupal.org/core/d8-bc-policy

So we aren't guaranteed it won't change from my understanding of @internal and this could be a mistake but it was changed here: https://www.drupal.org/project/drupal/issues/2594425

Which means even though my constructor would be safe from changes, the create() method would have to add the new parameter that the constructor added.

Though a bit more work I was able to Decorate the block plugin which doesn't have this problem... though this is still not tested and fresh so may not be the right approach... https://www.drupal.org/project/menu_block/issues/2968049

Comment by alexpott

Dated

There's no need for the public setter and the increase in stateful public API that that entails. In the static method you can access protected properties on any instance created. Like so https://3v4l.org/KSYat

Comment by joelpittet

Dated

Doesn't setFooMeddler need to be on the parent class for this to work?

Comment by Brian

Dated

Note that the plugin class has to implement \Drupal\Core\Plugin\ContainerFactoryPluginInterface for this to work. Not all plugins do, and without it, Drupal will never invoke the static create method on your plugin. See \Drupal\Core\Plugin\Factory\ContainerFactory.

Pagination

Add new comment

Restricted HTML

  • Allowed HTML tags: <a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd> <h2> <h3> <h4> <h5> <h6>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.
Not sure where to start? Try typing "hello" or "help" if you get stuck.