module

A Drupal backend screenshot showing the "Count down message" field

Leave this field empty to allow for translation via Drupal interface translation.


Example configuration created by enabling Maxlength:

core.entity_form_display.node.article_not_translatable.default.yml
# ...
content:
  field_copyright:
    type: text_textarea
    weight: 3
    region: content
    settings:
      rows: 2
      placeholder: ''
    third_party_settings:
      allowed_formats:
        hide_help: '1'
        hide_guidelines: '1'
      maxlength:
        maxlength_js: null
        maxlength_js_label: 'Content limited to @limit characters, remaining: <strong>@remaining</strong>'
        maxlength_js_enforce: false
# ...

However, the module already contains fallback logic for this label in maxlength.module: 

maxlength.module
$maxlength_js_label = !empty($thirdPartySettings['maxlength']['maxlength_js_label']) ? $thirdPartySettings['maxlength']['maxlength_js_label'] : t('Content limited to @limit characters, remaining: <strong>@remaining</strong>');
$maxlength_js = $thirdPartySettings['maxlength']['maxlength_js'];

Working configuration: empty the label

core.entity_form_display.node.article_translatable.default.yml
# ...
content:
  field_copyright:
    type: text_textarea
    weight: 3
    region: content
    settings:
      rows: 2
      placeholder: ''
    third_party_settings:
      allowed_formats:
        hide_help: '1'
        hide_guidelines: '1'
      maxlength:
        maxlength_js: null
        maxlength_js_label: ''
        maxlength_js_enforce: false
# ...

With the label empty, the module falls back to its internal t() string, which can then be translated normally via the interface translation system.

my_module.module
use Drupal\views\ViewExecutable;
use Drupal\my_module\Access\NodePageAccessControlHandler;
use Drupal\my_module\Access\RestrictedMediaAccessControlHandler;


/**
 * Implements hook_entity_type_alter().
 */
function my_module_entity_type_alter(array &$entity_types) {
  if (isset($entity_types['node'])) {
    $entity_types['node']->setAccessClass(NodePageAccessControlHandler::class);
  }
  if (isset($entity_types['media'])) {
    $entity_types['media']->setAccessClass(RestrictedMediaAccessControlHandler::class);
  }
}

/**
 * Implements hook_views_post_execute().
 */
function my_module_views_post_execute(ViewExecutable $view) {
  // Only execute if we are on the right view display.
  if ($view->id() === 'MY_VIEW_ID' && $view->current_display === 'MY_VIEW_DISPLAY' ) {

    // Mark this view results as token-allowed in the session for a short time.
    $ttl = 3600; // 1 hour; adjust to taste.
    $now = \Drupal::time()->getRequestTime();
    $session = \Drupal::service('request_stack')->getCurrentRequest()->getSession();
    $grants = (array) $session->get('my_module.token_mids', []);
    foreach ($view->result as $item) {
      $grants[(int) $item->mid] = $now + $ttl;
    }
    $session->set('my_module.token_mids', $grants);
  }
}

NodePageAccessControlHandler.php
namespace Drupal\my_module\Access;

/**
 * Override Node Access Control Handler.
 */
class NodePageAccessControlHandler extends NodeAccessControlHandler {

  ...

  /**
   * {@inheritdoc}
   */
  public function access(EntityInterface $entity, $operation, ?AccountInterface $account = NULL, $return_as_object = FALSE) {
    if ($operation === 'view') {
      // Mark this node as token-allowed in the session for a short time.
      $ttl = 3600; // 1 hour; adjust to taste.
      $now = $this->time->getRequestTime();
      $session = $this->requestStack->getSession();
      $grants = (array) $session->get('my_module.token_nids', []);
      $grants[(int) $entity->id()] = $now + $ttl;
      $session->set('my_module.token_nids', $grants);
    }
}

RestrictedMediaAccessControlHandler.php
namespace Drupal\my_module\Access;

/**
 * Override Media Access Control Handler.
 */
class RestrictedMediaAccessControlHandler extends MediaAccessControlHandler {

