Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
99.24% covered (success)
99.24%
130 / 131
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
PrimaryEntityReferenceViewsHooks
99.24% covered (success)
99.24%
130 / 131
66.67% covered (warning)
66.67%
2 / 3
9
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fieldViewsData
99.22% covered (success)
99.22%
128 / 129
0.00% covered (danger)
0.00%
0 / 1
7
 fieldViewsDataViewsDataAlter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\primary_entity_reference\Hook;
6
7use Drupal\Component\Utility\DeprecationHelper;
8use Drupal\Core\Entity\ContentEntityTypeInterface;
9use Drupal\Core\Entity\EntityFieldManagerInterface;
10use Drupal\Core\Entity\EntityTypeManagerInterface;
11use Drupal\Core\Hook\Attribute\Hook;
12use Drupal\Core\StringTranslation\StringTranslationTrait;
13use Drupal\field\FieldStorageConfigInterface;
14use Drupal\views\FieldViewsDataProvider;
15
16/**
17 * Hook implementations for primary_entity_reference.
18 */
19class PrimaryEntityReferenceViewsHooks {
20
21  use StringTranslationTrait;
22
23  /**
24   * Constructs a new PrimaryEntityReferenceViewsHooks instance.
25   *
26   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
27   *   The entity type manager.
28   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entityFieldManager
29   *   The entity field manager.
30   * @param \Drupal\views\FieldViewsDataProvider|null $fieldViewsDataProvider
31   *   The field views data provider service (available in Drupal 11.2+).
32   */
33  public function __construct(
34    protected readonly EntityTypeManagerInterface $entityTypeManager,
35    protected readonly EntityFieldManagerInterface $entityFieldManager,
36    protected readonly ?FieldViewsDataProvider $fieldViewsDataProvider = NULL,
37  ) {}
38
39  /**
40   * Implements hook_field_views_data().
41   *
42   * Views integration for primary entity reference fields. Adds standard
43   * relationships, primary-only relationships, and argument handling.
44   *
45   * @param \Drupal\field\FieldStorageConfigInterface $field_storage
46   *   The field storage configuration.
47   *
48   * @return array
49   *   The views data for the field.
50   */
51  #[Hook('field_views_data')]
52  public function fieldViewsData(FieldStorageConfigInterface $field_storage): array {
53    $data = DeprecationHelper::backwardsCompatibleCall(
54      currentVersion: \Drupal::VERSION,
55      deprecatedVersion: '11.2',
56      currentCallable: fn() => $this->fieldViewsDataProvider?->defaultFieldImplementation($field_storage),
57      deprecatedCallable: fn() => views_field_default_views_data($field_storage),
58    );
59
60    // The code below only deals with the primary_entity_reference field type.
61    if ($field_storage->getType() !== 'primary_entity_reference') {
62      return $data;
63    }
64
65    $entity_type_id        = $field_storage->getTargetEntityTypeId();
66    $target_entity_type_id = $field_storage->getSetting('target_type');
67    $target_entity_type    = $this->entityTypeManager->getDefinition($target_entity_type_id);
68    $entity_type           = $this->entityTypeManager->getDefinition($entity_type_id);
69    $target_base_table     = $target_entity_type->getDataTable() ?: $target_entity_type->getBaseTable();
70    $field_name            = $field_storage->getName();
71
72    /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
73    $table_mapping = $this->entityTypeManager->getStorage($entity_type_id)->getTableMapping();
74
75    foreach ($data as $table_name => $table_data) {
76      if (!($target_entity_type instanceof ContentEntityTypeInterface)) {
77        continue;
78      }
79
80      // Provide a standard relationship for all entity references.
81      $args = [
82        '@label'      => $target_entity_type->getLabel(),
83        '@field_name' => $field_name,
84      ];
85      $data[$table_name][$field_name]['relationship'] = [
86        'title'              => $this->t('@label referenced from @field_name', $args),
87        'label'              => $this->t('@field_name: @label', $args),
88        'group'              => $entity_type->getLabel(),
89        'help'               => $this->t('Appears in: @bundles.', [
90          '@bundles' => implode(', ', $field_storage->getBundles()),
91        ]),
92        'id'                 => 'standard',
93        'base'               => $target_base_table,
94        'entity type'        => $target_entity_type_id,
95        'base field'         => $target_entity_type->getKey('id'),
96        'relationship field' => $field_name . '_target_id',
97      ];
98
99      // Provide a primary-only relationship with join condition.
100      // Use a different key name to avoid conflict with the primary flag field.
101      $primary_relationship_field_name = $field_name . '__primary_target';
102      $data[$table_name][$primary_relationship_field_name]['relationship'] = [
103        'title'              => $this->t('Primary @label from @field_name', $args),
104        'label'              => $this->t('@field_name: Primary @label', $args),
105        'group'              => $entity_type->getLabel(),
106        'help'               => $this->t('Appears in: @bundles. This relationship will only include the primary reference.', [
107          '@bundles' => implode(', ', $field_storage->getBundles()),
108        ]),
109        'id'                 => 'standard',
110        'base'               => $target_base_table,
111        'entity type'        => $target_entity_type_id,
112        'base field'         => $target_entity_type->getKey('id'),
113        'relationship field' => $field_name . '_target_id',
114        'relationship table' => $table_name,
115        'extra'              => [
116          [
117            'table'   => $table_name,
118            'field'   => $field_name . '_primary',
119            'value'   => 1,
120            'numeric' => TRUE,
121          ],
122        ],
123      ];
124
125      // Provide a reverse relationship for the entity type that is referenced
126      // by the field.
127      $args['@entity'] = $entity_type->getLabel();
128      $args['@label'] = $target_entity_type->getSingularLabel();
129      $pseudo_field_name = 'reverse__' . $entity_type_id . '__' . $field_name;
130      $data[$target_base_table][$pseudo_field_name]['relationship'] = [
131        'title'      => $this->t('@entity using @field_name', $args),
132        'label'      => $this->t('@field_name', ['@field_name' => $field_name]),
133        'group'      => $target_entity_type->getLabel(),
134        'help'       => $this->t('Relate each @entity with a @field_name set to the @label.', $args),
135        'id'         => 'entity_reverse',
136        'base'       => $entity_type->getDataTable() ?: $entity_type->getBaseTable(),
137        'entity_type' => $entity_type_id,
138        'base field' => $entity_type->getKey('id'),
139        'field_name' => $field_name,
140        'field table' => $table_mapping->getDedicatedDataTableName($field_storage),
141        'field field' => $field_name . '_target_id',
142        'join_extra' => [
143          [
144            'field'   => 'deleted',
145            'value'   => 0,
146            'numeric' => TRUE,
147          ],
148        ],
149      ];
150
151      // Add reverse relationship for primary references only.
152      $primary_reverse_pseudo_field_name = 'reverse_primary__' . $entity_type_id . '__' . $field_name;
153      $data[$target_base_table][$primary_reverse_pseudo_field_name]['relationship'] = [
154        'title'       => $this->t('Primary @entity using @field_name', $args),
155        'label'       => $this->t('@field_name (Primary only)', ['@field_name' => $field_name]),
156        'group'       => $target_entity_type->getLabel(),
157        'help'        => $this->t('Relate each @entity with a @field_name set to the @label where the reference is marked as primary.', $args),
158        'id'          => 'entity_reverse',
159        'base'        => $entity_type->getDataTable() ?: $entity_type->getBaseTable(),
160        'entity_type' => $entity_type_id,
161        'base field'  => $entity_type->getKey('id'),
162        'field_name'  => $field_name,
163        'field table' => $table_mapping->getDedicatedDataTableName($field_storage),
164        'field field' => $field_name . '_target_id',
165        'join_extra'  => [
166          [
167            'field'   => 'deleted',
168            'value'   => 0,
169            'numeric' => TRUE,
170          ],
171          [
172            'field'   => $field_name . '_primary',
173            'value'   => 1,
174            'numeric' => TRUE,
175          ],
176        ],
177      ];
178
179      // Add the primary flag field to sort and filter.
180      $data[$table_name][$field_name . '_primary'] = [
181        'title'  => $this->t('@field_name: Primary flag', ['@field_name' => $field_name]),
182        'help'   => $this->t('Whether this reference is marked as primary.'),
183        'group'  => $entity_type->getLabel(),
184        'field'  => [
185          'id'           => 'boolean',
186          'label'        => $this->t('@field_name: Primary', ['@field_name' => $field_name]),
187          'type'         => 'yes-no',
188          'field_name'   => $field_name . '_primary',
189        ],
190        'sort'   => [
191          'id'         => 'standard',
192          'field_name' => $field_name . '_primary',
193        ],
194        'filter' => [
195          'id'         => 'boolean',
196          'field_name' => $field_name . '_primary',
197          'label'      => $this->t('Primary'),
198        ],
199      ];
200
201      // Provide an argument plugin that has a meaningful titleQuery()
202      // implementation getting the entity label.
203      $data[$table_name][$field_name . '_target_id']['argument']['id']                    = 'entity_target_id';
204      $data[$table_name][$field_name . '_target_id']['argument']['target_entity_type_id'] = $target_entity_type_id;
205    }
206
207    return $data;
208  }
209
210  /**
211   * Implements hook_field_views_data_views_data_alter().
212   *
213   * Views integration to provide reverse relationships on primary entity
214   * reference fields.
215   *
216   * @param array $data
217   *   The views data array to alter.
218   * @param \Drupal\field\FieldStorageConfigInterface $field_storage
219   *   The field storage configuration.
220   */
221  #[Hook('field_views_data_views_data_alter')]
222  public function fieldViewsDataViewsDataAlter(array &$data, FieldStorageConfigInterface $field_storage): void {
223    // The reverse relationships are already handled in fieldViewsData() method.
224    // This hook is left empty but implemented for completeness and future
225    // extensibility.
226  }
227
228}