6

Let's say I use a hook with a lot of code inside. To improve readability and reusability I'd like to use helper/private methods. However, a Drupal .module file is not a class.

The idea is to split big methods from this file into more readable and reusable smaller methods.

What is the correct way to call helper/private methods from a Drupal .module file?

4
  • 2
    Create a class with a public method, instantiate it in your procedural code, and then have that public method call a private method?
    – mradcliffe
    Sep 26, 2018 at 15:05
  • This isn’t a Drupal question really.
    – Kevin
    Sep 26, 2018 at 16:08
  • 1
    Since Drupal hook system force us to use a .module file, I thought this question would be relevant here to help Drupal users who wants to improve their code for hooks. The idea is to split big methods from this file in more readable and reusable smaller methods. May be I should have named it helper methods. Anyway, if there is some good practices to handle that, I even think that could be added in the doc for hooks.
    – Kwadz
    Sep 27, 2018 at 8:54
  • 5
    @Kwadz I would agree - while the concept you're talking about (SoC) is a very generic one, Drupal does have specific things in it that you should use to solve this. Specifically, create a service class, put the functionality in there, and consume it in the thinnest way possible in your hook. Then you can test the service class separately, and your hook is just a dumb proxy for the information it receives (which is arguably what you want)
    – Clive
    Sep 27, 2018 at 9:28

4 Answers 4

12

I tend to follow Symfony standards when it comes to helper functions. So I normally add a class under src/Utils for helper methods.

Then I create a service for it that I can inject other services to and that itself can be injected everywhere I need it. It can also be used in procedural code (hooks) in *.module or *.theme files easily.

web/modules/custom/my_module/src/Utils/MyHelperFunctions.php

namespace Drupal\my_module\Utils;
    
/**
 * Class MyHelperFunctions.
 */
class MyHelperFunctions {

  /**
   * The Site path.
   *
   * @var string
   */
  protected $sitePath;

  /**
   * MyHelperFunctions constructor.
   *
   * @param string $site_path
   *   The site path.
   */
  public function __construct($site_path) {
    $this->sitePath = $site_path;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('site.path')
    );
  }
    
  /**
   * Get current multisite directory name.
   *
   * @return string
   *   The basename of the matching multisite directory.
   */
  public function getMultisiteAlias() {

    $site_path = $this->sitePath;
    $site = explode('/', $site_path);

    return $site[1];
  }
    
}

web/modules/custom/my_module/my_module.services.yml

services:
  my_module.helper_functions:
    class: Drupal\my_module\Utils\MyHelperFunctions
    arguments: ['@site.path']

web/modules/themes/custom/my_theme/my_theme.theme

/**
 * Implements template_preprocess_html().
 */
function my_theme_preprocess_html(&$variables) {

  // Add multisite body class.
  $site = \Drupal::service('my_module.helper_functions')->getMultisiteAlias();
    
  $variables['attributes']['class'][] = 'site-' . $site;
}

Read more: When should I create a service or a utility function?

1
  • Am not friend of helper functions, but when am in need, I go for traits instead.
    – ssibal
    Sep 27, 2018 at 11:20
2

For people who prefer to use OOP, instead of using the traditional Drupal hook system, we can use the event system using Hook Event Dispatcher module which dispatches events for several Drupal core hooks.

May be all hooks will be replaced in Drupal as discussed here and here.

0

In our project we create classes for hooks.

/**
 * Implementation of hook xyz.
 */
function my_module_some_hook($arguments) {
  MyHookClass::create(\Drupal::getContainer())->myMethod($arguments);
}

In your class you just need to implement ContainerInjectionInterface, and you are good to go. You can do anything in OO now, nevertheless you can also write unit tests for it if needed.

1
  • 2
    Would you be able to improve your answer by providing the implementation of MyHookClass with some explanations?
    – Kwadz
    Sep 27, 2018 at 12:03
0

I would either put the code in a service, as leymannx said in his answer, or do like two Drupal core modules do: Put the code in a class that is retrieved and instantiated with \Drupal::service('class_resolver')->getInstanceFromDefinition($class_name).

That is what the Content Moderation module do in some of its hooks.

function content_moderation_entity_base_field_info(EntityTypeInterface $entity_type) {
  return \Drupal::service('class_resolver')
    ->getInstanceFromDefinition(EntityTypeInfo::class)
    ->entityBaseFieldInfo($entity_type);
}
function content_moderation_entity_update(EntityInterface $entity) {
  return \Drupal::service('class_resolver')
    ->getInstanceFromDefinition(EntityOperations::class)
    ->entityUpdate($entity);
}
class EntityOperations implements ContainerInjectionInterface {

  // …

  /**
   * Constructs a new EntityOperations object.
   *
   * @param \Drupal\content_moderation\ModerationInformationInterface $moderation_info
   *   Moderation information service.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   Entity type manager service.
   * @param \Drupal\Core\Form\FormBuilderInterface $form_builder
   *   The form builder.
   * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info
   *   The entity bundle information service.
   * @param \Drupal\Core\Routing\RouteBuilderInterface $router_builder
   *   The router builder service.
   */
  public function __construct(ModerationInformationInterface $moderation_info, EntityTypeManagerInterface $entity_type_manager, FormBuilderInterface $form_builder, EntityTypeBundleInfoInterface $bundle_info, RouteBuilderInterface $router_builder) {
    $this->moderationInfo = $moderation_info;
    $this->entityTypeManager = $entity_type_manager;
    $this->formBuilder = $form_builder;
    $this->bundleInfo = $bundle_info;
    $this->routerBuilder = $router_builder;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static($container
      ->get('content_moderation.moderation_information'), $container
      ->get('entity_type.manager'), $container
      ->get('form_builder'), $container
      ->get('entity_type.bundle.info'), $container
      ->get('router.builder'));
  }

  // …

  /**
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity that was just saved.
   *
   * @see hook_entity_update()
   */
  public function entityUpdate(EntityInterface $entity) {
    if ($this->moderationInfo->isModeratedEntity($entity)) {
      $this->updateOrCreateFromEntity($entity);
    }
    elseif ($entity instanceof Workflow && $entity->getTypePlugin()->getPluginId() == 'content_moderation') {
      $this->routerBuilder->setRebuildNeeded();
    }
  }

  // …

}

The used class can implement ContainerInjectionInterface (and ContainerAwareInterface); it can use dependency injection like service classes, but it's "lighter" than services classes, since it doesn't need an entry in the .services.yml file to work.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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