Problem/Motivation

To wire services to a plugin, developers must implement ContainerFactoryPluginInterface and a custom create() method that pulls services from the container and calls the plugin constructor. This is largely boilerplate code and must be written for each plugin implementation.

Using PHP 8 attributes, we can tag constructor parameters directly and allow the factory class to discover and inject the required services, and drop the create() method entirely.

This also has the advantage of placing the service name directly next to the parameter where it is used, instead of having to keep the create() method and constructor signature in sync in two different places.

Steps to reproduce

Proposed resolution

Add support to ContainerFactory for Symfony's #[Autowire] attribute.

Remaining tasks

Determine if there are any more edge cases to cover.

User interface changes

API changes

Plugins can now wire services by specifying an #[Autowire] attribute on the each constructor parameter, instead of writing a separate create() method.

Data model changes

Release notes snippet

CommentFileSizeAuthor
#24 3294266-24.patch2.98 KBlongwave
#3 3294266-3.patch6.38 KBlongwave

Issue fork drupal-3294266

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

longwave created an issue. See original summary.

longwave’s picture

As an example, this code

  /**
   * Constructs a new MenuLinkDefault.
   *
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin_id for the plugin instance.
   * @param mixed $plugin_definition
   *   The plugin implementation definition.
   * @param \Drupal\Core\Menu\StaticMenuLinkOverridesInterface $static_override
   *   The static override storage.
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, StaticMenuLinkOverridesInterface $static_override) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);

    $this->staticOverride = $static_override;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('menu_link.static.overrides')
    );
  }

can be reduced to something like

  /**
   * Constructs a new MenuLinkDefault.
   *
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin_id for the plugin instance.
   * @param mixed $plugin_definition
   *   The plugin implementation definition.
   * @param \Drupal\Core\Menu\StaticMenuLinkOverridesInterface $static_override
   *   The static override storage.
   */
  public function __construct(
    array $configuration,
    $plugin_id,
    $plugin_definition,
    #[Service('menu_link.static.overrides')]
    StaticMenuLinkOverridesInterface $static_override,
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);

    $this->staticOverride = $static_override;
  }

With PHP 8 constructor property promotion this can be reduced even further, but that is for a separate issue.

longwave’s picture

Status: Active » Needs review
StatusFileSize
new6.38 KB

Rough prototype which converts two core plugins. Backward compatibility will be the tricky part here.

The name ContainerAutowiredPluginInterface is in the hope that one day we can drop the attribute in most cases and still inject the correct service, if we complete #3049525: Enable service autowiring by adding interface aliases to core service definitions

longwave’s picture

Title: Allow plugin service wiring via constructor property attributes » Allow plugin service wiring via constructor parameter attributes
longwave’s picture

Issue summary: View changes
longwave’s picture

There is also an argument for using https://github.com/symfony/dependency-injection/blob/6.2/Attribute/Autow... as the attribute instead of creating our own, as we might want to use Symfony's attribute for autowiring services.

longwave’s picture

Refactored to not require an interface and borrow Symfony's autowire attribute. Added some tests for various cases including autowired subclasses inheriting from non-autowired parents and vice versa.

longwave’s picture

Issue summary: View changes
longwave’s picture

hawkeye.twolf’s picture

Are plugin constructors constructors still considered internal in Drupal's BC policy? This post by @larowlan outlines why it's better to use the create() method (which is protected from BC changes) to inject dependencies and not risk encountering breaking changes to the parent constructor signature.

Is it possible to use autowiring with the create() method instead of (or in addition to) the constructor?

longwave’s picture

Symfony also supports autowired setter injection, that is definitely something we could consider supporting as well, if that helps - but the code as it stands should support both the old create() method and autowiring, even in subclasses.

larowlan made their first commit to this issue’s fork.

larowlan’s picture

Looks like the last re-roll was off 10.0.x - rebased it off 10.1.x with --onto

Fixed the ContainerFactoryTest

andypost’s picture

Status: Needs review » Needs work
Issue tags: +Needs change record

thanks! otherwise RTBC as it needs to notify module developers

larowlan’s picture

Issue tags: +Needs tests

I was getting some fails locally on tests in entity hierarchy around condition plugins and then realised I still had this branch checked out.

The patch breaks this scenario

Plugin that implements ContainerFactoryPluginInterface that has a trait that pulls in the ::create and ::__construct methods.

See https://git.drupalcode.org/project/entity_hierarchy/-/blob/3.x/modules/e... for an example

So we also need tests for that

longwave’s picture

Status: Needs work » Needs review
Issue tags: -Needs tests

