Drupal Back End Specialist skill for custom module development, hooks, APIs, and PHP programming (Drupal 8-11+).
Enable expert-level Drupal back end development capabilities. Provide comprehensive guidance for custom module development, PHP programming, API usage, hooks, plugins, services, and database operations for Drupal 8, 9, 10, and 11+.
Invoke this skill when working with:
Create complete, standards-compliant Drupal modules:
Quick start workflow:
assets/module-template/MODULENAME with machine name (lowercase, underscores)MODULELABEL with human-readable nameddev drush en mymodule -yModule structure:
.info.yml - Module metadata and dependencies.module - Hook implementations.routing.yml - Route definitions.services.yml - Service definitions.permissions.yml - Custom permissionssrc/ - PHP classes (PSR-4 autoloading)config/ - Configuration filesReference documentation:
references/module_structure.md - Complete module patternsreferences/hooks.md - Hook implementationsImplement hooks to alter Drupal behavior:
Common hooks:
hook_entity_presave() - Modify entities before savinghook_entity_insert/update/delete() - React to entity changeshook_form_alter() - Modify any formhook_node_access() - Control node accesshook_cron() - Perform periodic taskshook_install/uninstall() - Module installation tasksHook pattern:
function MODULENAME_hook_name($param1, &$param2) {
// Implementation
}
Best practices:
.module file onlyCreate custom pages and endpoints:
Route definition (.routing.yml):
mymodule.example:
path: '/example/{param}'
defaults:
_controller: '\Drupal\mymodule\Controller\ExampleController::content'
_title: 'Example Page'
requirements:
_permission: 'access content'
param: \d+
Controller pattern:
namespace Drupal\mymodule\Controller;
use Drupal\Core\Controller\ControllerBase;
class ExampleController extends ControllerBase {
public function content() {
return ['#markup' => $this->t('Hello!')];
}
}
With dependency injection:
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->entityTypeManager = $entity_type_manager;
}
public static function create(ContainerInterface $container) {
return new static($container->get('entity_type.manager'));
}
Build configuration and custom forms:
Configuration form:
class SettingsForm extends ConfigFormBase {
protected function getEditableConfigNames() {
return ['mymodule.settings'];
}
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config('mymodule.settings');
$form['api_key'] = [
'#type' => 'textfield',
'#title' => $this->t('API Key'),
'#default_value' => $config->get('api_key'),
];
return parent::buildForm($form, $form_state);
}
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->config('mymodule.settings')
->set('api_key', $form_state->getValue('api_key'))
->save();
parent::submitForm($form, $form_state);
}
}
Form validation:
public function validateForm(array &$form, FormStateInterface $form_state) {
if (strlen($form_state->getValue('field')) < 5) {
$form_state->setErrorByName('field', $this->t('Too short.'));
}
}
Work with content and configuration entities:
Loading entities:
// Load single entity
$node = \Drupal::entityTypeManager()->getStorage('node')->load($nid);
// Load multiple entities
$nodes = \Drupal::entityTypeManager()->getStorage('node')->loadMultiple([1, 2, 3]);
// Load by properties
$nodes = \Drupal::entityTypeManager()->getStorage('node')->loadByProperties([
'type' => 'article',
'status' => 1,
]);
Creating/saving entities:
$node = \Drupal::entityTypeManager()->getStorage('node')->create([
'type' => 'article',
'title' => 'My Article',
'body' => ['value' => 'Content', 'format' => 'basic_html'],
]);
$node->save();
Entity queries:
$query = \Drupal::entityQuery('node')
->condition('type', 'article')
->condition('status', 1)
->accessCheck(TRUE)
->sort('created', 'DESC')
->range(0, 10);
$nids = $query->execute();
Create custom plugins (blocks, fields, etc.):
Block plugin:
/**
* @Block(
* id = "mymodule_custom_block",
* admin_label = @Translation("Custom Block"),
* )
*/
class CustomBlock extends BlockBase {
public function build() {
return ['#markup' => 'Block content'];
}
public function blockForm($form, FormStateInterface $form_state) {
$form['setting'] = [
'#type' => 'textfield',
'#title' => $this->t('Setting'),
'#default_value' => $this->configuration['setting'] ?? '',
];
return $form;
}
public function blockSubmit($form, FormStateInterface $form_state) {
$this->configuration['setting'] = $form_state->getValue('setting');
}
}
Create reusable services:
Service definition (.services.yml):
services:
mymodule.custom_service:
class: Drupal\mymodule\Service\CustomService
arguments: ['@entity_type.manager', '@current_user']
Service class:
namespace Drupal\mymodule\Service;
class CustomService {
protected $entityTypeManager;
protected $currentUser;
public function __construct(EntityTypeManagerInterface $entity_type_manager, AccountProxyInterface $current_user) {
$this->entityTypeManager = $entity_type_manager;
$this->currentUser = $current_user;
}
public function doSomething() {
// Business logic
}
}
Execute custom queries:
Using Database API:
$database = \Drupal::database();
// Select query
$query = $database->select('node_field_data', 'n')
->fields('n', ['nid', 'title'])
->condition('type', 'article')
->condition('status', 1)
->range(0, 10);
$results = $query->execute()->fetchAll();
// Insert
$database->insert('mymodule_table')
->fields(['name' => 'Example', 'value' => 123])
->execute();
// Update
$database->update('mymodule_table')
->fields(['value' => 456])
->condition('name', 'Example')
->execute();
Schema definition (.install file):
function mymodule_schema() {
$schema['mymodule_table'] = [
'fields' => [
'id' => ['type' => 'serial', 'not null' => TRUE],
'name' => ['type' => 'varchar', 'length' => 255, 'not null' => TRUE],
'value' => ['type' => 'int', 'not null' => TRUE, 'default' => 0],
],
'primary key' => ['id'],
'indexes' => ['name' => ['name']],
];
return $schema;
}
Scaffold the module:
cp -r assets/module-template/ /path/to/drupal/modules/custom/mymodule/
cd /path/to/drupal/modules/custom/mymodule/
mv MODULENAME.info.yml mymodule.info.yml
mv MODULENAME.module mymodule.module
mv MODULENAME.routing.yml mymodule.routing.yml
Update module files:
MODULENAME with machine nameMODULELABEL with readable name.info.yml dependenciesEnable and test:
ddev drush en mymodule -y
ddev drush cr
Develop iteratively:
ddev drush crddev drush watchdog:tail.module files$this->t() for all stringsclass MyModuleSubscriber implements EventSubscriberInterface {
public static function getSubscribedEvents() {
return [KernelEvents::REQUEST => ['onRequest', 0]];
}
public function onRequest(RequestEvent $event) {
// Handle event
}
}
# mymodule.permissions.yml
administer mymodule:
title: 'Administer My Module'
restrict access: true
class CustomAccessCheck implements AccessInterface {
public function access(AccountInterface $account) {
return AccessResult::allowedIf($account->hasPermission('access content'));
}
}
.info.yml syntaxcore_version_requirementddev drush crcomposer dump-autoloadddev drush updb after schema changesreferences/hooks.md - Common hooks with examples
references/module_structure.md - Module patterns
assets/module-template/ - Module scaffold.info.yml metadata.module hook file.routing.yml routes