Skip to content

Preprocess Functions

When to Use

When you need to modify or add variables before they reach a Twig template -- add render arrays, compute values, restructure data, add cache metadata.

Pattern: Basic Preprocess Function

Naming convention: {module/theme}_preprocess_{hook}(&$variables)

/**
 * Implements hook_preprocess_HOOK() for node templates.
 */
function mymodule_preprocess_node(&$variables) {
  /** @var \Drupal\node\NodeInterface $node */
  $node = $variables['node'];

  // Add a new variable as a render array
  $variables['author_info'] = [
    '#theme' => 'username',
    '#account' => $node->getOwner(),
  ];

  // Add classes based on node data
  $variables['attributes']['class'][] = 'node--type-' . $node->bundle();

  // Add cache metadata
  $variables['#cache']['tags'][] = 'user:' . $node->getOwnerId();
}

In node.html.twig:

<article{{ attributes }}>
  {{ author_info }}
  {{ content }}
</article>

Pattern: Adding Render Arrays in Preprocess

CORRECT -- Pass render arrays:

function mymodule_preprocess_page(&$variables) {
  // Add a render array
  $variables['copyright'] = [
    '#markup' => '<p>&copy; ' . date('Y') . ' My Site</p>',
    '#cache' => ['contexts' => ['timezone']],
  ];
}

WRONG -- Rendering in preprocess:

// DON'T DO THIS
function mymodule_preprocess_page(&$variables) {
  $build = ['#markup' => '<p>Copyright</p>'];

  // WRONG: Loses cache metadata, prevents alterations
  $variables['copyright'] = \Drupal::service('renderer')->render($build);
}

Why: Twig renders everything automatically. Rendering in preprocess:

  • Loses bubbleable metadata (cache tags, contexts, attachments)
  • Prevents other modules from altering the render array
  • Converts structured data to string too early

Reference: Twig best practices - preprocess functions and templates

Pattern: Modifying Existing Render Arrays

function mymodule_preprocess_node(&$variables) {
  // Hide a field
  $variables['content']['field_internal_notes']['#access'] = FALSE;

  // Change field label
  $variables['content']['field_name']['#title'] = t('Full Name');

  // Add prefix/suffix
  $variables['content']['field_price']['#prefix'] = '<div class="price-wrapper">';
  $variables['content']['field_price']['#suffix'] = '</div>';

  // Change field formatter (if not yet rendered)
  if (isset($variables['content']['field_image'][0])) {
    $variables['content']['field_image'][0]['#image_style'] = 'thumbnail';
  }
}

Pattern: Pre-Render Callbacks

Pre-render callbacks are an alternative to preprocess -- they run later in the rendering pipeline, just before the element is rendered.

// In a render array
$build['section'] = [
  '#type' => 'container',
  '#pre_render' => [
    [MyClass::class, 'preRenderSection'],
  ],
];

// The callback
public static function preRenderSection($element) {
  // Modify element
  $element['#attributes']['class'][] = 'enhanced';

  // Can add children
  $element['timestamp'] = [
    '#markup' => '<time>' . date('c') . '</time>',
  ];

  return $element;
}

Pre-render vs Preprocess:

Feature Pre-render callback Preprocess function
When it runs Just before rendering this element Before template rendering
Scope Single element All variables for template
Alterability Less discoverable (attached to element) More discoverable (hook system)
Use case Element-specific logic Template variable preparation

Reference: core/lib/Drupal/Core/Render/Element/RenderElementBase.php (lines 96-98) -- #pre_render documentation

Decision: Preprocess vs Pre-Render vs Alter Hook

Need to... Use... Why
Add variables to a template Preprocess function Standard pattern for template variable preparation
Modify a specific render element before rendering #pre_render callback Scoped to that element, no global side effects
Allow other modules to modify your data Alter hook (invoke moduleHandler->alter()) Explicitly extensible API
Modify render arrays from other modules hook_preprocess_HOOK() or render array alter Integrate with existing render arrays

Common Mistakes

  • Rendering in preprocess instead of passing render arrays -- Loses metadata, prevents alterations, hurts cacheability
  • Adding raw HTML strings instead of render arrays -- Bypasses XSS protection, theming, and caching
  • Not adding cache metadata when adding dynamic content -- Stale cache persists inappropriately
  • Modifying $variables['elements'] instead of $variables['content'] -- Wrong variable; elements is input, content is processed render array
  • Assuming preprocess runs only once -- Can run multiple times; avoid expensive operations without caching

See Also