Skip to content

Drupal Hooks — hook_entity_*, hook_form_alter and hook_theme Guide

DodaTech Updated 2026-06-27 10 min read

In this tutorial, you'll learn how Drupal hooks work, how to implement the most important hooks including hook_entity_* for CRUD operations, hook_form_alter for modifying forms, hook_theme for registering templates, and hook_help for module documentation.

What You'll Learn

  • What hooks are and how Drupal's hook system works
  • hook_help: providing help text for your module
  • hook_entity_type_build: defining custom entity types
  • hook_entity_insert/update/delete/predelete: reacting to entity operations
  • hook_form_alter: modifying any form with fields, validation, and submit handlers
  • hook_form_FORM_ID_alter: targeting specific forms
  • hook_theme: registering templates and defining template variables
  • hook_menu_links_discovered_alter: modifying menu links
  • hook_views_data: exposing custom data to Views
  • hook_views_data_alter: modifying Views data
  • hook_install and hook_uninstall: module installation and uninstallation tasks
  • hook_schema: defining database tables
  • hook_cron: scheduled tasks
  • Implementation conventions: prefixing functions with module name

Why It Matters

Hooks are the backbone of Drupal's extensibility. Every contributed module uses hooks to integrate with Drupal's core systems without modifying core files. When you understand hooks, you can modify any behavior in Drupal — forms, entities, views, menus, themes, and more — without writing complex code. The hook system is what makes Drupal a Content Management Framework rather than just a CMS.

Real-World Use

A publishing site needs to automatically create a notification email whenever a new article is published. Using hook_entity_insert, the module detects when a node of type "article" is saved in published state and triggers an email to subscribers. Later, the team wants to add a field to all content edit forms. Using hook_form_alter, they add the field without modifying any core code. When the site is upgraded, these hooks continue to work because they do not override any files — they just react to events.

Learning Path

flowchart LR
  A[Custom Module Development] --> B[Drupal Hooks]
  B --> C[hook_entity_*]
  C --> D[hook_form_alter]
  D --> E[hook_theme]
  E --> F[hook_views_data]
  F --> G[Drupal API]
  G --> H[Advanced Module Development]

What Are Hooks?

A hook is a PHP function with a specific name that Drupal calls at defined moments. When you implement a hook, you tell Drupal: "At this point, run my code too."

The naming convention is MODULE_NAME_hook_name(). For example, if your module is called my_module and you want to implement hook_help, you create:

<?php
function my_module_help($route_name, $context) {
  // Your help text here
}

Drupal discovers these functions by scanning the .module file. It calls every module's implementation of a hook whenever that hook is invoked.

How Hooks Work Internally

<?php
// This is what Drupal does internally when it needs
// to invoke a hook:
\Drupal::moduleHandler()->invokeAll('entity_insert', [$entity]);

// Every installed module with a function named
// MODULENAME_entity_insert() gets called.
// The $entity is passed as a parameter.

hook_help

Provide help text that appears on the module's help page:

<?php
/**
 * Implements hook_help().
 */
function my_module_help($route_name, $context) {
  switch ($route_name) {
    case 'help.page.my_module':
      $output = '';
      $output .= '<h3>' . t('About My Module') . '</h3>';
      $output .= '<p>' . t('My Module provides custom functionality for managing tasks and assignments.') . '</p>';
      $output .= '<h3>' . t('Configuration') . '</h3>';
      $output .= '<p>' . t('Configure at <a href=":url">the settings page</a>.', [
        ':url' => \Drupal\Core\Url::fromRoute('my_module.settings')->toString(),
      ]) . '</p>';
      return $output;

    case 'my_module.settings':
      return '<p>' . t('Configure your API key and endpoint settings here.') . '</p>';
  }
}

hook_entity_type_build

Define or alter entity types programmatically:

<?php
/**
 * Implements hook_entity_type_build().
 */
