Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
73.47% |
72 / 98 |
|
42.86% |
3 / 7 |
CRAP | |
0.00% |
0 / 1 |
| PrimaryEntityReferenceTokenHooks | |
73.47% |
72 / 98 |
|
42.86% |
3 / 7 |
64.97 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| tokenInfoAlter | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
5 | |||
| tokens | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
4 | |||
| processPrimaryTokens | |
78.05% |
32 / 41 |
|
0.00% |
0 / 1 |
16.07 | |||
| processIsPrimaryTokens | |
83.33% |
10 / 12 |
|
0.00% |
0 / 1 |
7.23 | |||
| getPrimaryEntityReferenceFields | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
| getPrimaryEntityReferenceFieldsForEntityType | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Drupal\primary_entity_reference\Hook; |
| 6 | |
| 7 | use Drupal\Core\Entity\ContentEntityInterface; |
| 8 | use Drupal\Core\Entity\EntityFieldManagerInterface; |
| 9 | use Drupal\Core\Entity\EntityTypeManagerInterface; |
| 10 | use Drupal\Core\Hook\Attribute\Hook; |
| 11 | use Drupal\Core\Render\BubbleableMetadata; |
| 12 | use Drupal\Core\StringTranslation\StringTranslationTrait; |
| 13 | use Drupal\Core\Utility\Token; |
| 14 | use Drupal\primary_entity_reference\Plugin\Field\PrimaryEntityReferenceFieldItemList; |
| 15 | |
| 16 | /** |
| 17 | * Token hook implementations for primary_entity_reference. |
| 18 | */ |
| 19 | class PrimaryEntityReferenceTokenHooks { |
| 20 | |
| 21 | use StringTranslationTrait; |
| 22 | |
| 23 | /** |
| 24 | * Constructs a new PrimaryEntityReferenceTokenHooks 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\Core\Utility\Token $token |
| 31 | * The token service. |
| 32 | */ |
| 33 | public function __construct( |
| 34 | protected readonly EntityTypeManagerInterface $entityTypeManager, |
| 35 | protected readonly EntityFieldManagerInterface $entityFieldManager, |
| 36 | protected readonly Token $token, |
| 37 | ) {} |
| 38 | |
| 39 | /** |
| 40 | * Implements hook_token_info_alter(). |
| 41 | * |
| 42 | * Adds token info for primary entity reference fields: |
| 43 | * - [entity:field_name:primary] - The primary referenced entity. |
| 44 | * - [entity:field_name:primary:*] - Chained tokens for the primary entity. |
| 45 | * - [entity:field_name:N:is_primary] - Whether delta N is the primary. |
| 46 | * |
| 47 | * @param array $info |
| 48 | * The token info array to alter. |
| 49 | */ |
| 50 | #[Hook('token_info_alter')] |
| 51 | public function tokenInfoAlter(array &$info): void { |
| 52 | // Get all primary entity reference fields across all entity types. |
| 53 | foreach ($this->getPrimaryEntityReferenceFields() as $entity_type_id => $fields) { |
| 54 | // Skip if no tokens for this entity type. |
| 55 | if (!isset($info['tokens'][$entity_type_id])) { |
| 56 | continue; |
| 57 | } |
| 58 | |
| 59 | foreach ($fields as $field_name => $field_definition) { |
| 60 | $target_type = $field_definition->getSetting('target_type'); |
| 61 | $target_entity_type = $this->entityTypeManager->getDefinition($target_type, FALSE); |
| 62 | |
| 63 | if (!$target_entity_type) { |
| 64 | continue; |
| 65 | } |
| 66 | |
| 67 | // Add the 'primary' pseudo-delta token. |
| 68 | $info['tokens'][$entity_type_id][$field_name . ':primary'] = [ |
| 69 | 'name' => $this->t('@field_name: Primary', ['@field_name' => $field_definition->getLabel()]), |
| 70 | 'description' => $this->t('The primary referenced @type.', [ |
| 71 | '@type' => $target_entity_type->getSingularLabel(), |
| 72 | ]), |
| 73 | 'type' => $target_type, |
| 74 | ]; |
| 75 | |
| 76 | // Add the 'is_primary' property token. |
| 77 | // This is added as a dynamic token that works with any delta. |
| 78 | $info['tokens'][$entity_type_id][$field_name . ':?:is_primary'] = [ |
| 79 | 'name' => $this->t('@field_name: Is primary', ['@field_name' => $field_definition->getLabel()]), |
| 80 | 'description' => $this->t('Whether the item at the specified delta is the primary reference (1 or 0).'), |
| 81 | 'dynamic' => TRUE, |
| 82 | ]; |
| 83 | } |
| 84 | } |
| 85 | } |
| 86 | |
| 87 | /** |
| 88 | * Implements hook_tokens(). |
| 89 | * |
| 90 | * Provides token replacements for primary entity reference fields. |
| 91 | * |
| 92 | * @param string $type |
| 93 | * The machine-readable name of the type of token being replaced. |
| 94 | * @param array $tokens |
| 95 | * An array of tokens to be replaced. |
| 96 | * @param array $data |
| 97 | * An associative array of data objects to be used for replacement. |
| 98 | * @param array $options |
| 99 | * An associative array of options for token replacement. |
| 100 | * @param \Drupal\Core\Render\BubbleableMetadata $bubbleable_metadata |
| 101 | * The bubbleable metadata. |
| 102 | * |
| 103 | * @return array |
| 104 | * An associative array of replacement values. |
| 105 | */ |
| 106 | #[Hook('tokens')] |
| 107 | public function tokens(string $type, array $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata): array { |
| 108 | $replacements = []; |
| 109 | |
| 110 | // Get the entity from data if available. |
| 111 | $entity = $data[$type] ?? NULL; |
| 112 | |
| 113 | if (!$entity instanceof ContentEntityInterface) { |
| 114 | return $replacements; |
| 115 | } |
| 116 | |
| 117 | // Get primary entity reference fields for this entity type. |
| 118 | $fields = $this->getPrimaryEntityReferenceFieldsForEntityType($entity->getEntityTypeId(), $entity->bundle()); |
| 119 | |
| 120 | if (empty($fields)) { |
| 121 | return $replacements; |
| 122 | } |
| 123 | |
| 124 | foreach ($fields as $field_name => $field_definition) { |
| 125 | // Handle :primary tokens. |
| 126 | $this->processPrimaryTokens($entity, $field_name, $tokens, $options, $bubbleable_metadata, $replacements); |
| 127 | |
| 128 | // Handle :N:is_primary tokens. |
| 129 | $this->processIsPrimaryTokens($entity, $field_name, $tokens, $replacements); |
| 130 | } |
| 131 | |
| 132 | return $replacements; |
| 133 | } |
| 134 | |
| 135 | /** |
| 136 | * Process primary-related tokens for a field. |
| 137 | * |
| 138 | * @param \Drupal\Core\Entity\ContentEntityInterface $entity |
| 139 | * The entity. |
| 140 | * @param string $field_name |
| 141 | * The field name. |
| 142 | * @param array $tokens |
| 143 | * The tokens to process. |
| 144 | * @param array $options |
| 145 | * Token replacement options. |
| 146 | * @param \Drupal\Core\Render\BubbleableMetadata $bubbleable_metadata |
| 147 | * The bubbleable metadata. |
| 148 | * @param array $replacements |
| 149 | * The replacements array to add to. |
| 150 | */ |
| 151 | protected function processPrimaryTokens( |
| 152 | ContentEntityInterface $entity, |
| 153 | string $field_name, |
| 154 | array $tokens, |
| 155 | array $options, |
| 156 | BubbleableMetadata $bubbleable_metadata, |
| 157 | array &$replacements, |
| 158 | ): void { |
| 159 | if (!$entity->hasField($field_name)) { |
| 160 | return; |
| 161 | } |
| 162 | |
| 163 | $field = $entity->get($field_name); |
| 164 | if (!$field instanceof PrimaryEntityReferenceFieldItemList) { |
| 165 | return; |
| 166 | } |
| 167 | |
| 168 | // Get the primary item. |
| 169 | $primary_item = $field->primary(); |
| 170 | if (!$primary_item) { |
| 171 | return; |
| 172 | } |
| 173 | |
| 174 | $target_type = $field->getFieldDefinition()->getSetting('target_type'); |
| 175 | |
| 176 | // Handle field_name:primary token (returns entity label). |
| 177 | if (isset($tokens[$field_name . ':primary'])) { |
| 178 | $referenced_entity = $primary_item->entity; |
| 179 | if ($referenced_entity) { |
| 180 | $replacements[$tokens[$field_name . ':primary']] = $referenced_entity->label(); |
| 181 | $bubbleable_metadata->addCacheableDependency($referenced_entity); |
| 182 | } |
| 183 | } |
| 184 | |
| 185 | // Handle field_name:primary:target_id token. |
| 186 | if (isset($tokens[$field_name . ':primary:target_id'])) { |
| 187 | $replacements[$tokens[$field_name . ':primary:target_id']] = $primary_item->target_id; |
| 188 | } |
| 189 | |
| 190 | // Handle field_name:primary:entity and chained tokens. |
| 191 | if ($primary_entity_tokens = $this->token->findWithPrefix($tokens, $field_name . ':primary')) { |
| 192 | $referenced_entity = $primary_item->entity; |
| 193 | if ($referenced_entity) { |
| 194 | $bubbleable_metadata->addCacheableDependency($referenced_entity); |
| 195 | |
| 196 | // Handle :entity chained tokens. |
| 197 | if ($entity_tokens = $this->token->findWithPrefix($primary_entity_tokens, 'entity')) { |
| 198 | $replacements += $this->token->generate( |
| 199 | $target_type, |
| 200 | $entity_tokens, |
| 201 | [$target_type => $referenced_entity], |
| 202 | $options, |
| 203 | $bubbleable_metadata |
| 204 | ); |
| 205 | } |
| 206 | |
| 207 | // Also handle direct chaining without :entity (e.g., :primary:title). |
| 208 | // Remove known tokens and entity-prefixed which are handled above. |
| 209 | $chained_tokens = $primary_entity_tokens; |
| 210 | unset($chained_tokens['target_id']); |
| 211 | |
| 212 | // Remove all entity-prefixed tokens (they're handled above). |
| 213 | foreach (array_keys($chained_tokens) as $key) { |
| 214 | if ($key === 'entity' || str_starts_with($key, 'entity:')) { |
| 215 | unset($chained_tokens[$key]); |
| 216 | } |
| 217 | } |
| 218 | |
| 219 | if (!empty($chained_tokens)) { |
| 220 | $replacements += $this->token->generate( |
| 221 | $target_type, |
| 222 | $chained_tokens, |
| 223 | [$target_type => $referenced_entity], |
| 224 | $options, |
| 225 | $bubbleable_metadata |
| 226 | ); |
| 227 | } |
| 228 | } |
| 229 | } |
| 230 | } |
| 231 | |
| 232 | /** |
| 233 | * Process is_primary tokens for a field. |
| 234 | * |
| 235 | * @param \Drupal\Core\Entity\ContentEntityInterface $entity |
| 236 | * The entity. |
| 237 | * @param string $field_name |
| 238 | * The field name. |
| 239 | * @param array $tokens |
| 240 | * The tokens to process. |
| 241 | * @param array $replacements |
| 242 | * The replacements array to add to. |
| 243 | */ |
| 244 | protected function processIsPrimaryTokens( |
| 245 | ContentEntityInterface $entity, |
| 246 | string $field_name, |
| 247 | array $tokens, |
| 248 | array &$replacements, |
| 249 | ): void { |
| 250 | if (!$entity->hasField($field_name)) { |
| 251 | return; |
| 252 | } |
| 253 | |
| 254 | $field = $entity->get($field_name); |
| 255 | if (!$field instanceof PrimaryEntityReferenceFieldItemList) { |
| 256 | return; |
| 257 | } |
| 258 | |
| 259 | // Look for field_name:N:is_primary patterns. |
| 260 | foreach ($tokens as $name => $original) { |
| 261 | // Match patterns like field_name:0:is_primary, field_name:1:is_primary. |
| 262 | if (preg_match('/^' . preg_quote($field_name, '/') . ':(\d+):is_primary$/', $name, $matches)) { |
| 263 | $delta = (int) $matches[1]; |
| 264 | |
| 265 | if (isset($field[$delta])) { |
| 266 | $item = $field[$delta]; |
| 267 | $is_primary = (bool) $item->get('primary')->getValue(); |
| 268 | $replacements[$original] = $is_primary ? '1' : '0'; |
| 269 | } |
| 270 | } |
| 271 | } |
| 272 | } |
| 273 | |
| 274 | /** |
| 275 | * Gets all primary entity reference fields grouped by entity type. |
| 276 | * |
| 277 | * @return array |
| 278 | * An array of field definitions keyed by entity type ID and field name. |
| 279 | */ |
| 280 | protected function getPrimaryEntityReferenceFields(): array { |
| 281 | $fields = []; |
| 282 | |
| 283 | $field_map = $this->entityFieldManager->getFieldMapByFieldType('primary_entity_reference'); |
| 284 | |
| 285 | foreach ($field_map as $entity_type_id => $entity_fields) { |
| 286 | foreach ($entity_fields as $field_name => $field_info) { |
| 287 | // Get the field definition from the first bundle. |
| 288 | $bundle = reset($field_info['bundles']); |
| 289 | $field_definitions = $this->entityFieldManager->getFieldDefinitions($entity_type_id, $bundle); |
| 290 | |
| 291 | if (isset($field_definitions[$field_name])) { |
| 292 | $fields[$entity_type_id][$field_name] = $field_definitions[$field_name]; |
| 293 | } |
| 294 | } |
| 295 | } |
| 296 | |
| 297 | return $fields; |
| 298 | } |
| 299 | |
| 300 | /** |
| 301 | * Gets primary entity reference fields for a specific entity type and bundle. |
| 302 | * |
| 303 | * @param string $entity_type_id |
| 304 | * The entity type ID. |
| 305 | * @param string $bundle |
| 306 | * The bundle. |
| 307 | * |
| 308 | * @return array |
| 309 | * An array of field definitions keyed by field name. |
| 310 | */ |
| 311 | protected function getPrimaryEntityReferenceFieldsForEntityType(string $entity_type_id, string $bundle): array { |
| 312 | $fields = []; |
| 313 | |
| 314 | $field_definitions = $this->entityFieldManager->getFieldDefinitions($entity_type_id, $bundle); |
| 315 | |
| 316 | foreach ($field_definitions as $field_name => $field_definition) { |
| 317 | if ($field_definition->getType() === 'primary_entity_reference') { |
| 318 | $fields[$field_name] = $field_definition; |
| 319 | } |
| 320 | } |
| 321 | |
| 322 | return $fields; |
| 323 | } |
| 324 | |
| 325 | } |