# Drupal Module Structure Reference Complete guide to Drupal module structure and development patterns for Drupal 8-11+. ## Basic Module Structure ``` mymodule/ ├── mymodule.info.yml # Module metadata (required) ├── mymodule.module # Hook implementations ├── mymodule.routing.yml # Route definitions ├── mymodule.services.yml # Service definitions ├── mymodule.permissions.yml # Custom permissions ├── mymodule.links.menu.yml # Menu links ├── mymodule.links.task.yml # Tab links ├── mymodule.links.action.yml # Action links ├── composer.json # Composer dependencies ├── config/ │ ├── install/ # Default configuration │ └── schema/ # Configuration schema │ └── mymodule.schema.yml ├── src/ │ ├── Controller/ # Controllers │ ├── Form/ # Forms │ ├── Plugin/ # Plugins (Blocks, Fields, etc.) │ │ └── Block/ │ ├── Entity/ # Custom entities │ ├── EventSubscriber/ # Event subscribers │ └── Service/ # Custom services ├── templates/ # Twig templates └── tests/ ├── src/ │ ├── Unit/ # Unit tests │ ├── Kernel/ # Kernel tests │ └── Functional/ # Functional tests └── modules/ # Test modules ``` ## Module Info File (mymodule.info.yml) Required metadata for every module: ```yaml name: My Module description: 'Description of what the module does.' type: module core_version_requirement: ^9 || ^10 || ^11 package: Custom # Optional dependencies dependencies: - drupal:node - drupal:views - webform:webform # Optional configuration dependencies config_devel: install: - core.entity_view_mode.node.teaser optional: - views.view.frontpage # Optional PHP requirement php: ^8.1 # Optional - mark as hidden hidden: true # Optional - module version version: 1.0.0 ``` ## Routing (mymodule.routing.yml) Define routes for pages and controllers: ```yaml # Simple page route mymodule.hello: path: '/hello' defaults: _controller: '\Drupal\mymodule\Controller\HelloController::content' _title: 'Hello World' requirements: _permission: 'access content' # Route with parameters mymodule.user_page: path: '/user/{user}/custom' defaults: _controller: '\Drupal\mymodule\Controller\UserController::view' requirements: _permission: 'access content' user: \d+ options: parameters: user: type: entity:user # Form route mymodule.settings_form: path: '/admin/config/mymodule/settings' defaults: _form: '\Drupal\mymodule\Form\SettingsForm' _title: 'My Module Settings' requirements: _permission: 'administer site configuration' # Route with custom access mymodule.custom_access: path: '/custom-access' defaults: _controller: '\Drupal\mymodule\Controller\CustomController::content' requirements: _custom_access: '\Drupal\mymodule\Access\CustomAccessCheck::access' ``` ## Controllers ### Basic Controller **src/Controller/HelloController.php** ```php 'markup', '#markup' => $this->t('Hello World!'), ]; return $build; } } ``` ### Controller with Dependency Injection **src/Controller/AdvancedController.php** ```php database = $database; } /** * {@inheritdoc} */ public static function create(ContainerInterface $container) { return new static( $container->get('database') ); } /** * Builds the response. */ public function content() { // Use injected services $count = $this->database->query('SELECT COUNT(*) FROM {node}')->fetchField(); $build['content'] = [ '#type' => 'markup', '#markup' => $this->t('Total nodes: @count', ['@count' => $count]), ]; return $build; } } ``` ## Forms ### Configuration Form **src/Form/SettingsForm.php** ```php config('mymodule.settings'); $form['api_key'] = [ '#type' => 'textfield', '#title' => $this->t('API Key'), '#default_value' => $config->get('api_key'), '#required' => TRUE, ]; $form['enable_feature'] = [ '#type' => 'checkbox', '#title' => $this->t('Enable Feature'), '#default_value' => $config->get('enable_feature'), ]; return parent::buildForm($form, $form_state); } /** * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { $this->config('mymodule.settings') ->set('api_key', $form_state->getValue('api_key')) ->set('enable_feature', $form_state->getValue('enable_feature')) ->save(); parent::submitForm($form, $form_state); } } ``` ### Simple Form **src/Form/ContactForm.php** ```php 'textfield', '#title' => $this->t('Name'), '#required' => TRUE, ]; $form['email'] = [ '#type' => 'email', '#title' => $this->t('Email'), '#required' => TRUE, ]; $form['message'] = [ '#type' => 'textarea', '#title' => $this->t('Message'), '#required' => TRUE, ]; $form['actions']['submit'] = [ '#type' => 'submit', '#value' => $this->t('Send'), ]; return $form; } /** * {@inheritdoc} */ public function validateForm(array &$form, FormStateInterface $form_state) { if (strlen($form_state->getValue('message')) < 10) { $form_state->setErrorByName('message', $this->t('Message is too short.')); } } /** * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { // Process form submission $this->messenger()->addStatus($this->t('Thank you for your message!')); } } ``` ## Services (mymodule.services.yml) ```yaml services: mymodule.custom_service: class: Drupal\mymodule\Service\CustomService arguments: ['@entity_type.manager', '@current_user', '@logger.factory'] mymodule.event_subscriber: class: Drupal\mymodule\EventSubscriber\MyModuleSubscriber arguments: ['@current_user'] tags: - { name: event_subscriber } ``` ### Custom Service **src/Service/CustomService.php** ```php entityTypeManager = $entity_type_manager; $this->currentUser = $current_user; $this->logger = $logger; } /** * Performs a custom operation. */ public function doSomething() { $this->logger->info('Custom service method called by user @uid', [ '@uid' => $this->currentUser->id(), ]); // Custom logic here } } ``` ## Plugins ### Block Plugin **src/Plugin/Block/CustomBlock.php** ```php '', ]; } /** * {@inheritdoc} */ public function blockForm($form, FormStateInterface $form_state) { $form['custom_text'] = [ '#type' => 'textfield', '#title' => $this->t('Custom Text'), '#default_value' => $this->configuration['custom_text'], ]; return $form; } /** * {@inheritdoc} */ public function blockSubmit($form, FormStateInterface $form_state) { $this->configuration['custom_text'] = $form_state->getValue('custom_text'); } /** * {@inheritdoc} */ public function build() { return [ '#markup' => $this->configuration['custom_text'], ]; } } ``` ## Permissions (mymodule.permissions.yml) ```yaml administer mymodule: title: 'Administer My Module' description: 'Configure My Module settings' restrict access: true access mymodule content: title: 'Access My Module content' description: 'View content provided by My Module' # Dynamic permissions callback permission_callbacks: - Drupal\mymodule\MyModulePermissions::permissions ``` ## Menu Links (mymodule.links.menu.yml) ```yaml mymodule.admin: title: 'My Module' description: 'My Module settings' route_name: mymodule.settings_form parent: system.admin_config weight: 10 ``` ## Configuration Schema (config/schema/mymodule.schema.yml) ```yaml mymodule.settings: type: config_object label: 'My Module settings' mapping: api_key: type: string label: 'API Key' enable_feature: type: boolean label: 'Enable Feature' ``` ## Event Subscribers **src/EventSubscriber/MyModuleSubscriber.php** ```php ['onRequest', 0], ]; } /** * Kernel request event handler. */ public function onRequest(RequestEvent $event) { // React to request event } } ``` ## Best Practices 1. **Namespacing**: Follow PSR-4 autoloading standards 2. **Dependency Injection**: Use DI in classes, `\Drupal::service()` in .module files 3. **Coding Standards**: Follow Drupal coding standards (use PHPCS) 4. **Documentation**: Add comprehensive docblocks 5. **Security**: Sanitize output, validate input, check permissions 6. **Performance**: Cache when possible, avoid loading unnecessary data 7. **Testing**: Write unit, kernel, and functional tests 8. **Configuration**: Use config entities for exportable configuration 9. **Hooks**: Implement hooks in .module file, not in classes 10. **Services**: Create reusable services for business logic