function my_module_entity_type_build(array &$entity_types) {
  // Add a custom entity type
  $entity_types['task'] = new \Drupal\Core\Entity\ContentEntityType([
    'id' => 'task',
    'label' => t('Task'),
    'label_collection' => t('Tasks'),
    'label_singular' => t('task'),
    'label_plural' => t('tasks'),
    'handlers' => [
      'storage' => 'Drupal\my_module\TaskStorage',
      'list_builder' => 'Drupal\my_module\TaskListBuilder',
      'form' => [
        'default' => 'Drupal\my_module\Form\TaskForm',
        'edit' => 'Drupal\my_module\Form\TaskForm',
        'delete' => 'Drupal\my_module\Form\TaskDeleteForm',
      ],
      'access' => 'Drupal\my_module\TaskAccessControlHandler',
    ],
    'base_table' => 'tasks',
    'entity_keys' => [
      'id' => 'id',
      'label' => 'title',
      'uuid' => 'uuid',
      'uid' => 'user_id',
    ],
  ]);
}

hook_entity_insert / update / delete / predelete

React to entity CRUD operations:

<?php
/**
 * Implements hook_entity_insert().
 */
function my_module_entity_insert(\Drupal\Core\Entity\EntityInterface $entity) {
  // Only act on article nodes
  if ($entity->getEntityTypeId() === 'node' && $entity->bundle() === 'article') {
    // Send notification when article is published
    if ($entity->isPublished()) {
      \Drupal::service('my_module.notifier')
        ->notifyNewArticle($entity);
    }
  }
}

/**
 * Implements hook_entity_update().
 */
function my_module_entity_update(\Drupal\Core\Entity\EntityInterface $entity) {
  if ($entity->getEntityTypeId() === 'node') {
    \Drupal::logger('my_module')
      ->info('Node @title was updated.', ['@title' => $entity->getTitle()]);
  }
}

/**
 * Implements hook_entity_predelete().
 */
function my_module_entity_predelete(\Drupal\Core\Entity\EntityInterface $entity) {
  // Archive content before deletion
  if ($entity->getEntityTypeId() === 'node') {
    \Drupal::service('my_module.archiver')
      ->archiveNode($entity);
  }
}

hook_form_alter

Modify any form in Drupal — this is one of the most powerful hooks:

<?php
/**
 * Implements hook_form_alter().
 */
function my_module_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
  // Add a help message to all forms
  $form['help_text'] = [
    '#markup' => '<p class="help-message">Fields marked with * are required.</p>',
    '#weight' => -100,
  ];
}

/**
 * Implements hook_form_FORM_ID_alter().
 * Targets only the node article edit form.
 */
function my_module_form_node_article_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
  // Add a custom field to the article form
  $form['editorial_notes'] = [
    '#type' => 'textarea',
    '#title' => t('Editorial Notes'),
    '#description' => t('Internal notes for the editorial team.'),
    '#weight' => 100,
  ];

  // Add a custom submit handler
  $form['#submit'][] = 'my_module_article_form_submit';

  // Change the submit button text
  $form['actions']['submit']['#value'] = t('Save and Review');
}

/**
 * Custom submit handler for article form.
 */
function my_module_article_form_submit(&$form, \Drupal\Core\Form\FormStateInterface $form_state) {
  $notes = $form_state->getValue('editorial_notes');
  if (!empty($notes)) {
    \Drupal::messenger()->addMessage(t('Editorial notes saved.'));
    // Store notes in state or a custom field
    \Drupal::state()->set('article_notes_' . $form_state->getFormObject()->getEntity()->id(), $notes);
  }
}

hook_theme

Register templates for your module:

<?php
/**
 * Implements hook_theme().
 */
function my_module_theme($existing, $type, $theme, $path) {
  return [
    'my_module_task_card' => [
      'variables' => [
        'title' => NULL,
        'status' => NULL,
        'priority' => NULL,
        'due_date' => NULL,
      ],
      'template' => 'task-card',
    ],
    'my_module_task_list' => [
      'variables' => [
        'tasks' => [],
        'empty_message' => 'No tasks found.',
      ],
      'template' => 'task-list',
    ],
  ];
}

