I'm working on a custom module (my_module) which provides some functionality to a specific paragraph type (my_paragraph) created from the user interface. In my routing file, I have the following route definition.

  path: '/node/{node}/my_paragraph/{paragraph}'
    _controller: '\Drupal\my_module\Controller\MyModuleController::action'
    _title: 'page title'
    _permission: 'access content'

Now I add a new entity reference field (field_paragraph) to the Basic page content type. Using this field I can add my_paragraph along with other paragraph types.

With this set up, I create a basic page, add a piece of my_paragraph, and saved it (/node/123). I can see my custom controller is not called (because now the path is /node/123), but if I go to /node/123/my_paragraph/1, my custom controller is getting called there and I get the output I want.

  1. What should I do so that my custom controller can be used on /node/123 page?
  2. If I add /about as path alias of /node/123, how do I achieve the same outcome?

I'm actually refactoring an old custom module I wrote and trying to move away from the hooks approach if possible. One of the main motivations for me by refactoring is I can use one or more services (together with dependency injections) to separate all the possible logics here.


The problem here is that controllers are specifically intended to return a result based on a given path. The result you want isn't dependent on a path, but rather on an operation performed on an entity.

What you'll want to use instead is one of the entity API hooks, found here: Entity CRUD, editing and view hooks

To respond to your entity being created, I recommend using: hook_ENTITY_TYPE_insert()

function my_module_node_insert($node) {
  // Get ids for all Paragraph references stored in your field.
  $paragraph_ids = $node->get('field_paragraph')->getValue();
  // Get Paragraph storage handler.
  $paragraph_storage = \Drupal::entityTypeManager()->getStorage('paragraph');
  // Load Paragraphs from their IDs.
  $paragraphs = $paragraph_storage->loadMultiple( array_column($paragraph_ids, 'target_id') );

  // Iterate through your loaded Paragraph instances...
  foreach($paragraphs as $paragraph) {
    // Do your stuff to the paragraph.

Similarly, if you need to update field_paragraph when you update your node, implement hook_ENTITY_TYPE_update().

| improve this answer | |
  • Thanks! Sorry I just added a note to my post. I still want it done in a controller but will take yours if that is not possible. – cchen May 9 at 21:34
  • 1
    Good on you for seeking a more OO route! :) As of Drupal 8.8.x, though, hooks are still the de facto method for reacting to entity CRUD events. If they weren't, then the links I included would all be deprecated. There is an effort to migrate entities to an event-subscriber system, but it's not yet fully baked: drupal.org/project/drupal/issues/2551893. I'll finish by pointing out that code doesn't exist in a vacuum, and other devs may need to maintain your code (you should always pretend that would be the case). Hooks are a very familiar and time-tested entity CRUD mechanism. – Beau May 9 at 21:49

In Drupal 8, routes and hooks have different purposes. Routes are for rendering a page containing information, while hooks are used to react to an event or to alter the information returned from other modules.

I assume you need to show information, and a controller is really what you need. (The rest of the answer will consider the case you really need a hook.)

The first point to keep in mind is that each path can only have a single controller. When Drupal is showing the page at /node/123, for example, only the controller that renders the node view is called. No other controllers will be used.
That doesn't mean the node can just be viewed and not, for example, edited, deleted, or translated. Drupal shows tabs that allow to take different actions on that node, or to show information that is related to that node.


(The screenshot is taken from the Drupal Umami Demo.)

Defining a tab controller it's done in two steps. (I will show what Drupal core does for /node/{node}/revisions.)

  • The route is defined in the .routing.yml file

      path: '/node/{node}/revisions'
        _title: 'Revisions'
        _controller: '\Drupal\node\Controller\NodeController::revisionOverview'
        _access_node_revision: 'view'
        node: \d+
         _node_operation_route: TRUE

    (Some of the values are very specific for that route.)

  • The .links.task.yml file define the route as tab link and where it should be shown (the base_route property)

      route_name: entity.node.version_history
      base_route: entity.node.canonical
      title: 'Revisions'
      weight: 20

If instead what you need is really a hook, then you just need to implement it. As for using a more OOP approach, the Content Moderation module shows how to achieve that, for example, in its content_moderation_entity_insert().

function content_moderation_entity_insert(EntityInterface $entity) {
  return \Drupal::service('class_resolver')

The EntityOperations class is merely a class implementing ContainerInjectionInterface and it can therefore use Dependency Injection.

| improve this answer | |
  • Thank you! Great information! – cchen May 11 at 13:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.