Introduce an applicable plugin API

Created on 4 January 2025, 3 months ago

Problem/Motivation

A pattern I found myself using a lot lately, when having to support multiple strategies to fulfill a requirement, involves defining a set of plugins, each one implementing a strategy. Depending on the logic being implemented, the first strategy applying to the current context or all strategies applying to it would be run.

A PoC implementation of the (reusable parts of) this pattern are available here.

Proposed resolution

Remaining tasks

  • Validate the proposed solution
  • Evaluate the PoC code.
  • Create a MR
  • Review the MR

Introduced terminology

  • Applicable plugin: a plugin applying to a certain context.
  • Weighted plugin: a plugin having a numeric weight as part of its definition.

API changes

No change, only additions.

Release notes snippet

TBD

Feature request
Status

Active

Version

11.0 🔥

Component

plugin system

Created by

🇮🇹Italy plach Venezia

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

  • Issue created by @plach
  • 🇮🇹Italy plach Venezia
  • 🇮🇹Italy plach Venezia
  • 🇮🇹Italy plach Venezia
  • 🇮🇹Italy plach Venezia
  • 🇦🇺Australia larowlan 🇦🇺🏝.au GMT+10

    This sounds similar to layout builder's section storage plugins which use context definitions to find matching plugins but don't have a way to order/weight plugins if there are multiple matches.
    Experience builder could also use something similar

  • 🇬🇧United Kingdom catch

    Also sounds similar-ish to breadcrumb builders (currently tagged services with an ::applies() method), a pattern which I recently copied for a contrib module. Not sure if it's actually applicable to breadcrumb builders but it did remind me of them.

  • 🇨🇭Switzerland berdir Switzerland

    IMHO: A primary reason for using plugins is to manage multiple instances and configuration for them, something that by definition isn't really needed for first-applicable-implementation wins.

    Tagged services seem sufficient for this, breadcrumb builders is a valid example, so is for example the theme negotiator. It's pretty well standardized using the service_id_collector tag, see theme.negotiator and services tagged with theme_negotiator. Also supports priority.

  • 🇦🇺Australia larowlan 🇦🇺🏝.au GMT+10

    One thing plugins have that tagged services don't is derivatives.

  • 🇮🇹Italy plach Venezia

    Thanks for the feedback :)

    @catch:

    Yep, and of course we have field widgets and formatters that have a similar isApplicable method. The main difference is that it's static. Having to instantiate the plugin to check its applicability is by design in the PoC code, since the logic may require to do more than just evaluating definition values.

    @berdir:

    Tagged services seem sufficient for this, breadcrumb builders is a valid example, so is for example the theme negotiator. It's pretty well standardized using the service_id_collector tag, see theme.negotiator and services tagged with theme_negotiator. Also supports priority
    

    Good point. I think there was a similar conversation when discussing the implementation of the language negotiation system: in the end we went with plugins but tagged services would likely have worked just as fine. At the time I was concerned with not burdening the DIC with too many services, but this is less of a concern.

    @larowlan:

    One thing plugins have that tagged services don't is derivatives.

    Another good point :)

    I definitely leveraged derivatives in combination with applicable plugins in one of my client projects. Being able to write custom logic for each plugin implementation but also to rely on derivatives for simpler strategies varying only by some parameter value (stored in the plugin definition) was definitely useful.

    Which is my main objection to @berdir's point:

    A primary reason for using plugins is to manage multiple instances and configuration for them, something that by definition isn't really needed for first-applicable-implementation wins.
    

    It's definitely true that I never relied on plugin configuration in the client projects that are using the PoC code :)

  • 🇨🇭Switzerland berdir Switzerland

    Yes, I was just thinking about language negotiation stuff while I wrote my comment, some of them actually do have some configuration, but it's configuration that's stored globally in a single place. Can't have differently configured negotiator plugins.

    You can kind of can register multiple services with a service provider, instead of the plugin definition you could pass in that logic in the constructor. But container-build time is trickier to manage, many things aren't fully available yet then. But you could also loop over something within the service itself. Coming back to the configuration point, if you don't need to manage differently configured variants, that should be fairly easy.

  • 🇮🇹Italy plach Venezia

    You can kind of can register multiple services with a service provider, instead of the plugin definition you could pass in that logic in the constructor. [...] But you could also loop over something within the service itself.

    I can imagine passing something analogous to a plugin definition stored in the .services.yml file via service arguments, but I'm not sure I follow your suggestions: would you mind posting some pseudo-code? :)

  • 🇨🇭Switzerland berdir Switzerland

    I'm not saying it's a good idea just that it's possible:

          foreach (range(1, 5) as $i) {
          $container->register('mymodule.foo.' . $i, Foo::class)
            ->addTag('tag_foo')
            ->addArgument($i);
         }
    

    But with my follow-up sentence, what I meant is that with plugins, having derivative plugins gives you distinct, reference things, e.g. specific menu blocks that you can place and use multiple times with different configuration. If all you care about is that any of the plugins take care of it, you don't need and you can just loop over your variations in your applies() of a single class.

Production build 0.71.5 2024