Then create the template file at templates/task-card.html.twig:

<div class="task-card priority-{{ priority|default('normal') }}">
  <h3 class="task-card__title">{{ title }}</h3>
  <div class="task-card__meta">
    <span class="task-card__status">{{ status }}</span>
    <span class="task-card__due-date">{{ due_date }}</span>
  </div>
</div>

And use it in a controller:

<?php
public function taskDashboard() {
  return [
    '#theme' => 'my_module_task_card',
    '#title' => 'Finish report',
    '#status' => 'In Progress',
    '#priority' => 'high',
    '#due_date' => '2026-07-15',
  ];
}

Modify existing menu links:

<?php
/**
 * Implements hook_menu_links_discovered_alter().
 */
function my_module_menu_links_discovered_alter(&$links) {
  // Change the weight of the "Add content" link
  if (isset($links['node.add_page'])) {
    $links['node.add_page']['weight'] = 5;
  }

  // Remove a specific menu link
  unset($links['contact.site_page']);
}

hook_views_data

Expose custom data to Views:

<?php
/**
 * Implements hook_views_data().
 */
function my_module_views_data() {
  $data = [];

  $data['tasks']['table']['group'] = t('Task Manager');
  $data['tasks']['table']['base'] = [
    'field' => 'id',
    'title' => t('Tasks'),
    'help' => t('Custom task data from My Module.'),
  ];

  $data['tasks']['id'] = [
    'title' => t('Task ID'),
    'help' => t('The unique task identifier.'),
    'field' => [
      'id' => 'numeric',
    ],
    'filter' => [
      'id' => 'numeric',
    ],
  ];

  $data['tasks']['title'] = [
    'title' => t('Task Title'),
    'help' => t('The task title.'),
    'field' => [
      'id' => 'standard',
    ],
    'sort' => [
      'id' => 'standard',
    ],
    'filter' => [
      'id' => 'string',
    ],
  ];

  return $data;
}

hook_schema

Define database tables when your module is installed:

<?php
/**
 * Implements hook_schema().
 */
function my_module_schema() {
  $schema['tasks'] = [
    'description' => 'Stores custom task data.',
    'fields' => [
      'id' => [
        'type' => 'serial',
        'unsigned' => true,
        'not null' => true,
        'description' => 'Primary key.',
      ],
      'title' => [
        'type' => 'varchar',
        'length' => 255,
        'not null' => true,
        'description' => 'The task title.',
      ],
      'status' => [
        'type' => 'varchar',
        'length' => 32,
        'not null' => true,
        'default' => 'pending',
        'description' => 'Task status.',
      ],
      'user_id' => [
        'type' => 'int',
        'unsigned' => true,
        'not null' => true,
        'description' => 'The user who created the task.',
      ],
      'created' => [
        'type' => 'int',
        'unsigned' => true,
        'not null' => true,
        'description' => 'Timestamp of creation.',
      ],
    ],
    'primary key' => ['id'],
    'indexes' => [
      'status' => ['status'],
      'user_id' => ['user_id'],
    ],
  ];

  return $schema;
}

hook_install and hook_uninstall

Run code when your module is installed or uninstalled:

<?php
/**
 * Implements hook_install().
 */
function my_module_install() {
  // Set default configuration
  \Drupal::configFactory()
    ->getEditable('my_module.settings')
    ->set('sync_interval', 60)
    ->set('api_endpoint', 'https://api.example.com')
    ->save();
}

/**
 * Implements hook_uninstall().
 */
function my_module_uninstall() {
  // Clean up configuration
  \Drupal::configFactory()
    ->getEditable('my_module.settings')
    ->delete();

  // Remove state values
  \Drupal::state()->delete('my_module.last_sync');
}

hook_cron

Run scheduled tasks:

<?php
/**
 * Implements hook_cron().
 */