@larowlan I added a test that is similar to your trait setup but it passed first time, so not sure what I have done wrong - any ideas?

larowlan’s picture

I'll see if I can still replicate it - might have been a PHP version issue?

longwave’s picture

For full backward compatibility we might need to provide an autowiring trait, which perhaps means we can simplify some of this.

class MenuLinkDefault extends MenuLinkBase implements ContainerFactoryPluginInterface {

  public function __construct(array $configuration, $plugin_id, $plugin_definition, StaticMenuLinkOverridesInterface $static_override) {
    ...
  }

  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    ...
  }
}

In the long run we probably want to drop the create method here and use autowiring instead, but then we are no longer implementing ContainerFactoryPluginInterface, and any subclasses will no longer have their create() method called.

So maybe the answer is to autowire the constructor, and use a trait to replace create() that handles the autowiring? Then any subclasses will work with no changes - we could also find a way to notify downstream users that they should switch to autowiring, or maybe we just live with the trait forever until we deprecate ContainerFactoryPluginInterface entirely?

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

smustgrave’s picture

Status: Needs review » Needs work

Sounds like this should be NW for the trait mentioned in #19?

duadua made their first commit to this issue’s fork.

bradjones1’s picture

Marked #2053415: Allow typed data plugins to receive injected dependencies as a duplicate as well. Would be great to make some traction on this by answering the questions raised in #19, or perhaps this just needs an IS update to reflect the current status after the recent MR changes.

longwave’s picture

Status: Needs work » Needs review
StatusFileSize
new2.98 KB

@duadua Some great ideas there. I especially like the idea of doing the reflection at discovery time instead of runtime, though that needs a bit more thought on how to architect and store the necessary data.

Following these issues:
#3049525: Enable service autowiring by adding interface aliases to core service definitions
#3325557: Enable more service autowiring by adding interface aliases to core modules
it is possible to infer the correct service in many cases without needing the #[Autowire] attribute. Therefore we can implement a trait that autowires based on the type where possible, or the attribute where it isn't possible, and this seems fairly trivial. I don't think we have to worry about inheritance even? Subclassed plugins will continue to provide their own create() method, parent or child classes can switch to the trait any time, as far as I can see - this might need a bit of testing though.

Proof of concept patch attached.

smustgrave’s picture

Status: Needs review » Needs work

If a new approach is being taken could the IS be updated?

Will new approach still require the change record? Leaving tag just in case.

joachim’s picture

Can this be expanded to all classes that have the container factory pattern -- forms and controllers IIRC.

longwave’s picture

Yes, but probably not in this issue; I picked plugins first as I was getting annoyed keeping create() methods in sync with the constructor but those are valid candidates as well.

joachim’s picture

Forms have exactly the same pattern of create() and __construct(), just use Drupal\Core\DependencyInjection\ContainerInjectionInterface instead.

longwave’s picture

But the plugin factory method and constructors have three additional arguments - array $configuration, $plugin_id, $plugin_definition - so I don't think we can use the same code/trait.

acbramley’s picture

donquixote’s picture

So, the trait in the patch does the job. +1.

Is there any benefit of doing it in ContainerFactory instead of using the trait?

donquixote’s picture

Actually, migrate plugins get an additional parameter passed to the ::create() method.
In MigratePluginManager:

      $plugin = $plugin_class::create(\Drupal::getContainer(), $configuration, $plugin_id, $plugin_definition, $migration);

The positional mapping with 3 parameters would lose this 4th parameter.

But technically there is no guarantee that calling code will always pass the `$migration` parameter.
The 4th parameter is optional, to comply with LSP.

I think such cases need a custom trait with 4 parameters instead of 3.
The trait can be chosen per class, according to the needs of the constructor.
E.g. some migrate plugins might not need the `$migration`, then they can use the regular trait.

joachim’s picture

> But the plugin factory method and constructors have three additional arguments - array $configuration, $plugin_id, $plugin_definition

Field formatter plugins have more base constructor arguments too. I know ALL about this problem from Module Builder.... :/

dpi’s picture

Created #3452852: Add create() factory method with autowired parameters to PluginBase, as an interim DX measure for all existing (legacy) plugins.

elc’s picture

As a step forward, would it be better to push the AutowirePluginTrait [1] in the lead up to D11, as it is so alike the services Drupal\Core\DependencyInjection\AutowireTrait ?

[1] Drupal\Core\DependencyInjection\AutowirePluginTrait
#3452852: Add create() factory method with autowired parameters to PluginBase

[2] Drupal\Core\DependencyInjection\AutowireTrait
AutowireTrait allows ContainerInjectionInterface classes to be autowired

