Problem/Motivation

core_field_views_data() adds both forward and reverse relationship data for entity_reference fields defined in configuration. Same should be done for entity_reference fields NOT defined in configuration, like entity_reference baseFields.

Proposed resolution

Forward relationships for entity_reference baseFields are added in \Drupal\views\EntityViewsData::processViewsDataForEntityReference(), but reverse references cannot be added here as they have to be on different tables than this method can handle. For the same reason, \Drupal\views\EntityViewsData::mapFieldDefinition() is not applicable either. As the cycle at the end of views_views_data() triggers core_field_views_data() but only for fields stored in configuration, I think it would be a good place to start checking to add reverse relationships for entity_reference baseFields (possibly some other extra corner cases might be handled here which I'm unaware of yet).

Remaining tasks

User interface changes

API changes

Data model changes

Comments

Boobaa created an issue. See original summary.

boobaa’s picture

boobaa’s picture

I'm working on a module which provides several entity types with entity reference baseFields between them, both single- and multi-value ones. Here's how I got around this problem.

/**
 * Implements hook_views_data_alter().
 */
function mymodule_views_data_alter(array &$data) {
  // Entity reference base fields should have a reverse relationship in Views
  // (just like the sitebuilder-added entity reference fields have), not only
  // a forward relationship.
  // @see core_field_views_data()
  $entity_type_manager = \Drupal::entityTypeManager();
  /** @var \Drupal\Core\Entity\EntityFieldManager $entity_field_manager */
  $entity_field_manager = \Drupal::service('entity_field.manager');
  $entity_types = $entity_type_manager->getDefinitions();
  foreach ($entity_types as $source_entity_type_id => $source_entity_type) {
    // Handle only our own entity types as source.
    if ($source_entity_type->getProvider() != 'mymodule') {
      continue;
    }
    // Handle only content entity types as source.
    if (!($source_entity_type instanceof \Drupal\Core\Entity\ContentEntityType)) {
      continue;
    }
    $base_field_definitions = $entity_field_manager->getBaseFieldDefinitions($source_entity_type_id);
    foreach ($base_field_definitions as $base_field_id => $base_field_definition) {
      // Handle only entity reference fields.
      if ($base_field_definition->getType() != 'entity_reference') {
        continue;
      }
      /** @var \Drupal\Core\Field\TypedData\FieldItemDataDefinition $item_definition */
      $item_definition = $base_field_definition->getItemDefinition();
      $field_definition = $item_definition->getFieldDefinition();
      $field_storage = $field_definition->getFieldStorageDefinition();
      $target_entity_type_id = $field_storage->getSetting('target_type');
      $target_entity_type = $entity_type_manager->getDefinition($target_entity_type_id);
      // Handle only our own entity types as target.
      if ($target_entity_type->getProvider() != 'mymodule') {
        continue;
      }
      // Handle only content entity types as target.
      if (!($target_entity_type instanceof \Drupal\Core\Entity\ContentEntityType)) {
        continue;
      }

      // Here comes the heart of the dance.
      $pseudo_field_name = 'reverse__' . $source_entity_type_id . '__' . $base_field_id;
      $args = [
        '@label' => $target_entity_type->getLabel(),
        '@field_name' => $base_field_definition->getName(),
        '@entity' => $source_entity_type->getLabel(),
      ];
      // The sitebuilder-added entity reference fields have the same storage
      // schema regardless their cardinality. Sadly, this is not true for entity
      // reference base fields: multi-value fields (ones with cardinality != 1)
      // have a link table between the source and the target entities (just like
      // sitebuilder-added fields have one regardless their cardinality).
      if ($base_field_definition->isMultiple()) {
        $views_table = $target_entity_type->getDataTable();
        $field_table = $source_entity_type_id . '__' . $base_field_id;
        $field_field = $base_field_id . '_' . $base_field_definition->getMainPropertyName();
        $data[$views_table][$pseudo_field_name]['relationship'] = [
          'title' => t('@entity using @field_name', $args),
          'label' => t('Rev: @entity', $args),
          'group' => $target_entity_type->getLabel(),
          'help' => t('Relate each @entity with the @field_name field set to the @label.', $args),
          'id' => 'entity_reverse',
          'base' => $source_entity_type->getDataTable(),
//          'entity_type' => $source_entity_type_id,
          'base field' => $source_entity_type->getKey('id'),
//          'base field' => 'entity_id',
          'field_name' => $source_entity_type_id . '_' . $base_field_id,
          'field table' => $field_table,
          'field field' => $field_field,
          'join_extra' => [
            [
              'field' => 'deleted',
              'value' => 0,
              'numeric' => TRUE,
            ],
          ],
        ];
      }
      else {
        $field_table = $target_entity_type->getDataTable();
        $field_field = $base_field_id;
        // Cannot use the `entity_reverse` @ViewsRelationship handler plugin here,
        // since it always creates two JOINS, which is totally unneeded for
        // single-value baseFields, because their value is available right away on
        // the entity_type_field_data table.
        $data[$field_table][$pseudo_field_name]['relationship'] = [
          'title' => t('@entity using @field_name', $args),
          'label' => t('Rev: @entity', $args),
          'group' => $target_entity_type->getLabel(),
          'help' => t('Relate each @entity with the @field_name field set to the @label.', $args),
          'id' => 'standard',
          'base' => $source_entity_type->getDataTable() ?: $source_entity_type->getBaseTable(),
          'entity type' => $target_entity_type_id,
          'base field' => $base_field_id,
          'relationship field' => $target_entity_type->getKey('id'),
        ];
      }
    }
  }
}

I know it's somewhat quick and dirty, but hopefully helps moving forward.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

borisson_’s picture

Issue tags: +Needs tests

As soon as we get a test for #2795455: Views does not add relationship data for entity_reference fields not defined in configuration and are multiple, writing a test for this issue should be trivial, but at the very least I'm tagging this as needs tests.

The code writting in #4 can be used as a starting point, and I think this part is the one that we'd need to use:

        $field_table = $target_entity_type->getDataTable();
        $field_field = $base_field_id;
        // Cannot use the `entity_reverse` @ViewsRelationship handler plugin here,
        // since it always creates two JOINS, which is totally unneeded for
        // single-value baseFields, because their value is available right away on
        // the entity_type_field_data table.
        $data[$field_table][$pseudo_field_name]['relationship'] = [
          'title' => t('@entity using @field_name', $args),
          'label' => t('Rev: @entity', $args),
          'group' => $target_entity_type->getLabel(),
          'help' => t('Relate each @entity with the @field_name field set to the @label.', $args),
          'id' => 'standard',
          'base' => $source_entity_type->getDataTable() ?: $source_entity_type->getBaseTable(),
          'entity type' => $target_entity_type_id,
          'base field' => $base_field_id,
          'relationship field' => $target_entity_type->getKey('id'),
        ];

However, that is just from looking at the code here, I need this for a project (with only single-value er-basefields), so I will report back when I get this working.

borisson_’s picture

In the code I quoted earlier, there needed to be one change: $field_table = $target_entity_type->getBaseTable();

borisson_’s picture

@berdir mentioned \Drupal\Tests\views\Unit\EntityViewsDataTest as a possible place to test this. Let's see if that will work.

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

majdi’s picture

Status: Active » Closed (duplicate)
Issue tags: -Needs tests +duplicate