Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
99.24% |
130 / 131 |
|
66.67% |
2 / 3 |
CRAP | |
0.00% |
0 / 1 |
| PrimaryEntityReferenceViewsHooks | |
99.24% |
130 / 131 |
|
66.67% |
2 / 3 |
9 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| fieldViewsData | |
99.22% |
128 / 129 |
|
0.00% |
0 / 1 |
7 | |||
| fieldViewsDataViewsDataAlter | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Drupal\primary_entity_reference\Hook; |
| 6 | |
| 7 | use Drupal\Component\Utility\DeprecationHelper; |
| 8 | use Drupal\Core\Entity\ContentEntityTypeInterface; |
| 9 | use Drupal\Core\Entity\EntityFieldManagerInterface; |
| 10 | use Drupal\Core\Entity\EntityTypeManagerInterface; |
| 11 | use Drupal\Core\Hook\Attribute\Hook; |
| 12 | use Drupal\Core\StringTranslation\StringTranslationTrait; |
| 13 | use Drupal\field\FieldStorageConfigInterface; |
| 14 | use Drupal\views\FieldViewsDataProvider; |
| 15 | |
| 16 | /** |
| 17 | * Hook implementations for primary_entity_reference. |
| 18 | */ |
| 19 | class 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 | } |