Allow to tag services as "configurator" for other services.

Created on 3 May 2023, about 1 year ago
Updated 9 June 2023, about 1 year ago

Problem/Motivation

Currently when you use a service collector with tagged services, a method on the "consumer" service will be called one time for each tagged service.
This works fine for the basic case when the tagged service itself is the thing that should e.g. respond to some kind of event.
The same also applies to basic symfony event listeners.

However, sometimes what you want to add to the collector/consumer is not the tagged service itself, but some other custom object or value, which itself might not even be registered in the container.

Steps to reproduce

Proposed resolution

For this purpose, it would be useful for the tagged service to act as a "configurator" on another service.

class ConfiguredService {
  public function addHandler($handler) {..}
  public function setColor($color) {..}
}

class ConfiguratorService {
  public function configureConsumer(ConfiguredService $service): void {
    $service->addHandler(new CustomObject(...));
    $service->setColor('blue');
  }
}
services:
  configured_service:
    class: ConfiguredService
  configurator_service:
    class: ConfiguratorService
    tags:
      - { name: configurator, method: configureConsumer, target: configured_service }

One limitation with the "configurator" mechanism is that there can be only one configurator per service.
But this could be circumvented by having a multicast configurator service.

Note:
I did not find anything equivalent in symfony, but perhaps I did not look hard enough.

Remaining tasks

Check if a similar system already exists somewhere in symfony.
Collect more possible use cases.

User interface changes

API changes

Data model changes

Release notes snippet

✨ Feature request
Status

Postponed

Version

11.0 πŸ”₯

Component
BaseΒ  β†’

Last updated about 1 hour ago

Created by

πŸ‡©πŸ‡ͺGermany donquixote

Live updates comments and jobs are added and updated live.
  • Needs change record

    A change record needs to be drafted before an issue is committed. Note: Change records used to be called change notifications.

Sign in to follow issues

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • Issue created by @donquixote
  • πŸ‡©πŸ‡ͺGermany donquixote

    All we need is a compiler pass. Proof of concept PR will follow :)

    { name: configurator, method: configureConsumer, target: configured_service }

    The method and target could be optional.
    Instead, a method attribute could be used.

    class ConfiguratorService {
      #[Configurator(target: 'configured_service')]
      public function configureConsumer(ConfiguredService $service): void {
        $service->addHandler(new CustomObject(...));
        $service->setColor('blue');
      }
    }
    

    If the service id is a class or interface name, then the "target" parameter could be omitted, instead the target id would be "ConfiguredService" as the parameter type.

    The mechanism would also allow decorators, if the parameter is by reference:
    (Actually I am not sure if this is meant to be supported.)

    class ConfiguratorService {
      public function configureConsumer(ConfiguredService &$service): void {
        $service = new ConfiguredServiceDecorator($service);
      }
    }
    
  • Open on Drupal.org β†’
    Environment: PHP 8.1 & MySQL 5.7
    last update about 1 year ago
    Not currently mergeable.
  • @donquixote opened merge request.
  • last update about 1 year ago
    Custom Commands Failed
  • @donquixote opened merge request.
  • πŸ‡©πŸ‡ͺGermany donquixote

    (I did not like the branch name of the first MR)

  • πŸ‡©πŸ‡ͺGermany donquixote

    34 | WARNING | Code after the THROW statement on line 30 cannot be
    | | executed
    | | (Squiz.PHP.NonExecutableCode.Unreachable)

            $method_name = $configurator_tag['method']
              ?? throw new LogicException(sprintf(
                "Missing 'method' key on 'configurator' service tag on '%s'.",
                $id,
              ));
            $target_id = $configurator_tag['target'] ?? NULL;
    

    PhpCS does not like null coalesce + throw. Sad!

  • last update about 1 year ago
    Custom Commands Failed
  • last update about 1 year ago
    Custom Commands Failed
  • last update about 1 year ago
    29,375 pass
  • πŸ‡©πŸ‡ͺGermany donquixote

    One possible flaw of this mechanism is that the "subscribers" or "configurators" are directly coupled to the specific service they configure, instead of just to a service tag.

    This means when changing the configured service id or its configuration method signature, one also has to change all the configurator services.

    I think this flaw is acceptable in situations where we only want to add a few configurators.

  • @donquixote opened merge request.
  • last update about 1 year ago
    29,375 pass
  • πŸ‡©πŸ‡ͺGermany donquixote

    Ok, this is it for now :)
    The mechanism works, there is a basic test.
    If we want this we can improve and refine the details.

    Obviously, the second PR with the 3357916-service-configurator-tag-proof-of-concept branch is _NOT_ what we want to merge.

    My own current use case has changed, so for now I am going with a different solution.
    And anyway, if I needed this urgently right now I would probably do it in custom or contrib code.

  • Status changed to Needs review about 1 year ago
  • Status changed to RTBC about 1 year ago
  • πŸ‡ΊπŸ‡ΈUnited States smustgrave

    Lets get this in front of a committers eye.

    Assuming if it gets green lit there will need to be a change record.

  • last update about 1 year ago
    29,421 pass
  • πŸ‡¬πŸ‡§United Kingdom longwave UK

    I am wary of adding this if we have no actual use case; given Symfony does not have this feature and @donquixote found another way in their own code, do we really need this in core?

  • πŸ‡©πŸ‡ͺGermany donquixote

    I agree, we should put this on hold until we have more use cases.
    The PR shows that it can be done, but this does not mean we have to.

    I already looked into existing code in Drupal core, to find cases that could have been done with this feature.
    There were some, but there was not the one super convincing example where it would make a significant difference.

    Also, something similar can be achieved with the decorator feature:
    You can register a decorator with a factory, and that factory, instead of creating a new instance wrapping the original instance, can simply call a setter method and return the old instance.

  • Status changed to Postponed about 1 year ago
  • πŸ‡¬πŸ‡§United Kingdom longwave UK

    Marking as postponed to get this out of the RTBC queue; if anyone finds that this is useful to them please comment and we can consider it again for core.

  • πŸ‡ΊπŸ‡ΈUnited States smustgrave

    @longwave thanks for taking a look!

Production build 0.69.0 2024