+ /**
+ * Get the apiv4 style custom field name.
+ *
+ * @param int $id
+ *
+ * @return string
+ *
+ * @throws \CRM_Core_Exception
+ */
+ protected function getCustomFieldName(int $id): string {
+ foreach ($this->getTokenMetadata() as $key => $field) {
+ if (($field['custom_field_id'] ?? NULL) === $id) {
+ return $key;
+ }
+ }
+ throw new CRM_Core_Exception(
+ "A custom field with the ID {$id} does not exist"
+ );
+ }
+
+ /**
+ * @param $entityID
+ * @param string $field eg. 'custom_1'
+ *
+ * @return array|string|void|null $mixed
+ *
+ * @throws \CRM_Core_Exception
+ */
+ protected function getCustomFieldValue($entityID, string $field) {
+ $id = str_replace('custom_', '', $field);
+ try {
+ $value = $this->prefetch[$entityID][$this->getCustomFieldName($id)] ?? '';
+ if ($value !== NULL) {
+ return CRM_Core_BAO_CustomField::displayValue($value, $id);
+ }
+ }
+ catch (CRM_Core_Exception $exception) {
+ return NULL;
+ }
+ }
+
+ /**
+ * Get the metadata for the field.
+ *
+ * @param string $fieldName
+ *
+ * @return array
+ */
+ protected function getMetadataForField($fieldName): array {
+ if (isset($this->getTokenMetadata()[$fieldName])) {
+ return $this->getTokenMetadata()[$fieldName];
+ }
+ if (isset($this->getTokenMappingsForRelatedEntities()[$fieldName])) {
+ return $this->getTokenMetadata()[$this->getTokenMappingsForRelatedEntities()[$fieldName]];
+ }
+ return $this->getTokenMetadata()[$this->getDeprecatedTokens()[$fieldName]] ?? [];
+ }
+
+ /**
+ * Get token mappings for related entities - specifically the contact entity.
+ *
+ * This function exists to help manage the way contact tokens is structured
+ * of an query-object style result set that needs to be mapped to apiv4.
+ *
+ * The end goal is likely to be to advertised tokens that better map to api
+ * v4 and deprecate the existing ones but that is a long-term migration.
+ *
+ * @return array
+ */
+ protected function getTokenMappingsForRelatedEntities(): array {
+ return [];
+ }
+
+ /**
+ * Get array of deprecated tokens and the new token they map to.
+ *
+ * @return array
+ */
+ protected function getDeprecatedTokens(): array {
+ return [];
+ }
+
+ /**
+ * Get any overrides for token metadata.
+ *
+ * This is most obviously used for setting the audience, which
+ * will affect widget-presence.
+ *
+ * @return \string[][]
+ */
+ protected function getTokenMetadataOverrides(): array {
+ return [];
+ }
+
+ /**
+ * To handle variable tokens, override this function and return the active tokens.
+ *
+ * @param \Civi\Token\Event\TokenValueEvent $e
+ *
+ * @return mixed
+ */
+ public function getActiveTokens(TokenValueEvent $e) {
+ $messageTokens = $e->getTokenProcessor()->getMessageTokens();
+ if (!isset($messageTokens[$this->entity])) {
+ return FALSE;
+ }
+ return array_intersect($messageTokens[$this->entity], array_keys($this->getTokenMetadata()));
+ }
+
+ /**
+ * Add the token to the metadata based on the field spec.
+ *
+ * @param array $tokensMetadata
+ * @param array $field
+ * @param array $exposedFields
+ * @param string $prefix
+ */
+ protected function addFieldToTokenMetadata(array &$tokensMetadata, array $field, array $exposedFields, string $prefix = ''): void {
+ if ($field['type'] !== 'Custom' && !in_array($field['name'], $exposedFields, TRUE)) {
+ return;
+ }
+ $field['audience'] = 'user';
+ if ($field['name'] === 'contact_id') {
+ // Since {contact.id} is almost always present don't confuse users
+ // by also adding (e.g {participant.contact_id)
+ $field['audience'] = 'sysadmin';
+ }
+ if (!empty($this->getTokenMetadataOverrides()[$field['name']])) {
+ $field = array_merge($field, $this->getTokenMetadataOverrides()[$field['name']]);
+ }
+ if ($field['type'] === 'Custom') {
+ // Convert to apiv3 style for now. Later we can add v4 with
+ // portable naming & support for labels/ dates etc so let's leave
+ // the space open for that.
+ // Not the existing quickform widget has handling for the custom field
+ // format based on the title using this syntax.
+ $parts = explode(': ', $field['label']);
+ $field['title'] = "{$parts[1]} :: {$parts[0]}";
+ $tokenName = 'custom_' . $field['custom_field_id'];
+ $tokensMetadata[$tokenName] = $field;
+ return;
+ }
+ $tokenName = $prefix ? ($prefix . '.' . $field['name']) : $field['name'];
+ if (in_array($field['name'], $exposedFields, TRUE)) {
+ if (
+ ($field['options'] || !empty($field['suffixes']))
+ // At the time of writing currency didn't have a label option - this may have changed.
+ && !in_array($field['name'], $this->getCurrencyFieldName(), TRUE)
+ ) {
+ $tokensMetadata[$tokenName . ':label'] = $tokensMetadata[$tokenName . ':name'] = $field;
+ $fieldLabel = $field['input_attrs']['label'] ?? $field['label'];
+ $tokensMetadata[$tokenName . ':label']['name'] = $field['name'] . ':label';
+ $tokensMetadata[$tokenName . ':name']['name'] = $field['name'] . ':name';
+ $tokensMetadata[$tokenName . ':name']['audience'] = 'sysadmin';
+ $tokensMetadata[$tokenName . ':label']['title'] = $fieldLabel;
+ $tokensMetadata[$tokenName . ':name']['title'] = ts('Machine name') . ': ' . $fieldLabel;
+ $field['audience'] = 'sysadmin';
+ }
+ if ($field['data_type'] === 'Boolean') {
+ $tokensMetadata[$tokenName . ':label'] = $field;
+ $tokensMetadata[$tokenName . ':label']['name'] = $field['name'] . ':label';
+ $field['audience'] = 'sysadmin';
+ }
+ $tokensMetadata[$tokenName] = $field;
+ }
+ }
+
+ /**
+ * Get a cache key appropriate to the current usage.
+ *
+ * @return string
+ */
+ protected function getCacheKey(): string {
+ $cacheKey = __CLASS__ . 'token_metadata' . $this->getApiEntityName() . CRM_Core_Config::domainID() . '_' . CRM_Core_I18n::getLocale();
+ if ($this->checkPermissions) {
+ $cacheKey .= '__' . CRM_Core_Session::getLoggedInContactID();
+ }
+ return $cacheKey;
+ }
+