  /**
   * {@inheritdoc
   *
   *  Restrict anonymous view of Media (bundle "restricted_file") to cases where
   *  the user can view the host Node (via paragraph field
   * 'field_download_restricted' or a view listing media entities), 
   * OR they have a valid short-lived token grant for that Node stored in session.
   */
  public function access(EntityInterface $entity, $operation, ?AccountInterface $account = NULL, $return_as_object = FALSE) {
    if (
      $operation !== 'view'
      || !$account
      || $entity->bundle() !== 'restricted_file'
    ) {
      return parent::access($entity, $operation, $account, $return_as_object);
    }

    $paragraphs = $this->entityTypeManager
      ->getStorage('paragraph')
      ->loadByProperties(['field_download_restricted' => $entity->id()]);

    // Check if we have a media entity id and a cookie for it does exist.
    $media_entity = NULL;
    if (array_key_exists($entity->id(), $mgrants = (array) $this->requestStack->getSession()->get('my_module.token_mids', []))) {
      $media_entity = $this->entityTypeManager
        ->getStorage('media')
        ->loadByProperties(['mid' => $entity->id()]);
      $now = $this->time->getRequestTime();

      // Session-granted access via token.
      $mid = (int) $entity->id();
      if (!empty($mgrants[$mid]) && $mgrants[$mid] >= $now) {
        return AccessResult::allowed()
          ->addCacheableDependency($entity)
          ->addCacheableDependency(reset($media_entity))
          // Important for anon/session-based grants.
          ->mergeCacheMaxAge(0);
      }
    }  

    $result = AccessResult::forbidden()
      ->addCacheableDependency($entity)
      ->cachePerPermissions();

    if (!$paragraphs && !$media_entity) {
      return $result;
    }

    // Read session grants set by my_module_node_access().
    $now = $this->time->getRequestTime();
    $grants = (array) $this->requestStack->getSession()->get('my_module.token_nids', []);

    foreach ($paragraphs as $paragraph) {
      if ($paragraph instanceof ParagraphInterface) {
        $result = $result->addCacheableDependency($paragraph);
      }

      $node = $this->getAncestorEntity($paragraph);
      if (!$node instanceof NodeInterface) {
        continue;
      }

      // Always add the node as a dependency.
      $result = $result->addCacheableDependency($node);

      // Session-granted access via token.
      $nid = (int) $node->id();
      if (!empty($grants[$nid]) && $grants[$nid] >= $now) {
        return AccessResult::allowed()
          ->addCacheableDependency($entity)
          ->addCacheableDependency($node)
          // Important for anon/session-based grants.
          ->mergeCacheMaxAge(0);
      }

      // Existing rule: inherit from node view access.
      if ($node->access('view', $account)) {
        return AccessResult::allowed()
          ->addCacheableDependency($entity)
          ->addCacheableDependency($node)
          ->cachePerPermissions();
      }

      return $result;
    }
  }

}

my_module_with_presave_hook.module
<?php

/**
 * @file
 * Primary module hooks for my module.
 */

/**
 * Implements hook_ENTITY_TYPE_presave().
 */
function my_module_node_presave(\Drupal\Core\Entity\EntityInterface $entity) {
  if ($entity->bundle() === 'article') {
    // Do something here with the article.
  }
}

/**
 * Implements hook_entity_presave().
 */
function my_module_entity_presave(\Drupal\Core\Entity\EntityInterface $entity) {
  if ($entity instanceof \Drupal\node\NodeInterface || $entity instanceof \Drupal\paragraphs\Entity\Paragraph) {
    // Do something here with the node and paragraph.
  }
}

10.07.2025 | Lothar Ferreira Neumann

Presave hook on one or multiple entities

my_module.install
function my_module_update_XXXXX() {
  $entity_type_manager = \Drupal::entityTypeManager();

  $old_terms = $entity_type_manager->getStorage('taxonomy_term')->loadByProperties(['vid' => 'old_tags']);
  foreach ($old_terms as $term) {
    $term->vid->target_id = 'new_tags';
    $term->save();
  }

  $nodes = $entity_type_manager->getStorage('node')->loadByProperties(['type' => 'my_bundle']);
  foreach ($nodes as $node) {
    $node_old_terms = $node->field_old_terms->getValue();
    foreach ($node_old_terms as $old_term) {
      $node->field_new_tags[] = $old_term;
    }
    $node->save();
  }

}

my_module.module
function my_module_preprocess_field(&$variables): void {
  if ($variables['element']['#bundle'] == 'my_bundle') {
    if (in_array($variables['element']['#field_name'], ['my_field'])) {
      $variables['attributes']['lang'] = 'en';
    }
  }
}

Issue: https://www.drupal.org/project/drupal/issues/953034

my_module.module
<?php

use Drupal\views\Plugin\Block\ViewsBlock;

/**
 * Implements hook_block_alter().
 */
function hook_block_alter(array &$definitions) {
  foreach ($definitions as $block_id => &$block_definition) {
    if ($block_definition['class'] === ViewsBlock::class) {
      $block_definition['class'] = 'Drupal\my_module\Plugin\Block\ViewsBlock';
    }
  }
}

ViewsBlock.php
<?php

namespace Drupal\my_module\Plugin\Block;

use Drupal\views\Plugin\Block\ViewsBlock as ViewsBlockCore;

/**
 * Replaces the generic Views block.
 */
class ViewsBlock extends ViewsBlockCore {

