Problem/Motivation

Computed fields are currently not available as Tokens. This may be a simple oversight as it seems like something that should be supported?

I can't see how it would be that different to replacing a basic field providing it's class extends FieldItemList.

Steps to reproduce

1. Create a custom content entity with a simple computed field

  /**
   * {@inheritdoc}
   */
  public static function baseFieldDefinitions(EntityTypeInterface $entity_type): array {

    $fields = parent::baseFieldDefinitions($entity_type);

    // Add a field that shows a link to the user's current company.
    $fields['label'] = BaseFieldDefinition::create('string')
      ->setName('label')
      ->setLabel(t('Label'))
      ->setComputed(TRUE)
      ->setReadOnly(TRUE)
      ->setClass(LabelComputedField::class)
      ->setDisplayConfigurable('view', TRUE)
      ->setDisplayOptions('view', [
        'label' => 'hidden',
        'weight' => -5,
      ]);

    return $fields;
  }

/**
 * Label computed field class.
 */
class LabelComputedField extends FieldItemList {
  use ComputedItemListTrait;
  /**
   * Computes the values for an item list.
   */
  protected function computeValue() {
    /** @var \Drupal\Core\Field\FieldItemInterface $item */
    $item = $this->createItem(0, 'My Amazing Label');
    $this->list[0] = $item;
  }
}

2. View available tokens. The computed field is not available.

Proposed resolution

The code in token.tokens.inc currently calls EntityFieldManager::getFieldStorageDefinitions():

$fields = \Drupal::service('entity_field.manager')->getFieldStorageDefinitions($entity_type_id);

Where this code deliberately ignores Computed fields:

foreach ($this->getBaseFieldDefinitions($entity_type_id) as $field_name => $definition) {
  if (!$definition->isComputed()) {
    $this->fieldStorageDefinitions[$entity_type_id][$field_name] = $definition;
  }
}

So either:

1. Create a way of retrieving fields, including computed definitions, from via `EntityFieldManager` either as an additional arg or new method
2. Rework _field_token_info_alter() to also separately fetch and merge in computed fields

Remaining tasks

1. Decide on best solution
2. Implement
3. Add tests

Comments

mattjones86 created an issue. See original summary.

mattjones86’s picture

This could also possibly just be fixed in contrib:


/**
 * Alter the metadata about available placeholder tokens and token types.
 *
 * @param $data
 *   The associative array of token definitions from hook_token_info().
 *
 * @see hook_token_info()
 */
function mymodule_token_info_alter(&$data) {
  $entity_type_manager = \Drupal::entityTypeManager();
  $type_info = \Drupal::service('plugin.manager.field.field_type')->getDefinitions();

  foreach ($entity_type_manager->getDefinitions() as $entity_type_id => $entity_type) {

    $definitions = \Drupal::service('entity_field.manager')->getBaseFieldDefinitions($entity_type_id);
    $token_type = \Drupal::service('token.entity_mapper')->getTokenTypeForEntityType($entity_type_id);

    // This is mostly lifted from token.tokens.inc, which does not include computed fields.
    // @see https://www.drupal.org/project/drupal/issues/3540258
    foreach ($definitions as $field_name => $field) {
      if ($field->isComputed()) {
        $labels = _token_field_label($entity_type_id, $field_name);
        $label = array_shift($labels);
        $params['@type'] = $type_info[$field->getType()]['label'];
        if (!empty($labels)) {
          $params['%labels'] = implode(', ', $labels);
          $description = t('@type field. Also known as %labels.', $params);
        }
        else {
          $description = t('@type field.', $params);
        }

        $cardinality = $field->getCardinality();
        $cardinality = ($cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED || $cardinality > 3) ? 3 : $cardinality;
        $field_token_name = $token_type . '-' . $field_name;

        $data['tokens'][$token_type][$field_name] = [
          'name' => Html::escape($label),
          'description' => $description,
          'module' => 'token',
          // For multivalue fields the field token is a list type.
          'type' => $cardinality > 1 ? "list<$field_token_name>" : $field_token_name,
        ];

        foreach ($field->getPropertyDefinitions() as $property => $property_definition) {
          if (is_subclass_of($property_definition->getClass(), 'Drupal\Core\TypedData\PrimitiveInterface')) {
            $data['tokens'][$field_token_name][$property] = [
              'name' => $property_definition->getLabel(),
              'description' => $property_definition->getDescription(),
              'module' => 'token',
            ];
          }
          elseif (($property_definition instanceof DataReferenceDefinitionInterface) && ($property_definition->getTargetDefinition() instanceof EntityDataDefinitionInterface)) {
            $referenced_entity_type = $property_definition->getTargetDefinition()
              ->getEntityTypeId();
            $referenced_token_type = \Drupal::service('token.entity_mapper')
              ->getTokenTypeForEntityType($referenced_entity_type);
            $data['tokens'][$field_token_name][$property] = [
              'name' => $property_definition->getLabel(),
              'description' => $property_definition->getDescription(),
              'module' => 'token',
              'type' => $referenced_token_type,
            ];
          }
        }
      }
    }
  }
}
quietone’s picture

Version: 11.2.x-dev » 11.x-dev

In Drupal core changes are made on on 11.x (our main development branch) first, and are then back ported as needed according to the Core change policies. Thanks.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.