Need some help with your project? Contact me

Drupal 8: custom data on configuration entities using the ThirdPartySettingsInterface

In this article we are going to look at how to use the ThirdPartySettingsInterface to add some extra data to existing configuration entities. For example, if you ever need to store some config together with a node type or a taxonomy vocabulary, there is a great way to do so using this interface. Today we are going to see an example of this and add an extra field to the menu definition and store the value in this way.

There are a number of steps involved in this process. First, we need to alter the form with which the entity configuration data is added and saved. In the case of the menu entity there are two forms (one for adding and one for editing) so we need to alter them both. We can do something like this:

/**
 * Implements hook_form_alter().
 */
function my_module_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
  if ($form_id === 'menu_add_form' || $form_id === 'menu_edit_form') {
    my_module_alter_menu_forms($form, $form_state, $form_id);
  }
}

Inside this general hook_form_alter() implementation we delegate the logic to a custom function if the form is one of the two we need. Alternatively you can also implement hook_form_FORM_ID_alter() for both those forms and delegate from each. That would limit a bit on the function calls. But let's see our custom function:

/**
 * Handles the form alter for the menu_add_form and menu_edit_form forms.
 */
function my_module_alter_menu_forms(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
  $menu = $form_state->getFormObject()->getEntity();
  $form['my_text_field'] = array(
    '#type' => 'textfield',
    '#title' => t('My text field'),
    '#description' => t('This is some extra data'),
    '#default_value' => $menu->getThirdPartySetting('my_module', 'my_text_field'),
    '#weight' => 1
  );

  if (isset($form['links'])) {
    $form['links']['#weight'] = 2;
  }

  $form['#entity_builders'][] = 'my_module_form_menu_add_form_builder';
}

In here we do a couple of things. First, we retrieve the configuration entity object which the form is currently editing. Then, we define a new textfield and add it to the form. Next, we check if the form has menu links on it (meaning that it's probably the edit form) in which case we make its weight higher than one of our new field (just so that the form looks nicer). And last, we add a new #entity_builder to the form which will be triggered when the form is submitted.

The getThirdPartySetting() method on the entity object is provided by the ThirdPartySettingsInterface which all configuration entities have by default if they extend from the ConfigEntityBase class. With this method we simply retrieve a value that is stored as third party for a given module (my_module in this case). It will return NULL if none is set so we don't even need to provide a default in this case.

Let us now turn to our #entity_builder which gets called when the form is submitted and is responsible for mapping data to the entity:

/**
 * Entity builder for the menu configuration entity.
 */
function my_module_form_menu_add_form_builder($entity_type, \Drupal\system\Entity\Menu $menu, &$form, \Drupal\Core\Form\FormStateInterface $form_state) {
  if ($form_state->getValue('my_text_field')) {
    $menu->setThirdPartySetting('my_module', 'my_text_field', $form_state->getValue('my_text_field'));
    return;
  }

  $menu->unsetThirdPartySetting('my_module', 'my_text_field');
}

Inside we check if our textfield was filled in and set it to the third party setting we can access from the config entity object that is passed as an argument. If the form value is empty we reset the third party setting to remove lingering data in case there is something there.

And that's pretty much it for the business logic. We can clear the cache and try this out by creating/editing a menu and storing new data with it. However, our job is not quite finished. We need to add our configuration schema so that it becomes translatable. Inside the /config/schema/my_module.schema.yml file of our module we need to add this:

system.menu.*.third_party.my_module:
  type: mapping
  label: 'My module textfield'
  mapping:
    my_text_field:
      type: text
      label: 'My textfield'

With this schema definition we are basically appending to the schema of the system.menu config entity by specifying some metadata about the third party settings our module provides. For more information on config schemas be sure to check out the docs on Drupal.org.

Now if we reinstall our module and turn on configuration translation, we can translate the values users add to my_text_field. You go to admin/config/regional/config-translation/menu, select a menu and when translating in a different language you see a new Third Party Settings fieldset containing all the translatable values defined in the schema.

Hope this helps.

Comments

I think the last line of the function my_module_form_menu_add_form_builder should start $menu not $type?

Thanks for the tip. Sorry for the delay.

The website encountered an unexpected error. Please try again later.

TypeError: Argument 2 passed to menu_per_user_form_menu_add_form_builder() must be an instance of Drupal\system\Entity\Menu, instance of Drupal\menu_link_content\Entity\MenuLinkContent given in menu_per_user_form_menu_add_form_builder() (line 53 of modules/custom/menu_per_user/menu_per_user.module)

Using same code above.
Drupal 8.1.6

Hi Danny,
Thanks for the useful article! I've added an entity_autocomplete field, and if someone interested, the '#default_value' for 'entity_autocomplete' has to be either an entity object, or an array of entity objects (for multi-value field). Here's an example:

/**
 * Handles the form alter for the menu_add_form and menu_edit_form forms.
 */
function menu_series_connector_alter_menu_forms(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {

  // Load menu from $form_state.
  $menu = $form_state->getFormObject()->getEntity();

  // Get ThirdPartySetting from the menu.
  $seriesId = $menu->getThirdPartySetting('menu_series_connector', 'series_connector');

  // Add Entity Autocomplete field.
  $form['series_connector'] = array(
    '#type' => 'entity_autocomplete',
    '#target_type' => 'node',
    '#title' => t('Connect Menu to Specific Series'),
    '#description' => t('Connect Menu to Specific Series'),
    '#selection_settings' => array(
      'target_bundles' => array('series'),
    ),
    '#default_value' => (isset($seriesId) && is_numeric($seriesId)) ? \Drupal\node\Entity\Node::load($seriesId) : NULL,
    '#weight' => 1
  );

  if (isset($form['links'])) {
    $form['links']['#weight'] = 2;
  }

  $form['#entity_builders'][] = 'menu_series_connector_form_menu_add_form_builder';
}

Great example, thanks. This information was hard to find, but I adapted your example to work with node types, and it works well.

This is very cool and very helpful! Thank you.

Indeed! This is a very helpful example.

I tried to implement this for a Config Entity defined by a contrib module without success.

Later, I found out the ThirdPartySettings works only if the entity contains the "config_export" information ( See ConfigEntityType::getPropertiesToExport() )

The solution was to add the "config_export = {..." definition in the given Config Entity annotation.

Hope this information will help you.

Add new comment

You can post comments in Markdown and basic HTML tags.
For code blocks, wrap your code within '~~~'. For example:
~~~
$var = 'my variable';
~~~