Generating permissions programmatically with hook_permission()for Drupal 8 , 9 , 10 , and 11

Last updated :  

The last lesson's .permissions.yml file works perfectly when you know the exact list of permissions ahead of time. But what happens when the number of permissions your module needs isn't fixed — one permission per content type someone might add later, one per report a plugin discovers at runtime? A static YAML list can't grow itself. hook_permission() can: it lets your module hand Drupal a permissions array built by real PHP code, computed fresh every time the system asks.

What you'll learn in this lesson

  • When to reach for hook_permission() instead of a static .permissions.yml file
  • The exact array shape Drupal expects back from the hook
  • How to build permission names dynamically from data your module discovers
  • Why this hook still gets cached, even though it's PHP, not YAML

The hook signature

/**
 * Implements hook_permission().
 */
function mymodule_permission() {
  $permissions = [];

  $permissions['administer mymodule settings'] = [
    'title' => t('Administer MyModule settings'),
    'description' => t('Access the MyModule configuration form.'),
  ];

  return $permissions;
}

How it works

The return shape matches .permissions.yml exactly

Notice the array structure: each key is a permission machine name, each value is an associative array with title and description. That's not a coincidence — Drupal internally merges permissions from both sources (YAML files and every module's hook_permission() implementation) into one combined list before building the permissions page. From the system's point of view, they're the same data; YAML is just the static special case.

Building permissions dynamically

function mymodule_permission() {
  $permissions = [];
  foreach (mymodule_get_report_types() as $type_id => $type_label) {
    $permissions["view $type_id reports"] = [
      'title' => t('View @label reports', ['@label' => $type_label]),
    ];
  }
  return $permissions;
}

This is the entire point of using the hook instead of YAML: if mymodule_get_report_types() returns a different list tomorrow — because someone configured a new report type through the UI — the permissions list grows to match, with zero code changes and zero manual YAML edits.

Still cached, just like YAML. Even though this is real PHP that could theoretically return something different on every request, Drupal calls hook_permission() once and caches the result along with every other module's permissions. If the underlying data changes (a new report type gets added), you still need to clear the cache before the new permission appears — the hook isn't re-run on every page load.

Quick check: could you mix both approaches in the same module — some permissions in .permissions.yml, others generated by hook_permission()? (Answer: yes, without any conflict. Drupal merges every source together before presenting the permissions page; a module can use YAML for its fixed permissions and the hook only for the ones that genuinely need to be computed.)

Key takeaways

  • Use .permissions.yml for a fixed, known set of permissions; use hook_permission() when the set depends on runtime data.
  • The hook must return an array keyed by machine name, with the same title/description shape as the YAML format.
  • YAML-declared and hook-declared permissions are merged into one list — a module can use both at once.
  • The result is still cached; adding a new dynamically-generated permission still requires a cache rebuild to show up.

Coming up next

Declaring a permission — whether in YAML or via this hook — only creates the checkbox. It doesn't enforce anything by itself. The next lesson, Restricting Route Access, covers the other half: actually wiring a permission into a route's _permission requirement so Drupal enforces it on every request.