  /**
   * {@inheritdoc}
   */
  public function build() {
    if (empty($this->view->result) && empty($this->view->empty)) {
      // Without caching.
      // return ['#cache' => ['max-age' => 0]];
      // With hard caching.
      return [];
    }

    return parent::build();
  }

}

my_module.module
<?php

/**
 * Implements hook_theme_suggestions_HOOK_alter() for table.
 */
function hook_theme_suggestions_table_alter(array &$suggestions, array $variables): void {
  if (empty($variables['attributes']['class'])) {
    return;
  }

  if (is_array($variables['attributes']['class']) && in_array('ief-entity-table', $variables['attributes']['class'])) {
    $suggestions[] = 'table__simple';
  }
}
22.06.2026 | Pascal Crott

Gin: Make use of the simple table

⚠️ WARNING: Potential data loss. Use with caution.

scp_base.module
/**
 * Run missing database schema updates.
 */
function mymodule_update_10001(&$sandbox) {
  $entity_type_manager = \Drupal::entityTypeManager();
  $entity_type_manager->clearCachedDefinitions();
  $change_summary = \Drupal::service('entity.definition_update_manager')->getChangeSummary();
  foreach ($change_summary as $entity_type_id => $change_list) {
    $entity_type = $entity_type_manager->getDefinition($entity_type_id);
    \Drupal::entityDefinitionUpdateManager()->installEntityType($entity_type);
  }
}
07.05.2026 | Peter Gerken

Run missing entity Schema updates

my_module.module
<?php

use Drupal\Component\Utility\Html;
use Drupal\Core\Form\FormStateInterface;


/**
 * Implements hook_form_FORM_ID_alter() for 'views_exposed_form'.
 */
function hook_form_views_exposed_form_alter(&$form, FormStateInterface $form_state) {
  // Configure the values to your needs.
  $view_name = 'my_view';
  $display_name = 'my_display';
  $entity_type_id = 'taxonomy_term';
  $reference_field = 'field_category';
  // The field on the referenced entity.
  $flag_on_reference_field = 'field_hide_on_exposed_form';

  if ($form['#id'] == Html::getId("views-exposed-form-{$view_name}-{$display_name}")) {
    $entity_storage = \Drupal::entityTypeManager()->getStorage($entity_type_id);
    $options = &$form[$reference_field]['#options'];
    foreach ($entity_storage->loadMultiple(array_keys($options)) as $id => $entity) {
      if ($entity->hasField($flag_on_reference_field) && !$entity->{$flag_on_reference_field}->isEmpty()) {
        unset($options[$id]);
      }
    }
  }
}

my_group.module
/**
 * Implements hook_tokens().
 */
function example_group_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
  $replacements = [];

  // Automatically expose related entities to group_relationships.
  if ($type == 'group_relationship' && !empty($data[$type])) {
    $token_service = \Drupal::token();

    /** @var \Drupal\example\GroupRelationshipTypeServiceInterface $group_relationship_type_service */
    $group_relationship_type_service = \Drupal::service('example.group_relationship_type_service');

    $group_relationship = $data['group_relationship'];
    assert($group_relationship instanceof GroupRelationshipInterface);

    foreach ($tokens as $name => $original) {
      /** @var \Drupal\Core\Entity\EntityTypeInterface $entity_type */
      foreach ($group_relationship_type_service->getConfiguredEntityTypes() as $entity_type_id => $entity_type) {
        if ($name == $entity_type_id) {
          $entity = $group_relationship->getEntity();
          $bubbleable_metadata->addCacheableDependency($entity);
          $replacements[$original] = $entity->label();
        }

        // Actual chaining of tokens handled below.
        if ($entity_tokens = $token_service->findWithPrefix($tokens, $entity_type_id)) {
          $replacements += $token_service->generate($entity_type_id, $entity_tokens, [$entity_type_id => $group_relationship->getEntity()], $options, $bubbleable_metadata);
        }
      }
    }
  }

  return $replacements;
}