On this page
- Overview
- Creating Custom Import Sources
- Step 1: Create the Plugin Class
- Step 2: Understanding the Plugin Annotation
- The allowed_formats Property
- Simplified Annotation
- Step 3: Implementing Required Methods
- importMenu(string $menuName)
- getImportDescription()
- Step 4: Clear Cache and Test
- Adding Custom Configuration
- Defining the Plugin Schema
- Schema for Custom Configuration
- Using the Import Action Form
- Customizing the Action Form
- Implementation
- Reference Implementations
Extending Import Sources
Overview
Import Sources are plugins of type MenuMigrationSource that reside in Plugin\menu_migration\ImportSource.
They define where and how menu hierarchy data is retrieved for import, using a specified Format plugin to decode the menu data.
The module comes with two predefined import sources:
- Codebase - Imports menus from files in the codebase, using a configurable path relative to
DRUPAL_ROOT - File Upload - Imports one menu at a time by uploading a file through the browser
Creating Custom Import Sources
If the import sources provided by the module don't meet your needs, you can create your own custom source plugin.
Tip: If you create a source plugin that could be beneficial to others, I encourage you to contribute it back to the module.
Step 1: Create the Plugin Class
For this demonstration, let's create a plugin named ExampleSource. Within your custom module (we'll refer to it as MY_MODULE for this example), create the following file:
MY_MODULE/src/Plugin/menu_migration/ImportSource/ExampleSource.php
<?php
namespace Drupal\MY_MODULE\Plugin\menu_migration\ImportSource;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\menu_migration\Attribute\MenuMigrationSource;
use Drupal\menu_migration\Plugin\menu_migration\ImportSource\ImportSourceBase;
/**
* Provides an Example import source.
*/
#[MenuMigrationSource(
id: 'example',
label: new TranslatableMarkup('Example'),
allowed_formats: ['json', 'yaml'],
multiple: FALSE,
cli: TRUE
)]
class ExampleSource extends ImportSourceBase {
/**
* {@inheritdoc}
*/
public function importMenu(string $menuName) {
// Retrieve the menu data from your custom source
// For example: fetch from external API, read from custom storage, etc.
$menuData = $this->fetchMenuDataFromSource($menuName);
// Decode the menu data using the selected format
$menuTree = $this->getFormatPlugin()->decode($menuData);
// Generate the menu items in Drupal
$this->menuMigrationService->generateMenuItems($menuTree, $menuName);
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getImportDescription() {
// Return an array of translatable strings that will appear in the
// import confirmation form to inform the user what and where
// the import will happen from.
return [
$this->t('The selected menu will be imported from the Example source.'),
];
}
/**
* Helper method to fetch menu data from the custom source.
*/
protected function fetchMenuDataFromSource(string $menuName) {
// Implement your custom data retrieval logic here
return '';
}
}
Step 2: Understanding the Plugin Annotation
The #[MenuMigrationSource] attribute supports the following properties:
- id - The unique machine name of the plugin (required)
- label - The human-readable name displayed in the user interface (required)
- allowed_formats - An array of format plugin IDs that this source supports (required as of version 4.1.0)
- multiple - Boolean indicating if multiple menus can be imported at once (optional, defaults to
TRUE) - cli - Boolean indicating if the source supports Drush commands (optional, defaults to
FALSE)
The allowed_formats Property
As of version 4.1.0, the allowed_formats property is required for all Import Source plugins. This property defines which format plugins can be used with your source.
For example, the Codebase and FileUpload sources support file-based formats: ['json', 'yaml'].
Note: If allowed_formats is omitted, a deprecation warning is triggered and the defaults (json and yaml) are used. In version 5.0.0, omitting this property will cause an error. See drupal.org/node/3498853 for more information.
Simplified Annotation
If you're using the default values for multiple (TRUE) and cli (FALSE), you can simplify the annotation:
/**
* Provides an Example import source.
*/
#[MenuMigrationSource(
id: 'example',
label: new TranslatableMarkup('Example'),
allowed_formats: ['json', 'yaml']
)]
class ExampleSource extends ImportSourceBase {
Step 3: Implementing Required Methods
importMenu(string $menuName)
This method is called for each selected menu and must implement the import logic.
- Parameter:
$menuName- The machine name of the menu to import into - Returns: Boolean indicating success or failure
The ImportSourceBase class handles iterating through all selected menus, so you only need to implement the logic for importing a single menu.
getImportDescription()
This method returns an array of translatable strings that appear in both the import confirmation form (UI) and in the Drush command confirmation prompt, informing users what will happen during the import.
- Returns: An array of translatable strings or markup describing the import operation
- Note: Each array element is displayed as a separate line. Use multiple array elements to create multi-line descriptions
- Note: If the description contains HTML, the tags will be stripped when used with Drush
Step 4: Clear Cache and Test
After creating your import source plugin, clear Drupal's caches:
drush crNavigate to Configuration → Development → Menu Migration → Menu Imports, and click on +Add menu import. Your new source should appear in the Import Source field.

Adding Custom Configuration
If your import source requires additional configuration beyond the default format and menu selections, you can add custom form fields by implementing the following methods:
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'my_config' => '',
] + parent::defaultConfiguration();
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = parent::buildConfigurationForm($form, $form_state);
$form['my_config'] = [
'#type' => 'textfield',
'#title' => $this->t('My configuration'),
'#description' => $this->t('Enter something cool for my configuration.'),
'#default_value' => $this->configuration['my_config'],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
parent::validateConfigurationForm($form, $form_state);
// Add your custom validation logic here (optional)
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
parent::submitConfigurationForm($form, $form_state);
// Add your custom submit logic here (optional)
}
With this configuration added, your menu import form will include the custom field:

