module
Option 1 (recommended): custom entity reference selection handler. Further options down below.
ActiveUserSelection.php
<?php
namespace Drupal\my_module\Plugin\EntityReferenceSelection;
use Drupal\Core\Entity\Plugin\EntityReferenceSelection\UserSelction;
use Drupal\Core\StringTranslation\TranslatableMarkup;
#[EntityReferenceSelection(
id: "default:active_user",
label: new TranslatableMarkup("User selection (only active users)"),
entity_types: ["user"],
group: "default",
weight: 1
)]
class ActiveUserSelection extends UserSelection {
/**
* {@inheritdoc}
*/
protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') {
$query = parent::buildEntityQuery($match, $match_operator);
$query->condition('status', 1);
return $query;
}
}After implementing that plugin, you need to activate it on the entity form display for the field you want it.
08.10.2026 | Michael Ebert
Filter user entity reference field options to active users only
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.
16.07.2026 | Lothar Ferreira Neumann
How to translate the Maxlength Countdown message label globally
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;
}
}
}
13.11.2025 | Michael Ebert
Manage access to private files in paragraphs or views on restricted nodes
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();
}
}
19.09.2024 | Michael Ebert
Migrate taxonomy terms to an other vocabulary
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';
}
}
}
13.11.2025 | Michael Ebert
How to alter the langcode of a field for accessibility reasons
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();
}
}
30.04.2026 | Pascal Crott
Prevent empty views blocks from getting rendered even if they are empty.
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