This would seem to be in keeping with the current thrust of development. There would also need to be a specialist Trait for the migration plugins which require 4 parameters.

What would be the next steps needed? Core tests: one that shows it working the old way, and one that shows it working the new?

acbramley’s picture

Agree that the Trait would be awesome

longwave’s picture

Urgh, the field formatters case is pretty horrible.

  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $plugin_id,
      $plugin_definition,
      $configuration['field_definition'],
      $configuration['settings'],
      $configuration['label'],
      $configuration['view_mode'],
      $configuration['third_party_settings'],
      $container->get('entity_type.manager'),
      $container->get('language_manager')
    );
  }

Shouldn't that extraction work be done in the constructor instead? But changing all existing constructors and providing BC is going to be no fun at all.

solideogloria’s picture

Shouldn't that extraction work be done in the constructor instead?

Agreed. Just pass the $configuration to the constructor.

kim.pepper’s picture

I did a quick count and there are 92 sub-classes of FormatterBase in core alone.

catch’s picture

I was a bit worried about extra attribute parsing etc. but looking at the MR, it only happens when we need want to instantiate the plugin anyway so any overhead should be negligible.

bradjones1’s picture

Drive-by idea: Is the autowiring logic in Symfony able to be extrapolated/re-used here to basically allow for autowiring similar to services? It's the same thing... except these aren't first-class services. Or am I way off?

longwave’s picture

Status: Needs work » Needs review

Following #40 I've had an idea for a while that I've now managed to test out, and it's looking quite promising. We can do the reflection at discovery time and store the set of required services for plugins in the plugin definition, then use that metadata in the plugin factory to skip the create() method. MR!11031 has a prototype of this. We do instantiate quite a few plugins on some page loads so storing the metadata in advance and avoiding reflection at runtime would be good.

I think with this technique we can work around the varying requirements of constructors of different plugin types by passing in more metadata into the discovery and/or extending the factory.

longwave’s picture

We can't just delete ::create() straight away, because a subclass might be calling it. But for plugins that can be autowired, what about flagging ::create() as #[\Deprecated] (or a similar custom attribute)? This would indicate both that we intend to delete it in the next major (and so we don't need to notify directly that autowiring is available) but any subclasses *should* be notified because it will break in future? Subclasses can then also implement the attribute themselves, or just convert to autowiring.

godotislate’s picture

This was mentioned in #11, but it's been common practice to use ::create() in plugin classes that extend other plugin classes, to avoid issues if the parent constructor's signature changes. I think before deprecating ::create(), at least the suggestion from #12 to accommodate autowired setter injection (presumably with the use of the the \Symfony\Contracts\Service\Attribute\Required atribute) should be done in a follow up.

donquixote’s picture

For the inheritance problem:
I just found out that we can put attributes on variadic parameters :)

class B {
  public function __construct(
    protected readonly ServiceForB $serviceForB,
  ) {}
}

class C extends B {
  public function __construct(
    protected readonly ServiceForC $serviceForC,
    #[AutowireParentArgs]
    mixed ...$parent_args,
  ) {
    parent::__construct(...$parent_args);
  }
}

If we could create this attribute or find out that it already exists somewhere...

mxh’s picture

Running the reflection scan on possibly thousands of plugin definitions where only a fraction of them make use of it looks like a disproportionate resource overhead. Why not handling this similar to Annotations -> Attributes transition? For sure once autowiring is possible, more and more plugin implementations will adapt, but it's a matter of months if not years to come. I'd introduce an Autowire interface for plugins similar as ContainerFactoryPluginInterface to quickly identify which plugin is making use of it. For comparison, services also need an additional autowire: true declaration in order to make use of it. And there may be way more plugin definitions than service definitions around.

longwave’s picture

Fixed some more tests. But we have some interesting cases to solve:

  1. ContentModerationHooks::actionInfoAlter() swaps the class of two action plugins. But these have different constructor signatures and factory methods, so we need to ideally parse the constructor after alter hooks have run. But maybe someone also wants to alter the autowiring metadata?
  2. NavigationMenuBlock extends SystemMenuBlock but passes in navigation.menu_tree which is an alternative implementation of MenuLinkTreeInterface. This must be explicitly declared; I think some way of opting in plugins is the only way to solve this.

Perhaps we need to add an autowire property to Drupal\Component\Plugin\Attribute and all subclasses? This doesn't solve point 1, however.

longwave’s picture

I think to solve #48.1 we need to drop the decorator - instead move the autowiring discovery to the factory, and call it as a kind of factory-preparation step that follows discovery and alter.