Defining the Plugin Schema
If your import source has custom configuration fields, you need to define a schema for proper configuration validation and data structure in your module's schema file:
MY_MODULE/config/schema/MY_MODULE.schema.yml
Note: If your plugin has no custom configuration fields beyond the defaults (format and menus), a schema definition is optional. The Menu Migration module provides a wildcard fallback (menu_migration.source_config.*) that will handle basic plugins automatically.
Schema for Custom Configuration
If your plugin defines custom configuration fields (like the my_config example above), extend the schema:
menu_migration.source_config.example:
type: source_destination_config_single
label: 'Example'
mapping:
my_config:
type: string
label: 'My configuration'
Where:
exampleis the ID of yourImportSourcepluginsource_destination_config_singleis the data type for plugins that don't allow multiple menus- If your plugin allows multiple menus (
multiple: TRUE), usesource_destination_config_multipleinstead
You can find existing schema examples in menu_migration/config/schema/menu_migration.schema.yml.
Using the Import Action Form
When you click the Import button in the Menu Imports listing, users are presented with a confirmation form before the import executes.

By default, this form displays a confirmation message and a submit button:

Customizing the Action Form
If you need to collect additional information just before import (information that shouldn't be stored permanently in the configuration), you can customize this form by implementing the ImportExportActionPluginInterface.
For example, the FileUpload import source uses this to prompt for a file upload before each import operation.
Implementation
Add the interface to your class declaration:
use Drupal\menu_migration\Plugin\ImportExportActionPluginInterface;
class ExampleSource extends ImportSourceBase implements ImportExportActionPluginInterface {
Then implement the required methods:
/**
* {@inheritdoc}
*/
public function buildActionForm(array $form, FormStateInterface $form_state) {
$form['something'] = [
'#type' => 'textfield',
'#title' => $this->t('Something'),
'#description' => $this->t('Enter something needed for this import.'),
'#required' => TRUE,
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateActionForm(array $form, FormStateInterface $form_state) {
// Add validation logic here (optional)
$value = $form_state->getValue('something');
if (empty($value)) {
$form_state->setErrorByName('something', $this->t('This field is required.'));
}
}
/**
* {@inheritdoc}
*/
public function submitActionForm(array $form, FormStateInterface $form_state) {
// Process the form values and store them for use in importMenu()
$this->configuration['runtime_value'] = $form_state->getValue('something');
}
With this implementation, the confirmation form will include your custom fields:

Reference Implementations
For complete examples of Import Source implementations, refer to the following files in the menu_migration/src/Plugin/menu_migration/ImportSource/ directory:
Codebase.php- Imports from the file system with custom directory configurationFileUpload.php- Imports from an uploaded file with validation and temporary file handling
Help improve this page
You can:
- Log in, click Edit, and edit this page
- Log in, click Discuss, update the Page status value, and suggest an improvement
- Log in and create a Documentation issue with your suggestion