eca-1.0.x-dev/eca.tokens.inc
eca.tokens.inc
<?php /** * @file * Tokens provided by ECA. */ use Drupal\Core\Access\AccessibleInterface; use Drupal\Core\Cache\CacheableDependencyInterface; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\ContentEntityTypeInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Render\BubbleableMetadata; use Drupal\Core\Render\Markup; use Drupal\Core\TypedData\ListInterface; use Drupal\Core\TypedData\TraversableTypedDataInterface; use Drupal\Core\TypedData\TypedDataInterface; use Drupal\eca\Token\TokenServices; /** * Implements hook_token_info(). */ function eca_token_info(): array { $info = []; $info['types']['dto'] = [ 'name' => t('DTO'), 'description' => t('Tokens containing arbitrary data (Data Transfer Objects).'), 'needs-data' => 'dto', 'nested' => TRUE, 'dynamic' => TRUE, ]; $info['types']['_eca_root_token'] = [ 'name' => t('ECA root-level token'), 'description' => t('Support for tokens to access data on their root level, for example [list].'), 'nested' => TRUE, 'dynamic' => TRUE, ]; $info['types']['plain'] = [ 'name' => t('Plain value'), 'description' => t('Get the plain text value for any available token, without escaping HTML characters.'), ]; // @see https://www.drupal.org/project/eca/issues/3306180 $info['tokens']['dto']['dummy'] = [ 'name' => t('Just a dummy'), ]; $info['tokens']['_eca_root_token']['dummy'] = [ 'name' => t('Just a dummy'), ]; $info['tokens']['plain']['?'] = [ 'name' => '?', 'description' => t('Use any available token. Example: <b>[plain:node:title]</b>'), ]; return $info; } /** * Implements hook_token_info_alter(). */ function eca_token_info_alter(array &$data): void { $definitions = \Drupal::entityTypeManager()->getDefinitions(); foreach ($definitions as $definition) { if ($definition instanceof ContentEntityTypeInterface && isset($data['tokens'][$definition->id()])) { if ($definition->hasKey('id') && !isset($data['tokens'][$definition->id()]['id'])) { $data['tokens'][$definition->id()]['id'] = [ 'name' => t('Entity ID'), 'description' => t('The ID of the entity.'), ]; } if ($definition->hasKey('label') && !isset($data['tokens'][$definition->id()]['label'])) { $data['tokens'][$definition->id()]['label'] = [ 'name' => t('Entity label'), 'description' => t('The label of the entity.'), ]; } $data['tokens'][$definition->id()]['entity_type'] = [ 'name' => t('Entity type'), 'description' => t('The type ID of the entity.'), ]; } } } /** * Implements hook_tokens(). */ function eca_tokens(string $type, array $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata): array { $replacements = []; $definitions = \Drupal::entityTypeManager()->getDefinitions(); if (isset($definitions[$type]) && !empty($data[$type]) && $data[$type] instanceof ContentEntityInterface) { foreach ($tokens as $name => $original) { switch ($name) { case 'id': if ($definitions[$type]->hasKey('id')) { $replacements[$original] = $data[$type]->id(); } break; case 'label': if ($definitions[$type]->hasKey('label')) { $replacements[$original] = $data[$type]->label(); } break; case 'entity_type'; $replacements[$original] = $data[$type]->getEntityTypeId(); break; } } } if ($type === 'dto' && !empty($data['dto'])) { /** @var \Drupal\eca\Token\TokenInterface $token_services */ $token_services = \Drupal::service('eca.token_services'); /** @var \Drupal\eca\Plugin\DataType\DataTransferObject $dto */ $dto = $data['dto']; foreach ($tokens as $name => $original) { $access_allowed = TRUE; $parts = explode(':', $name); $typed_object = $dto; $entity = NULL; $token_type = NULL; $value = NULL; // Traverse through the property path, possibly until the leaf point or // when we found a token type that may take over for token replacements. while (($typed_object instanceof TraversableTypedDataInterface) && !empty($parts)) { $property = array_shift($parts); if (method_exists($typed_object, 'get') && (isset($typed_object->$property) || ($typed_object instanceof \ArrayAccess && isset($typed_object[$property])))) { $typed_object = $typed_object->get($property); } elseif ($typed_object instanceof ListInterface && !ctype_digit((string) $property)) { // Allow implicitly jumping to the first item of the list. array_unshift($parts, $property); $typed_object = $typed_object->first(); } else { $typed_object = NULL; } if (!($typed_object instanceof TypedDataInterface)) { $typed_object = NULL; break; } $value = $typed_object->getValue(); // Perform access checks and add existing cacheability metadata. foreach ([$typed_object, $value] as $subject) { if ($subject instanceof CacheableDependencyInterface) { $bubbleable_metadata->addCacheableDependency($subject); } if ($subject instanceof AccessibleInterface) { $access_result = $subject->access('view', NULL, TRUE); $access_allowed = $access_allowed && $access_result->isAllowed(); if ($access_result instanceof CacheableDependencyInterface) { $bubbleable_metadata->addCacheableDependency($access_result); } } } if ($value instanceof EntityInterface) { $entity = $value; } elseif (method_exists($typed_object, 'getEntity') && $entity = $typed_object->getEntity()) { // Field items and arbitrary item lists may belong to an entity. $bubbleable_metadata->addCacheableDependency($entity); $access_result = $entity->access('view', NULL, TRUE); $access_allowed = $access_allowed && $access_result->isAllowed(); if ($access_result instanceof CacheableDependencyInterface) { $bubbleable_metadata->addCacheableDependency($access_result); } } if ($entity && $token_type = $token_services->getTokenType($entity)) { if (isset($entity->$property)) { array_unshift($parts, $property); } break; } if ($token_type = $token_services->getTokenType($value)) { if ((is_object($value) && isset($value->$property)) || ((is_array($value) || $value instanceof \ArrayAccess) && isset($value[$property]))) { array_unshift($parts, $property); } break; } } if (!$access_allowed || !$typed_object || !$value) { continue; } if ($token_type && $parts) { $chained_token = implode(':', $parts); if ($entity) { $replacements += \Drupal::token()->generate($token_type, [$chained_token => $original], [$token_type => $entity], $options, $bubbleable_metadata); if (isset($replacements[$original])) { continue; } } $replacements += \Drupal::token()->generate($token_type, [$chained_token => $original], [$token_type => $value], $options, $bubbleable_metadata); if (isset($replacements[$original])) { continue; } } if (empty($parts)) { // Within the scope of DTOs, we assume conscious handling of string // values by their users. Therefore we allow passing through the // replacement values as safe markup. // @todo Reconsider this once https://www.drupal.org/node/2580723 // got fixed. $replacements[$original] = Markup::create($typed_object->getString()); } } } if ($type === '_eca_root_token') { $available = $data + TokenServices::get()->getTokenData(); foreach ($tokens as $name => $original) { if (isset($available[$name])) { $value = $available[$name] ?? NULL; $replacement = is_scalar($value) || is_null($value) || (is_object($value) && method_exists($value, '__toString')) ? (string) $value : ($value instanceof EntityInterface ? $value->id() : ''); if ($replacement !== '') { $replacements[$original] = Markup::create($replacement); } } } } if ($type === 'plain') { foreach ($tokens as $name => $original) { $replacement = (string) \Drupal::token()->replacePlain('[' . $name . ']', $data, ['clear' => TRUE] + $options, $bubbleable_metadata); if ($replacement !== '') { $replacements[$original] = Markup::create($replacement); } } } return $replacements; }