function my_module_cron() {
  $last_sync = \Drupal::state()->get('my_module.last_sync', 0);
  $interval = \Drupal::config('my_module.settings')
    ->get('sync_interval');

  // Only sync if enough time has passed
  if (\Drupal::time()->getRequestTime() - $last_sync >= $interval) {
    try {
      // Perform the sync operation
      \Drupal::service('my_module.api_client')->syncData();
      \Drupal::state()->set('my_module.last_sync', \Drupal::time()->getRequestTime());
      \Drupal::logger('my_module')->info('Data sync completed successfully.');
    } catch (\Exception $e) {
      \Drupal::logger('my_module')->error('Sync failed: @message', [
        '@message' => $e->getMessage(),
      ]);
    }
  }
}

Common Mistakes

  1. Using wrong function name: The function must follow MODULENAME_hookname() exactly. If your module is my_custom_module, the function for form_alter must be my_custom_module_form_alter(). Drupal scans for exact matches.

  2. Putting hooks in the wrong file: Most hooks go in the .module file. However, some hooks like hook_theme can go in a .theme file for themes. Check the hook documentation for the correct file location.

  3. Not checking entity type or bundle in entity hooks: hook_entity_insert fires for ALL entity types. Always check $entity->getEntityTypeId() and $entity->bundle() before acting, or you will affect all content types.

  4. Modifying form state incorrectly in hook_form_alter: Form state access differs between form alters and submit handlers. In hook_form_alter, use $form_state->getValue(). In submit handlers, values are already processed.

  5. Forgetting to return values from hook_theme: hook_theme must return an array. If you return nothing, no templates are registered, and your theme function fails silently.

Practice Questions

  1. What is the naming convention for a hook function in a module called custom_tools?
  2. How do you target only the user registration form with hook_form_alter?
  3. What is the difference between hook_entity_insert and hook_entity_predelete? When would you use each?
  4. Challenge: Create a module that uses at least six different hooks: hook_help, hook_form_alter to add a field to the article form, hook_entity_insert to log new articles, hook_theme to register a custom template, hook_cron to clean up old log entries, and hook_schema to create a log table.

FAQ

What is the difference between hooks and events in Drupal?

Hooks are the traditional Drupal extensibility system, where Drupal calls specially named functions in .module files. Events are the Symfony-based system using the event dispatcher. Both achieve similar goals, but events use event classes and subscribers. Modern Drupal modules increasingly use events, but hooks remain widely used and supported.

Can I implement a hook in a theme?

Yes. Themes can implement hooks using a .theme file. However, most hooks are intended for modules. Theme-specific hooks like hook_theme_registry_alter are common in themes. Module developers should implement hooks in .module files.

How do I find all available hooks in Drupal?

The Drupal.org API documentation lists all hooks by category. You can also search for @ingroup hooks in the Drupal core codebase. The Module Handler service (\Drupal::moduleHandler()) manages hook discovery and invocation.

What happens if two modules implement the same hook?

Both modules' implementations run. The order depends on the module weight. You cannot control which runs first unless you use hook_module_implements_alter() to change the invocation order. Each implementation receives the same parameters.

How do I pass parameters to a hook?

You cannot pass custom parameters to a core hook. The parameters are determined by the hook definition. For custom data sharing, use the State API, configuration, or a custom service with dependency injection.

Mini Project

Goal: Build a module that uses hooks to implement an editorial notification system.

  1. Create a module called editorial_notify
  2. Implement hook_help to provide documentation
  3. Implement hook_schema to create a table notifications_log with columns: id, nid, action (insert/update/delete), timestamp
  4. Implement hook_entity_insert to log when any node is created
  5. Implement hook_entity_update to log when a node's publishing status changes
  6. Implement hook_form_node_article_form_alter to add a "Notify subscribers" checkbox
  7. Implement hook_theme to register a template for the notification log display
  8. Implement hook_cron to clean up log entries older than 30 days
  9. Create a controller that displays the notification log using your registered template

What's Next

Now that you understand hooks, proceed to Drupal API to learn about render arrays, entity queries, and the service container. Then explore Drush commands to manage Drupal from the command line.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro