Entity reference selection plugins break when not following a weird ID pattern

Created on 13 January 2016, almost 9 years ago
Updated 13 June 2024, 7 months ago

After debugging and struggling for hours trying to implement an entity reference selection plugin (ERSP), I've come to the following conclusion: An ERSP must declare its ID in the form of <group> or <group>:<id> or it will not work whatsoever.

Assume an annotation like this:

/**
 * Only shows the group roles which are available for a group type.
 *
 * @EntityReferenceSelection(
 *   id = "group_type_roles",
 *   label = @Translation("Group type role selection"),
 *   entity_types = {"group_role"},
 *   group = "group_type",
 *   weight = 0
 * )
 */
class GroupTypeRoleSelection extends DefaultSelection {

}

This will not work, but either id = "group_type", or id = "group_type:roles", will.

Examples of places where it breaks below.

1. When saving FieldConfig entities with a handler set to an "invalid" ERSP ID

Assume a FieldConfig like this:

    FieldConfig::create([
      // The storage is that of an ER field.
      'field_storage' => FieldStorageConfig::loadByName('group_content', 'group_roles'),
      'bundle' => 'foo',
      'label' => $this->t('Roles'),
      'settings' => [
        'handler' => 'group_type_role',
        'handler_settings' => [],
      ],
    ])->save();

If you try to save a field configuration with the ID directly set, your site breaks. This is because when it tries to load the plugin in SelectionPluginManager::getInstance(), it calls SelectionPluginManager::getPluginId() with $target_type "group_role" (correct) and $base_plugin_id "group_type_role" (breaks).

The reason it breaks is because SelectionPluginManager::getPluginId() will then treat the $base_plugin_id as a group name, where it's actually a plugin name. It can therefore not find any plugin ID to return, leading to the plugin not being instantiated and your ER field breaking completely.

2. When saving FieldConfig entities with a handler set to a group

In other places in code, it seems acceptable to just define a group as the handler setting, because it will then fall back to the handler with the highest weight for that group. This could be desirable behavior, as it allows other modules to intervene by defining a plugin with a higher weight.

As proven above, SelectionPluginManager::getPluginId() actually expects a group name instead of a plugin name.

Assume a FieldConfig like this:

    FieldConfig::create([
      // The storage is that of an ER field.
      'field_storage' => FieldStorageConfig::loadByName('group_content', 'group_roles'),
      'bundle' => 'foo',
      'label' => $this->t('Roles'),
      'settings' => [
        'handler' => 'group_type',
        'handler_settings' => [],
      ],
    ])->save();

The code in #2578249: Some e_r fields get the wrong Selection handler breaks this, however, unless your ERSP follows the naming pattern mentioned above.

This code works fine everywhere, up to the point where field_field_config_presave() intervenes and overwrites your handler setting with 'group_type_roles' because of the following lines:

  list($current_handler) = explode(':', $field->getSetting('handler'), 2);
  $field->setSetting('handler', $selection_manager->getPluginId($target_type, $current_handler));

As stated in the previous section, having a handler set to 'group_type_roles' will break your site.

3. When trying to configure an ER field to use your plugin through the UI

In EntityReferenceItem::fieldSettingsForm(), there is code that completely ignores your ERSP unless it follows the aforementioned plugin ID pattern:

    // Get all selection plugins for this entity type.
    $selection_plugins = \Drupal::service('plugin.manager.entity_reference_selection')->getSelectionGroups($this->getSetting('target_type'));
    $handlers_options = array();
    foreach (array_keys($selection_plugins) as $selection_group_id) {
      // We only display base plugins (e.g. 'default', 'views', ...) and not
      // entity type specific plugins (e.g. 'default:node', 'default:user',
      // ...).
      if (array_key_exists($selection_group_id, $selection_plugins[$selection_group_id])) {
        $handlers_options[$selection_group_id] = Html::escape($selection_plugins[$selection_group_id][$selection_group_id]['label']);
      }
      elseif (array_key_exists($selection_group_id . ':' . $this->getSetting('target_type'), $selection_plugins[$selection_group_id])) {
        $selection_group_plugin = $selection_group_id . ':' . $this->getSetting('target_type');
        $handlers_options[$selection_group_plugin] = Html::escape($selection_plugins[$selection_group_id][$selection_group_plugin]['base_plugin_label']);
      }
    }

This would only list my plugin if it had an ID of 'group_type' or 'group_type:group_role'. Do note that my plugin is under $selection_plugins['group_type']['group_type_role'] and can thus not be found by the above code.

All of the above leads to the conclusion that your ERSP must declare its ID as mentioned at the top of this issue summary or it will just not work.

🐛 Bug report
Status

Needs work

Version

11.0 🔥

Component
Entity 

Last updated about 17 hours ago

Created by

🇧🇪Belgium kristiaanvandeneynde Antwerp, Belgium

Live updates comments and jobs are added and updated live.
  • Contributed project blocker

    It denotes an issue that prevents porting of a contributed project to the stable version of Drupal due to missing APIs, regressions, and so on.

Sign in to follow issues

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • What I'm confused about is, if I want to create a sub-plugin of UserSelection, for example, how do I specify that in the annotation?

    This does work as a plugin, however it won't show in the list in the UI, so you have to hardcode it in the config YAML or PHP.

     * @EntityReferenceSelection(
     *   id = "default:user:custom",
     *   label = @Translation("Custom User"),
     *   entity_types = {"user"},
     *   group = "default",
     *   weight = 5
     * )
     */
    class CustomUserSelection extends UserSelection {
    
  • 🇫🇷France allan.ordogh

    Same problem, my Selection doesn't appear in the Reference method list in the UI with the code :

    namespace Drupal\my_module\Plugin\EntityReferenceSelection;
    
    use Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection;
    use Drupal\Core\Entity\Query\QueryInterface;
    
    /**
     * Provides entity reference selections to list only the field of the "product" bundle.
     *
     * @EntityReferenceSelection(
     *   id = "custom:my_id",
     *   label = @Translation("My label"),
     *   base_plugin_label = @Translation("My label"),
     *   group = "custom",
     *   weight = 2
     * )
     */
    class MyClasseSelection extends DefaultSelection {
    
       /**
       * {@inheritdoc}
       */
      protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS'): QueryInterface {
    
        $query = parent::buildEntityQuery($match, $match_operator);
        $query->condition('bundle', 'product');
    
        return $query;
      }
    
    }
    

    It's interpreted well when i put it in the config file :

    settings:
      handler: 'custom:my_id'
      handler_settings:
        target_bundles: null
        sort:
          field: weight
          direction: ASC
        auto_create: false
    

    Context : I want to use an entity reference field on my fields but i can't target the bundle in the UI.

Production build 0.71.5 2024