| name | debug |
| description | Diagnose and fix common Drupal code-level issues (hooks, services, cache, entities, routes, permissions) |
| version | 1.0.0 |
You are a Drupal 11 code-level debugging assistant. You help diagnose why custom code isn't working — hooks not firing, services not found, cache not invalidating, entities not loading, routes returning 403/404, etc.
This skill complements the ddev skill. Use ddev for environment issues (containers, ports, PHP errors). Use this skill for Drupal code logic issues.
Determine the Problem Category
Ask the user or detect from context:
| Category | Symptoms |
|---|
| Hook not firing | alter/presave/insert hook has no effect |
| Service not found | ServiceNotFoundException, class not found |
| Cache not invalidating | Stale content after save, changes not visible |
| Entity query issues | Empty results, access denied, wrong data |
| Route/controller 404 | Page not found for custom route |
| Permission/access 403 | Access denied unexpectedly |
| Plugin not discovered | Block/widget/formatter not appearing |
| Config issues | Missing config, schema mismatch, import fails |
| AJAX/form issues | Form rebuild fails, AJAX callback errors |
| Event subscriber not firing | Subscriber registered but never called |
1. Hook Not Firing
OOP Hooks (src/Hook/)
Checklist:
ddev drush pm:list --type=module --status=enabled | grep MODULE_NAME
ddev exec grep -r "Hook\\" web/modules/custom/MODULE_NAME/MODULE_NAME.services.yml
ddev drush cr
ddev exec drush php:eval "var_dump(class_exists('Drupal\\MODULE_NAME\\Hook\\CLASSNAME'));"
Common causes:
src/Hook/ listed in exclude: in services.yml — remove it from excludes
- Missing
autowire: true on the service definition
- Wrong namespace — must match
Drupal\{module}\Hook\{ClassName}
- Module not enabled
- Cache not cleared after adding hook class
Procedural Hooks (.module)
ddev exec grep -n "function MODULE_NAME_hook_name" web/modules/custom/MODULE_NAME/MODULE_NAME.module
ddev exec drush php:eval "print_r(\Drupal::moduleHandler()->getModuleList());"
form_alter Hooks
ddev exec drush php:eval "
// Enable Devel if available for form ID inspection
var_dump(\Drupal::hasService('devel.dumper'));
"
2. Service Not Found
ddev exec drush php:eval "var_dump(\Drupal::hasService('MODULE_NAME.service_name'));"
ddev exec drush php:eval "
\$container = \Drupal::getContainer();
\$ids = \$container->getServiceIds();
foreach (\$ids as \$id) {
if (str_contains(\$id, 'MODULE_NAME')) { echo \$id . PHP_EOL; }
}
"
ddev exec php -r "var_dump(yaml_parse_file('/var/www/html/web/modules/custom/MODULE_NAME/MODULE_NAME.services.yml'));"
ddev composer dump-autoload
ddev drush cr
Common causes:
- Typo in service ID (services.yml vs code reference)
- services.yml syntax error (indentation, missing colon)
- Class doesn't exist at the namespace path
- Module not enabled
- Missing
use statement for the interface
- Argument references non-existent service (e.g.
@logger.channel.MODULE_NAME without defining the channel)
3. Cache Not Invalidating
Render Cache
ddev exec drush php:eval "
\$tags = ['node:123', 'node_list'];
\Drupal::service('cache_tags.invalidator')->invalidateTags(\$tags);
echo 'Tags invalidated: ' . implode(', ', \$tags);
"
ddev drush cache:clear render
ddev drush cache:clear dynamic_page_cache
ddev drush cache:clear page
Checklist for render arrays:
$build['#cache'] = [
'tags' => ['node:' . $node->id(), 'node_list'],
'contexts' => ['user.permissions', 'url.path'],
'max-age' => Cache::PERMANENT,
];
Common causes:
- Missing
#cache tags — Drupal doesn't know when to invalidate
- Missing
#cache contexts — same cached version shown to all users
- Using
max-age: 0 everywhere (kills performance)
- Not invalidating tags after programmatic entity updates
- BigPipe/Dynamic Page Cache serving stale responses
Config Cache
ddev drush cr
ddev drush config:status
ddev drush config:get MODULE.settings
4. Entity Query Issues
ddev exec drush php:eval "
\$query = \Drupal::entityTypeManager()
->getStorage('node')
->getQuery()
->accessCheck(TRUE)
->condition('type', 'article')
->condition('status', 1)
->range(0, 5);
\$ids = \$query->execute();
var_dump(\$ids);
"
ddev exec drush php:eval "
// With access check (respects permissions)
\$with = \Drupal::entityTypeManager()->getStorage('node')->getQuery()
->accessCheck(TRUE)->condition('type', 'article')->count()->execute();
// Without access check (all entities, regardless of perms)
\$without = \Drupal::entityTypeManager()->getStorage('node')->getQuery()
->accessCheck(FALSE)->condition('type', 'article')->count()->execute();
echo \"With access: \$with, Without access: \$without\";
"
ddev exec drush php:eval "
\$storage = \Drupal::entityTypeManager()->getStorage('node');
\$count = \$storage->getQuery()->accessCheck(FALSE)->count()->execute();
echo \"Total nodes: \$count\";
"
Common causes:
- Missing
->accessCheck(TRUE) — required in Drupal 11, throws deprecation/error
- Running as anonymous user without permission
- Wrong condition field name (use
field_name not field_name.value for simple fields)
- Entity type doesn't exist or isn't installed
- Status condition missing (unpublished content filtered out)
5. Route/Controller 404
ddev exec drush eval "var_dump(\Drupal::service('router.route_provider')->getRouteByName('MODULE.route_name'));"
ddev exec drush eval "
\$routes = \Drupal::service('router.route_provider')->getAllRoutes();
foreach (\$routes as \$name => \$route) {
if (str_contains(\$name, 'MODULE_NAME')) {
echo \$name . ' => ' . \$route->getPath() . PHP_EOL;
}
}
"
ddev drush cr
ddev exec php -r "var_dump(yaml_parse_file('/var/www/html/web/modules/custom/MODULE_NAME/MODULE_NAME.routing.yml'));"
Common causes:
- Routing YAML indentation error
- Controller class not found (wrong namespace)
- Missing
_controller or _form in defaults
- Route path conflicts with another module
- Module not enabled
6. Permission/Access 403
ddev exec drush php:eval "
\$account = \Drupal::currentUser();
echo 'User: ' . \$account->getAccountName() . PHP_EOL;
echo 'Roles: ' . implode(', ', \$account->getRoles()) . PHP_EOL;
echo 'Has permission: ' . var_export(\$account->hasPermission('PERMISSION_NAME'), true);
"
ddev exec drush eval "
\$route = \Drupal::service('router.route_provider')->getRouteByName('MODULE.route_name');
print_r(\$route->getRequirements());
"
ddev exec grep -r "PERMISSION_NAME" web/modules/custom/MODULE_NAME/*.permissions.yml
Common causes:
- Permission string doesn't match between routing.yml and permissions.yml
- User role doesn't have the permission
- Custom access checker returns FALSE
_access: 'TRUE' missing quotes in routing.yml (must be string 'TRUE')
7. Plugin Not Discovered
ddev drush cr
ddev exec drush php:eval "
// For blocks:
\$definitions = \Drupal::service('plugin.manager.block')->getDefinitions();
foreach (\$definitions as \$id => \$def) {
if (str_contains(\$id, 'MODULE_NAME') || str_contains(\$id, 'my_block')) {
echo \$id . ' => ' . \$def['class'] . PHP_EOL;
}
}
"
ddev exec drush php:eval "var_dump(class_exists('Drupal\\MODULE\\Plugin\\Block\\MyBlock'));"
ddev exec php -l web/modules/custom/MODULE_NAME/src/Plugin/Block/MyBlock.php
Common causes:
- Wrong directory structure (must be
src/Plugin/Block/ for blocks)
- Missing or malformed
#[Block(...)] attribute
- PHP syntax error in the class
- Namespace doesn't match file path
- Module not enabled
#[Block] attribute missing id: parameter
8. Config Issues
ddev drush config:status
ddev drush config:get MODULE_NAME.settings
ddev exec drush php:eval "
\$typed_config = \Drupal::service('config.typed');
\$config = \Drupal::config('MODULE_NAME.settings');
\$definition = \$typed_config->getDefinition('MODULE_NAME.settings');
var_dump(\$definition);
"
Common causes:
- Config file in
config/install/ but module was already installed (reinstall module)
- Schema doesn't match config structure (extra/missing keys)
- Config overridden in settings.php (
$config['MODULE.settings']['key'] = value)
9. AJAX/Form Issues
ddev exec drush watchdog:show --type=php --count=10
Common causes:
- AJAX callback returns wrong type (must be render array or
AjaxResponse)
#ajax wrapper ID doesn't match element in form
- Missing
'#prefix' => '<div id="wrapper-id">' on the target element
- Form rebuild changes structure, breaking AJAX wrapper reference
10. Event Subscriber Not Firing
ddev exec drush php:eval "
\$dispatcher = \Drupal::service('event_dispatcher');
\$listeners = \$dispatcher->getListeners('kernel.request');
foreach (\$listeners as \$listener) {
if (is_array(\$listener)) {
echo get_class(\$listener[0]) . '::' . \$listener[1] . PHP_EOL;
}
}
"
ddev exec grep -A 3 "SubscriberClass" web/modules/custom/MODULE_NAME/MODULE_NAME.services.yml
Common causes:
- Missing
tags: [{ name: event_subscriber }] in services.yml
getSubscribedEvents() returns wrong event name
- Priority too low — another subscriber stops propagation
- Autowiring does not auto-tag event subscribers
Diagnostic Quick Reference
ddev drush pm:list | grep MODULE_NAME
ddev exec drush php:eval "var_dump(class_exists('Drupal\\MODULE\\ClassName'));"
ddev exec drush php:eval "var_dump(\Drupal::hasService('module.service'));"
ddev exec drush config:list | grep MODULE_NAME
ddev exec drush watchdog:show --severity=Error --count=20
ddev exec php -l web/modules/custom/MODULE_NAME/src/ClassName.php
ddev drush cr && ddev composer dump-autoload
Related Skills
- ddev — Environment-level debugging (containers, logs, Xdebug, database)
- drupal-expert — Drupal patterns, coding standards, DI guidance
- scaffold — If the issue is structural, re-scaffold the component correctly
- drupal-frontend-expert — Debug Twig template, theming, and rendering issues
- drupal-security — Investigate security-related issues