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.ymlfile - 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.
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 byhook_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.ymlfor a fixed, known set of permissions; usehook_permission()when the set depends on runtime data. - The hook must return an array keyed by machine name, with the same
title/descriptionshape 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.