longwave’s picture

Looking for reviews/opinions on whether adding autowire as a boolean property to every plugin definition is the correct solution here. This lets plugins opt in individually which solves #47, and I now think may be required as per #48.2, because we can't guarantee to infer correctly in all cases from interfaces alone.

If we do do this I also think we have to keep the create() methods around for now because there might be subclasses calling parent::create(). There is an attempt in the MR at a BC layer for this, because I think we can and should start removing the interface when the plugins are autowired, but the interface might not be explicitly added to subclasses.

joachim’s picture

> Looking for reviews/opinions on whether adding autowire as a boolean property to every plugin definition is the correct solution here.

Dumping it in the plugin definition seems wrong, when it's something that's only needed internally, and not by any code using the plugin definition.

catch’s picture

Looking for reviews/opinions on whether adding autowire as a boolean property to every plugin definition is the correct solution here.

1. Could we put it in its own attribute per #51?

2. Could we flip the default in a future major release? Seems worth a spin-off issue even if we eventually decide not to do that.

longwave’s picture

We put the class name in the plugin definition, it's not too dissimilar from that - something that is needed in order to construct the plugin? To me the plugin definition is any metadata that's required before the plugin needs to be constructed, and the autowire flag fits into that category. We also need to store the set of services somewhere - if not on the plugin definition, then where?

It would be nice to reuse the existing #[Autowire] attribute from Symfony on the class, but that's only allowed on parameters:

#[\Attribute(\Attribute::TARGET_PARAMETER)]
class Autowire

If we do go this route this would mean having a new name - #[AutowiredPlugin]?

We can flip the default either way, either globally or on a per-definition basis I think. But we haven't done that for services, I don't know if we would do it here or not.

smustgrave’s picture

Status: Needs review » Needs work

Tried to the resolve the threads I could.

Maybe the CR well help clear up the others?

longwave’s picture

#3452852: Add create() factory method with autowired parameters to PluginBase landed, which implements this at runtime instead of discovery time. I think we can continue here with adding the reflection step at discovery time and using that data if it's available instead of needing reflection at runtime.

godotislate’s picture

There's work started in #3544405: [PP-1] Replace SearchBlock service properties with service closures to use closures in order to inject services lazily into plugin classes (the search block, in this case). I wonder if it makes sense to expand on #3452852: Add create() factory method with autowired parameters to PluginBase to accommodate the use of the AutowireServiceClosure attribute as well. In which case, does it make sense to do this first and do the reflection at discovery time, and then add autowiring for service closures? Or can the two tasks be done independently?

Note that to use service closures in plugin classes #3544994: Make service closures serializable in classes using DependencySerializationTrait will need to land as well.

longwave’s picture

I think they can be done independently? What I'm kinda proposing here is splitting up the reflection and instantiation parts so they are done at different times, but as long as both parts understand about closures, it doesn't matter whether the reflection is done at discovery or runtime.

longwave’s picture

Title: Allow plugin service wiring via constructor parameter attributes » Improve plugin autowiring performance by moving reflection to discovery time

Retitling this as I think we can narrow the scope a bit.

longwave’s picture

Status: Needs work » Needs review

Needs tests, but MR!13493 should improve performance by handling reflection at discovery time for plugins autowired via PluginBase::create().

longwave’s picture

Can't reproduce those three fails locally, all those tests pass for me.

berdir’s picture

I think we'll definitely need to carefully compare both build and runtime. This also adds to the plugin definition cache size and it means more things to unserialize, for all plugin definitions on all requests. I've seen huge block plugin caches, so this might cost us more than we gain since it's for classes that we'll load and instantiate anyway. If you have lots of content_blocks or lots of menus, then we'd add those definitions many times.

longwave’s picture

Maybe we need a separate map object where we store the arguments once against the class name. Which when I write it out, sounds quite like a container!

kim.pepper’s picture

My 2 cents... if we are injecting services at discovery time, what advantage does this have over regular container services?

bradjones1’s picture

Re: #62, At the risk of sounding like a broken record about my favorite (kinda stalled) core feature... if we use JSON data columns for this kind of caching we can avoid serialization entirely and index on whatever paths need rapid access? #3343634: Add "json" as core data type to Schema and Database API

dcam’s picture

Status: Needs review » Needs work
Issue tags: +Needs issue summary update, +Needs tests

The issue summary still talks about desire to add autowiring to classes, which landed some time ago and is mentioned in #55. Since the issue's focus has changed the summary needs to be updated. MRs that are no longer relevant need to be closed. I'm also adding the Needs Tests tag per #60.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.