Score:0

Multiple Custom Module Event Subscribers Override Each Other

et flag

I've been working on custom modules that alter RequestEvents based on the entity type provided by the route.

The functionality from one entity to another doesn't change, but the data does, hence I split the module. The working example extends a class provided by a base module that holds the core functionality.

I'm not sure if it matters, be it run order precedence or what have you, but the names are akin to my_module_nodes and my_module_taxonomy_terms.

When I have just my_module_nodes installed, it works perfectly. When I install my_module_taxonomy_terms alongside it, my_module_taxonomy_terms appears to override the functionality of my_module_nodes.

I've debugged by logging to Watchdog the routes I'm checking against with both modules enabled, and it checks for the route defined in my_module_taxonomy_terms twice, rather than checking for the routes defined in each individual module. E.g. I would expect to see entity.node.canonical logged as well as entity.taxonomy_term.canonical, but I see entity.taxonomy_term.canonical logged twice per page load.

Note that I have attempted this with static strings in the __construct method as opposed to using arguments from the services file, but nothing changing. Also note that installation order does not change the result - my_module_taxonomy_terms always overrides my_module_nodes.

One of the two submodule event subscribers in question - identical to the other besides the data method.

/custom/my_module_base/src/MyModuleBaseSubscriberBase.php

namespace Drupal\my_module_base;

use Drupal\Core\Cache\CacheableJsonResponse;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Routing\CurrentRouteMatch;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;

/**
 * @package Drupal\my_module_base\JsonEntitySubscriberBase
 */
class MyModuleBaseSubscriberBase implements EventSubscriberInterface {
  
  /** @var \Drupal\Core\Cache\CacheBackendInterface */
  protected $cacheBackend;
  
  /** @var \Drupal\Core\Routing\CurrentRouteMatch */
  protected $routeMatch;

  /** @var string */
  protected $routeName;

  /** @var string */
  protected $routeParameter;
  
  /**
   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
   *   The "cache.render" cache backend service.
   * @param \Drupal\Core\Routing\CurrentRouteMatch $route_match
   *   The "current_route_match" route match interface service.
   * @param string $route_name
   *   The Name of the route to check against.
   * @param string $route_parameter
   *   The Name of the route parameter to get.
   */
  public function __construct(CacheBackendInterface $cache_backend, CurrentRouteMatch $route_match, string $route_name, string $route_parameter) {
    $this->cacheBackend = $cache_backend;
    $this->routeMatch = $route_match;
    $this->routeName = $route_name;
    $this->routeParameter = $route_parameter;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('cache.render'),
      $container->get('current_route_match'),
      $container->get('route.name'),
      $container->get('route.parameter'),
    );
  }
  
  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents() {
    $events[KernelEvents::REQUEST][] = ['onRequest', 29];
    return $events;
  }

  /**
   * @param \Symfony\Component\HttpKernel\Event\RequestEvent $event
   *   The current RequestEvent object.
   */
  public function onRequest(RequestEvent $event) {
    $request = $event->getRequest();
    $route_match = $this->routeMatch->getRouteMatchFromRequest($request);

    if ($route_match->getRouteName() !== $this->routeName) {
      return;
    }

    $event->setResponse($this->getResponse(
      $route_match->getParameter($this->routeParameter)
    ));
  }

  // Other functionality...

  /**
   * @param mixed $entity
   * @return array
   */
  protected function getData(mixed $entity): array {
    return [
      'type' => $entity->getEntityTypeId(),
      'id' => $entity->id(),
    ];
  }

}

One of the two submodule event subscribers in question - identical to the other besides the data method.

/custom/my_module_base/modules/my_module_nodes/src/EventSubscriber/MyModuleNodesSubscriber.php

namespace Drupal\my_module_nodes\EventSubscriber;

use Drupal\my_module_base\MyModuleBaseSubscriberBase;

/**
 * @package Drupal\my_module_nodes\EventSubscriber
 */
class MyModuleNodesSubscriber extends MyModuleBaseSubscriberBase {
  /**
   * {@inheritdoc}
   */
  protected function getData(mixed $entity): array {
    return [
      'type' => $entity->getEntityTypeId(),
      'id' => $entity->id(),
      // Other values...
    ];
  }
}

One of the two submodule service files in question - identical to the other besides the parameters values.

/custom/my_module_base/modules/my_module_nodes/my_module_nodes.services.yml

parameters:
  route.name: entity.node.canonical
  route.parameter: node

services:
  my_module_nodes.subscriber:
    class: \Drupal\my_module_nodes\EventSubscriber\MyModuleNodesSubscriber
    arguments: ['@cache.render', '@current_route_match', '%route.name%', '%route.parameter%']
    tags:
    - { name: event_subscriber }

I've been tinkering on this project over the past week after work and I'm at a bit of a loss. Is there any other relevant information I should include? Thanks!

Score:1
cn flag

If you have the same container parameter name in different modules the module last in alphabetic order will overwrite previous values. The solution would be to prefix the parameter name with the module name if it is specific to this module.

But why use parameters when you can use string literals?

my_module_nodes.services.yml

services:
  my_module_nodes.subscriber:
    class: \Drupal\my_module_nodes\EventSubscriber\MyModuleNodesSubscriber
    arguments: ['@cache.render', '@current_route_match', 'entity.node.canonical', 'node']
    tags:
    - { name: event_subscriber }
FrankieD avatar
et flag
Ah, makes sense! Honestly, I'm not sure how to get an unnamed argument. The documentation I've found on the `get()` method is fairly sparse, though it seems to expect a named argument. E.g. `$container->get('cache.render')`. How would I go about achieving this?
4uk4 avatar
cn flag
For a container parameter you would need `$container->getParameter()`, but the create() method in your service class has no function, it's not used when the service is instantiated. The arguments in *.services.yml are the input of __construct() and it's no problem to feed __construct() with string literals.
FrankieD avatar
et flag
That did it. Thanks for clearing that up.
I sit in a Tesla and translated this thread with Ai:

mangohost

Post an answer

Most people don’t grasp that asking a lot of questions unlocks learning and improves interpersonal bonding. In Alison’s studies, for example, though people could accurately recall how many questions had been asked in their conversations, they didn’t intuit the link between questions and liking. Across four studies, in which participants were engaged in conversations themselves or read transcripts of others’ conversations, people tended not to realize that question asking would influence—or had influenced—the level of amity between the conversationalists.