diff --git a/CRM/Contact/Tokens.php b/CRM/Contact/Tokens.php index d482c89447ca..b8798e27b911 100644 --- a/CRM/Contact/Tokens.php +++ b/CRM/Contact/Tokens.php @@ -56,13 +56,7 @@ public function registerTokens(TokenRegisterEvent $e): void { if (!$this->checkActive($e->getTokenProcessor())) { return; } - foreach (array_merge($this->getContactTokens(), $this->getCustomFieldTokens()) as $name => $label) { - $e->register([ - 'entity' => $this->entity, - 'field' => $name, - 'label' => $label, - ]); - } + parent::registerTokens($e); foreach ($this->getLegacyHookTokens() as $legacyHookToken) { $e->register([ 'entity' => $legacyHookToken['category'], @@ -133,96 +127,85 @@ public function getLegacyHookTokens(): array { } /** - * @return array - * @throws \CRM_Core_Exception + * Get all tokens advertised as contact tokens. + * + * @return string[] */ - public function getCustomFieldTokens(): array { - $tokens = []; - $customFields = \CRM_Core_BAO_CustomField::getFields(['Individual', 'Address', 'Contact']); - foreach ($customFields as $customField) { - $tokens['custom_' . $customField['id']] = $customField['label'] . " :: " . $customField['groupTitle']; - } - return $tokens; + public function getExposedFields(): array { + return [ + 'contact_type', + 'do_not_email', + 'do_not_phone', + 'do_not_mail', + 'do_not_sms', + 'do_not_trade', + 'is_opt_out', + 'external_identifier', + 'sort_name', + 'display_name', + 'nick_name', + 'image_URL', + 'preferred_communication_method', + 'preferred_language', + 'preferred_mail_format', + 'hash', + 'source', + 'first_name', + 'middle_name', + 'last_name', + 'prefix_id', + 'suffix_id', + 'formal_title', + 'communication_style_id', + 'job_title', + 'gender_id', + 'birth_date', + 'employer_id', + 'is_deleted', + 'created_date', + 'modified_date', + 'addressee_display', + 'email_greeting_display', + 'postal_greeting_display', + 'checksum', + 'id', + ]; } /** - * Get all tokens advertised as contact tokens. + * Get the fields exposed from related entities. * - * @return string[] + * @return \string[][] */ - public function getContactTokens(): array { + protected function getRelatedEntityTokenMetadata(): array { return [ - 'contact_type' => 'Contact Type', - 'do_not_email' => 'Do Not Email', - 'do_not_phone' => 'Do Not Phone', - 'do_not_mail' => 'Do Not Mail', - 'do_not_sms' => 'Do Not Sms', - 'do_not_trade' => 'Do Not Trade', - 'is_opt_out' => 'No Bulk Emails (User Opt Out)', - 'external_identifier' => 'External Identifier', - 'sort_name' => 'Sort Name', - 'display_name' => 'Display Name', - 'nick_name' => 'Nickname', - 'image_URL' => 'Image Url', - 'preferred_communication_method:label' => 'Preferred Communication Method', - 'preferred_language:label' => 'Preferred Language', - 'preferred_mail_format:label' => 'Preferred Mail Format', - 'hash' => 'Contact Hash', - 'source' => 'Contact Source', - 'first_name' => 'First Name', - 'middle_name' => 'Middle Name', - 'last_name' => 'Last Name', - 'prefix_id:label' => 'Individual Prefix', - 'suffix_id:label' => 'Individual Suffix', - 'formal_title' => 'Formal Title', - 'communication_style_id:label' => 'Communication Style', - 'job_title' => 'Job Title', - 'gender_id:label' => 'Gender ID', - 'birth_date' => 'Birth Date', - 'current_employer_id' => 'Current Employer ID', - 'is_deleted:label' => 'Contact is in Trash', - 'created_date' => 'Created Date', - 'modified_date' => 'Modified Date', - 'addressee_display' => 'Addressee', - 'email_greeting_display' => 'Email Greeting', - 'postal_greeting_display' => 'Postal Greeting', - 'current_employer' => 'Current Employer', - 'location_type' => 'Location Type', - 'address_id' => 'Address ID', - 'street_address' => 'Street Address', - 'street_number' => 'Street Number', - 'street_number_suffix' => 'Street Number Suffix', - 'street_name' => 'Street Name', - 'street_unit' => 'Street Unit', - 'supplemental_address_1' => 'Supplemental Address 1', - 'supplemental_address_2' => 'Supplemental Address 2', - 'supplemental_address_3' => 'Supplemental Address 3', - 'city' => 'City', - 'postal_code_suffix' => 'Postal Code Suffix', - 'postal_code' => 'Postal Code', - 'geo_code_1' => 'Latitude', - 'geo_code_2' => 'Longitude', - 'manual_geo_code' => 'Is Manually Geocoded', - 'address_name' => 'Address Name', - 'master_id' => 'Master Address ID', - 'county' => 'County', - 'state_province' => 'State', - 'country' => 'Country', - 'phone' => 'Phone', - 'phone_ext' => 'Phone Extension', - 'phone_type_id' => 'Phone Type ID', - 'phone_type' => 'Phone Type', - 'email' => 'Email', - 'on_hold' => 'On Hold', - 'signature_text' => 'Signature Text', - 'signature_html' => 'Signature Html', - 'im_provider' => 'IM Provider', - 'im' => 'IM Screen Name', - 'openid' => 'OpenID', - 'world_region' => 'World Region', - 'url' => 'Website', - 'checksum' => 'Checksum', - 'id' => 'Internal Contact ID', + 'address' => [ + 'location_type_id', + 'id', + 'street_address', + 'street_number', + 'street_number_suffix', + 'street_name', + 'street_unit', + 'supplemental_address_1', + 'supplemental_address_2', + 'supplemental_address_3', + 'city', + 'postal_code_suffix', + 'postal_code', + 'geo_code_1', + 'geo_code_2', + 'name', + 'master_id', + 'county_id', + 'state_province_id', + 'country_id', + ], + 'phone' => ['phone', 'phone_ext', 'phone_type_id'], + 'email' => ['email', 'signature_html', 'signature_text', 'on_hold'], + 'website' => ['url'], + 'OpenID' => ['openid'], + 'im' => ['name', 'provider_id'], ]; } @@ -290,7 +273,6 @@ public function onEvaluate(TokenValueEvent $e) { if (empty($messageTokens)) { return; } - $this->fieldMetadata = (array) civicrm_api4('Contact', 'getfields', ['checkPermissions' => FALSE], 'name'); foreach ($e->getRows() as $row) { if (empty($row->context['contactId']) && empty($row->context['contact'])) { @@ -314,24 +296,11 @@ public function onEvaluate(TokenValueEvent $e) { $row->format('text/html') ->tokens('contact', $token, "cs={$cs}"); } - elseif (!empty($row->context['contact'][$token]) && - $this->isDateField($token) - ) { - // Handle dates here, for now. Standardise with other token entities next round - $row->format('text/plain')->tokens('contact', $token, \CRM_Utils_Date::customFormat($row->context['contact'][$token])); - } - elseif ( - ($row->context['contact'][$token] ?? '') == 0 - && $this->isBooleanField($token)) { - // Note this will be the default behaviour once we fetch with apiv4. - $row->format('text/plain')->tokens('contact', $token, ''); - } elseif ($token === 'signature_html') { $row->format('text/html')->tokens('contact', $token, html_entity_decode($row->context['contact'][$token])); } else { - $row->format('text/html') - ->tokens('contact', $token, $this->getFieldValue($row, $token)); + parent::evaluateToken($row, $this->entity, $token, $row->context['contact']); } } } @@ -367,56 +336,42 @@ protected function getFieldValue(TokenRow $row, string $field) { } /** - * Is the given field a boolean field. - * - * @param string $fieldName + * Get the metadata for the available fields. * - * @return bool + * @return array + * @noinspection PhpDocMissingThrowsInspection + * @noinspection PhpUnhandledExceptionInspection */ - public function isBooleanField(string $fieldName): bool { - // no metadata for these 2 non-standard fields - // @todo - fix to api v4 & have metadata for all fields. Migrate contact_is_deleted - // to {contact.is_deleted}. on hold feels like a token that exists by - // accident & could go.... since it's not from the main entity. - if (in_array($fieldName, ['contact_is_deleted', 'on_hold'])) { - return TRUE; + protected function getTokenMetadata(): array { + if ($this->tokensMetadata) { + return $this->tokensMetadata; } - if (empty($this->getFieldMetadata()[$fieldName])) { - return FALSE; + if (Civi::cache('metadata')->has($this->getCacheKey())) { + return Civi::cache('metadata')->get($this->getCacheKey()); } - return $this->getFieldMetadata()[$fieldName]['data_type'] === 'Boolean'; - } - - /** - * Is the given field a date field. - * - * @param string $fieldName - * - * @return bool - */ - public function isDateField(string $fieldName): bool { - if (empty($this->getFieldMetadata()[$fieldName])) { - return FALSE; + $this->fieldMetadata = (array) civicrm_api4('Contact', 'getfields', ['checkPermissions' => FALSE], 'name'); + $this->tokensMetadata = $this->getBespokeTokens(); + foreach ($this->fieldMetadata as $field) { + $this->addFieldToTokenMetadata($field, $this->getExposedFields()); } - return in_array($this->getFieldMetadata()[$fieldName]['data_type'], ['Timestamp', 'Date'], TRUE); - } - /** - * Get the metadata for the available fields. - * - * @return array - */ - protected function getFieldMetadata(): array { - if (empty($this->fieldMetadata)) { - try { - // Tests fail without checkPermissions = FALSE - $this->fieldMetadata = (array) civicrm_api4('Contact', 'getfields', ['checkPermissions' => FALSE], 'name'); - } - catch (\API_Exception $e) { - $this->fieldMetadata = []; + foreach ($this->getRelatedEntityTokenMetadata() as $entity => $exposedFields) { + $metadata = (array) civicrm_api4($entity, 'getfields', ['checkPermissions' => FALSE], 'name'); + foreach ($metadata as $field) { + if ($entity === 'address' && in_array($field['name'], ['name', 'id'])) { + // The original thought was to support apiv4 join prefixes for + // tokens across the board - advertising {contact.primary_address.street_name} + // and providing less visible support for {contact.billing_address.street_name} + // for workflow templates. That change seems a bit too ambitious for now + // so only the field that does not map to the real db name is being prefixed + // for now. + $field['advertised_name'] = 'primary_' . $entity . '.' . $field['name']; + } + $this->addFieldToTokenMetadata($field, $exposedFields, 'primary_' . $entity); } } - return $this->fieldMetadata; + Civi::cache('metadata')->set($this->getCacheKey(), $this->tokensMetadata); + return $this->tokensMetadata; } /** @@ -556,7 +511,10 @@ protected function getDeprecatedTokens(): array { return [ 'individual_prefix' => 'prefix_id:label', 'individual_suffix' => 'suffix_id:label', + 'contact_type' => 'contact_type:label', 'gender' => 'gender_id:label', + 'address_name' => 'primary_address.name', + 'address_id' => 'primary_address.id', 'communication_style' => 'communication_style_id:label', 'preferred_communication_method' => 'preferred_communication_method:label', 'email_greeting' => 'email_greeting_display', @@ -565,6 +523,102 @@ protected function getDeprecatedTokens(): array { 'contact_id' => 'id', 'contact_source' => 'source', 'contact_is_deleted' => 'is_deleted:label', + 'current_employer_id' => 'employer_id', + 'current_employer' => 'employer_id.display_name', + ]; + } + + /** + * Get the tokens that are accessed by joining onto a related entity. + * + * Note the original thinking was to migrate to advertising the tokens + * that more accurately reflect the schema & also add support for e.g + * billing_address.street_address - which would be hugely useful for workflow + * message templates. + * + * However that feels like a bridge too far for this round + * since we haven't quite hit the goal of all token processing going through + * the token processor & we risk advertising tokens that don't work if we get + * ahead of that process. + * + * @return string[] + */ + protected function getTokenMappingsForRelatedEntities(): array { + return [ + 'on_hold' => 'primary_email.on_hold:label', + 'phone_type_id' => 'primary_phone.phone_type_id', + 'current_employer' => 'employer_id.display_name', + 'location_type_id' => 'primary_address.location_type_id', + 'location_type' => 'primary_address.location_type_id:label', + 'street_address' => 'primary_address.street_address', + 'address_id' => 'primary_address.id', + 'street_number' => 'primary_address.street_number', + 'street_number_suffix' => 'primary_address.street_number_suffix', + 'street_name' => 'primary_address.street_name', + 'street_unit' => 'primary_address.street_unit', + 'supplemental_address_1' => 'primary_address.supplemental_address_1', + 'supplemental_address_2' => 'primary_address.supplemental_address_2', + 'supplemental_address_3' => 'primary_address.supplemental_address_3', + 'city' => 'primary_address.city', + 'postal_code' => 'primary_address.postal_code', + 'postal_code_suffix' => 'primary_address.postal_code_suffix', + 'geo_code_1' => 'primary_address.geo_code_1', + 'geo_code_2' => 'primary_address.geo_code_2', + 'master_id' => 'primary_address.master_id', + 'county' => 'primary_address.county_id:label', + 'county_id' => 'primary_address.county_id', + 'state_province' => 'primary_address.state_province_id:abbr', + 'state_province_id' => 'primary_address.state_province_id', + 'country' => 'primary_address.country_id:label', + 'country_id' => 'primary_address.country_id', + 'world_region' => 'primary_address.country_id.region_id:name', + 'phone_type' => 'primary_phone.phone_type_id:label', + 'phone' => 'primary_phone.phone', + 'phone_ext' => 'primary_phone.phone_ext', + 'email' => 'primary_email.email', + 'signature_text' => 'primary_email.signature_text', + 'signature_html' => 'primary_email.signature_html', + 'im' => 'primary_im.name', + 'im_provider' => 'primary_im.provider_id:label', + 'provider_id' => 'primary_im.provider_id', + 'openid' => 'primary_OpenID.openid', + 'url' => 'primary_website.url', + ]; + } + + /** + * Get calculated or otherwise 'special', tokens. + * + * @return array[] + */ + protected function getBespokeTokens(): array { + return [ + 'checksum' => [ + 'title' => ts('Checksum'), + 'name' => 'checksum', + 'type' => 'calculated', + 'options' => NULL, + 'data_type' => 'String', + 'audience' => 'user', + ], + 'employer_id.display_name' => [ + 'title' => ts('Current Employer'), + 'name' => 'employer_id.display_name', + 'type' => 'mapped', + 'api_v3' => 'current_employer', + 'options' => NULL, + 'data_type' => 'String', + 'audience' => 'user', + ], + 'primary_address.country_id.region_id:name' => [ + 'title' => ts('World Region'), + 'name' => 'country_id.region_id.name', + 'type' => 'mapped', + 'api_v3' => 'world_region', + 'options' => NULL, + 'data_type' => 'String', + 'audience' => 'user', + ], ]; } diff --git a/CRM/Core/EntityTokens.php b/CRM/Core/EntityTokens.php index 5f2baaccbce9..cf2d1071a459 100644 --- a/CRM/Core/EntityTokens.php +++ b/CRM/Core/EntityTokens.php @@ -61,7 +61,11 @@ public function registerTokens(TokenRegisterEvent $e) { if ($field['audience'] === 'user') { $e->register([ 'entity' => $this->entity, - 'field' => $tokenName, + // Advertised name allows us a longer transition for contact tokens + // where the 'real' name is {contact.primary_address.street_name} + // but we don't want to advertise that until we are confident the + // list is being used in the context of the token processor. + 'field' => $field['advertised_name'] ?? $tokenName, 'label' => $field['title'], ]); } @@ -82,49 +86,7 @@ protected function getTokenMetadata(): array { else { $this->tokensMetadata = $this->getBespokeTokens(); foreach ($this->getFieldMetadata() as $field) { - $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']; - $this->tokensMetadata[$tokenName] = $field; - continue; - } - if (in_array($field['name'], $this->getExposedFields(), 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) - ) { - $this->tokensMetadata[$field['name'] . ':label'] = $this->tokensMetadata[$field['name'] . ':name'] = $field; - $fieldLabel = $field['input_attrs']['label'] ?? $field['label']; - $this->tokensMetadata[$field['name'] . ':label']['name'] = $field['name'] . ':label'; - $this->tokensMetadata[$field['name'] . ':name']['name'] = $field['name'] . ':name'; - $this->tokensMetadata[$field['name'] . ':name']['audience'] = 'sysadmin'; - $this->tokensMetadata[$field['name'] . ':label']['title'] = $fieldLabel; - $this->tokensMetadata[$field['name'] . ':name']['title'] = ts('Machine name') . ': ' . $fieldLabel; - $field['audience'] = 'sysadmin'; - } - if ($field['data_type'] === 'Boolean') { - $this->tokensMetadata[$field['name'] . ':label'] = $field; - $this->tokensMetadata[$field['name'] . ':label']['name'] = $field['name'] . ':label'; - $field['audience'] = 'sysadmin'; - } - $this->tokensMetadata[$field['name']] = $field; - } + $this->addFieldToTokenMetadata($field, $this->getExposedFields()); } foreach ($this->getHiddenTokens() as $name) { $this->tokensMetadata[$name]['audience'] = 'hidden'; @@ -152,6 +114,14 @@ public function evaluateToken(TokenRow $row, $entity, $field, $prefetch = NULL) // If it's set here it has already been loaded in pre-fetch. return $row->format('text/plain')->tokens($entity, $field, (string) $fieldValue); } + if ($field === 'state_province_id:abbr') { + // Hack handle this one for now. + $apiv3Value = $this->getFieldValue($row, 'state_province'); + if ($apiv3Value) { + return $row->tokens($entity, $field, $apiv3Value); + } + } + // Once prefetch is fully standardised we can remove this - as long // as tests pass we should be fine as tests cover this. $split = explode(':', $field); @@ -219,13 +189,6 @@ public function getExtendableTableName(): string { return CRM_Core_DAO_AllCoreTables::getTableForEntityName($this->getApiEntityName()); } - /** - * Get the relevant bao name. - */ - public function getBAOName(): string { - return CRM_Core_DAO_AllCoreTables::getFullName($this->getApiEntityName()); - } - /** * Get an array of fields to be requested. * @@ -328,11 +291,16 @@ protected function getBespokeTokens(): array { * @internal function will likely be protected soon. */ protected function getPseudoValue(string $realField, string $pseudoKey, $fieldValue): string { + $bao = CRM_Core_DAO_AllCoreTables::getFullName($this->getMetadataForField($realField)['entity']); if ($pseudoKey === 'name') { - $fieldValue = (string) CRM_Core_PseudoConstant::getName($this->getBAOName(), $realField, $fieldValue); + $fieldValue = (string) CRM_Core_PseudoConstant::getName($bao, $realField, $fieldValue); } if ($pseudoKey === 'label') { - $fieldValue = (string) CRM_Core_PseudoConstant::getLabel($this->getBAOName(), $realField, $fieldValue); + $fieldValue = (string) CRM_Core_PseudoConstant::getLabel($bao, $realField, $fieldValue); + } + if ($pseudoKey === 'abbr' && $realField === 'state_province_id') { + // hack alert - currently only supported for state. + $fieldValue = (string) CRM_Core_PseudoConstant::stateProvinceAbbreviation($fieldValue); } return (string) $fieldValue; } @@ -583,9 +551,27 @@ 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. * @@ -622,6 +608,72 @@ public function getActiveTokens(TokenValueEvent $e) { return array_intersect($messageTokens[$this->entity], array_keys($this->getTokenMetadata())); } + /** + * Add the token to the metadata based on the field spec. + * + * @param array $field + * @param array $exposedFields + * @param string $prefix + */ + protected function addFieldToTokenMetadata(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']; + if ($prefix) { + $tokenName = $prefix . '.' . $tokenName; + } + $this->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) + ) { + $this->tokensMetadata[$tokenName . ':label'] = $this->tokensMetadata[$tokenName . ':name'] = $field; + $fieldLabel = $field['input_attrs']['label'] ?? $field['label']; + $this->tokensMetadata[$tokenName . ':label']['name'] = $field['name'] . ':label'; + $this->tokensMetadata[$tokenName . ':name']['name'] = $field['name'] . ':name'; + $this->tokensMetadata[$tokenName . ':name']['audience'] = 'sysadmin'; + $this->tokensMetadata[$tokenName . ':label']['title'] = $fieldLabel; + $this->tokensMetadata[$tokenName . ':name']['title'] = ts('Machine name') . ': ' . $fieldLabel; + $field['audience'] = 'sysadmin'; + if ($field['name'] === 'state_province_id') { + $this->tokensMetadata[$tokenName . ':abbr'] = $this->tokensMetadata[$tokenName . ':label']; + $this->tokensMetadata[$tokenName . ':abbr']['title'] .= ts('Abbreviated'); + } + } + if ($field['data_type'] === 'Boolean') { + $this->tokensMetadata[$tokenName . ':label'] = $field; + $this->tokensMetadata[$tokenName . ':label']['name'] = $field['name'] . ':label'; + $field['audience'] = 'sysadmin'; + } + + $this->tokensMetadata[$tokenName] = $field; + } + } + /** * Get a cache key appropriate to the current usage. * diff --git a/tests/phpunit/CRM/Core/BAO/MessageTemplateTest.php b/tests/phpunit/CRM/Core/BAO/MessageTemplateTest.php index 9e936813f1f9..c95c834832d1 100644 --- a/tests/phpunit/CRM/Core/BAO/MessageTemplateTest.php +++ b/tests/phpunit/CRM/Core/BAO/MessageTemplateTest.php @@ -323,7 +323,7 @@ public function testContactTokens(): void { $this->hookClass->setHook('civicrm_tokens', [$this, 'hookTokens']); $this->createCustomGroupWithFieldsOfAllTypes([]); - $tokenData = $this->getAllContactTokens(); + $tokenData = $this->getOldContactTokens(); $address = $this->setupContactFromTokeData($tokenData); $advertisedTokens = CRM_Core_SelectValues::contactTokens(); $this->assertEquals($this->getAdvertisedTokens(), $advertisedTokens); @@ -458,25 +458,34 @@ public function hookTokens(array &$hookTokens): void { */ public function testContactTokensRenderedByTokenProcessor(): void { $this->createCustomGroupWithFieldsOfAllTypes([]); - $tokenData = $this->getAllContactTokens(); + $tokenData = $this->getOldContactTokens(); $address = $this->setupContactFromTokeData($tokenData); $tokenString = ''; foreach (array_keys($tokenData) as $key) { $tokenString .= "{$key}:{contact.{$key}}\n"; } + $newStyleTokenString = ''; + foreach (array_keys($this->getAdvertisedTokens()) as $key) { + $newStyleTokenString .= substr($key, 9, -1) . ' |' . $key . "\n"; + } $tokenProcessor = new TokenProcessor(Civi::dispatcher(), []); $tokenProcessor->addMessage('html', $tokenString, 'text/html'); + $tokenProcessor->addMessage('new', $newStyleTokenString, 'text/html'); + $tokenProcessor->addRow(['contactId' => $tokenData['contact_id']]); $tokenProcessor->evaluate(); $rendered = ''; foreach ($tokenProcessor->getRows() as $row) { $rendered = (string) $row->render('html'); + $newStyleRendered = $row->render('new'); } $expected = $this->getExpectedContactOutput($address['id'], $tokenData, $rendered); // @todo - this works better in token processor than in CRM_Core_Token. // once synced we can fix $this->getExpectedContactOutput to return the right thing. $expected = str_replace("preferred_communication_method:\n", "preferred_communication_method:Phone\n", $expected); $this->assertEquals($expected, $rendered); + $this->assertEquals($this->getExpectedContactOutputNewStyle($address['id'], $tokenData, $newStyleRendered), $newStyleRendered); + } /** @@ -512,13 +521,13 @@ protected function getDomainTokenData(): array { */ public function getAdvertisedTokens(): array { return [ - '{contact.contact_type}' => 'Contact Type', - '{contact.do_not_email}' => 'Do Not Email', - '{contact.do_not_phone}' => 'Do Not Phone', - '{contact.do_not_mail}' => 'Do Not Mail', - '{contact.do_not_sms}' => 'Do Not Sms', - '{contact.do_not_trade}' => 'Do Not Trade', - '{contact.is_opt_out}' => 'No Bulk Emails (User Opt Out)', + '{contact.contact_type:label}' => 'Contact Type', + '{contact.do_not_email:label}' => 'Do Not Email', + '{contact.do_not_phone:label}' => 'Do Not Phone', + '{contact.do_not_mail:label}' => 'Do Not Mail', + '{contact.do_not_sms:label}' => 'Do Not Sms', + '{contact.do_not_trade:label}' => 'Do Not Trade', + '{contact.is_opt_out:label}' => 'No Bulk Emails (User Opt Out)', '{contact.external_identifier}' => 'External Identifier', '{contact.sort_name}' => 'Sort Name', '{contact.display_name}' => 'Display Name', @@ -537,18 +546,18 @@ public function getAdvertisedTokens(): array { '{contact.formal_title}' => 'Formal Title', '{contact.communication_style_id:label}' => 'Communication Style', '{contact.job_title}' => 'Job Title', - '{contact.gender_id:label}' => 'Gender ID', + '{contact.gender_id:label}' => 'Gender', '{contact.birth_date}' => 'Birth Date', - '{contact.current_employer_id}' => 'Current Employer ID', + '{contact.employer_id}' => 'Current Employer ID', '{contact.is_deleted:label}' => 'Contact is in Trash', '{contact.created_date}' => 'Created Date', '{contact.modified_date}' => 'Modified Date', '{contact.addressee_display}' => 'Addressee', '{contact.email_greeting_display}' => 'Email Greeting', '{contact.postal_greeting_display}' => 'Postal Greeting', - '{contact.current_employer}' => 'Current Employer', - '{contact.location_type}' => 'Location Type', - '{contact.address_id}' => 'Address ID', + '{contact.employer_id.display_name}' => 'Current Employer', + '{contact.location_type_id:label}' => 'Address Location Type', + '{contact.primary_address.id}' => 'Address ID', '{contact.street_address}' => 'Street Address', '{contact.street_number}' => 'Street Number', '{contact.street_number_suffix}' => 'Street Number Suffix', @@ -562,21 +571,19 @@ public function getAdvertisedTokens(): array { '{contact.postal_code}' => 'Postal Code', '{contact.geo_code_1}' => 'Latitude', '{contact.geo_code_2}' => 'Longitude', - '{contact.manual_geo_code}' => 'Is Manually Geocoded', - '{contact.address_name}' => 'Address Name', + '{contact.primary_address.name}' => 'Address Name', '{contact.master_id}' => 'Master Address ID', - '{contact.county}' => 'County', - '{contact.state_province}' => 'State', - '{contact.country}' => 'Country', + '{contact.county_id:label}' => 'County', + '{contact.state_province_id:abbr}' => 'State/Province', + '{contact.country_id:label}' => 'Country', '{contact.phone}' => 'Phone', '{contact.phone_ext}' => 'Phone Extension', - '{contact.phone_type_id}' => 'Phone Type ID', - '{contact.phone_type}' => 'Phone Type', + '{contact.phone_type_id:label}' => 'Phone Type', '{contact.email}' => 'Email', - '{contact.on_hold}' => 'On Hold', + '{contact.on_hold:label}' => 'On Hold', '{contact.signature_text}' => 'Signature Text', '{contact.signature_html}' => 'Signature Html', - '{contact.im_provider}' => 'IM Provider', + '{contact.provider_id:label}' => 'IM Provider', '{contact.im}' => 'IM Screen Name', '{contact.openid}' => 'OpenID', '{contact.world_region}' => 'World Region', @@ -595,7 +602,7 @@ public function getAdvertisedTokens(): array { '{contact.custom_12}' => 'Yes No :: Custom Group', '{contact.custom_3}' => 'Test Date :: Custom Group', '{contact.checksum}' => 'Checksum', - '{contact.id}' => 'Internal Contact ID', + '{contact.id}' => 'Contact ID', '{important_stuff.favourite_emoticon}' => 'Best coolest emoticon', ]; } @@ -616,7 +623,7 @@ public function getAdvertisedTokens(): array { * @throws \CRM_Core_Exception * @throws \CiviCRM_API3_Exception */ - public function getAllContactTokens(): array { + public function getOldContactTokens(): array { return [ 'contact_type' => 'Individual', 'do_not_email' => 1, @@ -668,7 +675,6 @@ public function getAllContactTokens(): array { 'postal_code' => '90210', 'geo_code_1' => '48.858093', 'geo_code_2' => '2.294694', - 'manual_geo_code' => TRUE, 'address_name' => 'The white house', 'master_id' => $this->callAPISuccess('Address', 'create', [ 'contact_id' => $this->individualCreate(), @@ -761,7 +767,7 @@ protected function getExpectedContactOutput($id, array $tokenData, string $actua $contact = Contact::get(FALSE)->addWhere('id', '=', $tokenData['contact_id'])->setSelect(['modified_date', 'employer_id'])->execute()->first(); $expected = 'contact_type:Individual do_not_email:1 -do_not_phone: +do_not_phone:0 do_not_mail:1 do_not_sms:1 do_not_trade:1 @@ -787,8 +793,8 @@ protected function getExpectedContactOutput($id, array $tokenData, string $actua gender:Female birth_date:December 31st, 1998 current_employer_id:' . $contact['employer_id'] . ' -contact_is_deleted: -created_date:January 1st, 2020 12:00 AM +contact_is_deleted:0 +created_date:January 1st, 2020 modified_date:' . CRM_Utils_Date::customFormat($contact['modified_date']) . ' addressee:Mr. Robert Frank Smith II email_greeting:Dear Robert @@ -809,7 +815,6 @@ protected function getExpectedContactOutput($id, array $tokenData, string $actua postal_code:90210 geo_code_1:48.858093 geo_code_2:2.294694 -manual_geo_code:1 address_name:The white house master_id:' . $tokenData['master_id'] . ' county: @@ -820,7 +825,7 @@ protected function getExpectedContactOutput($id, array $tokenData, string $actua phone_type_id:2 phone_type:Mobile email:anthony_anderson@civicrm.org -on_hold: +on_hold:0 signature_text:Yours sincerely signature_html:
Yours
im_provider:1 @@ -843,6 +848,106 @@ protected function getExpectedContactOutput($id, array $tokenData, string $actua custom_13:Purple checksum:cs=' . $checksum . ' contact_id:' . $tokenData['contact_id'] . ' +'; + return $expected; + } + + /** + * Get the expected rendered string. + * + * @param int $id + * @param array $tokenData + * @param string $actualOutput + * + * @return string + * @throws \API_Exception + */ + protected function getExpectedContactOutputNewStyle($id, array $tokenData, string $actualOutput): string { + $checksum = substr($actualOutput, (strpos($actualOutput, 'cs=') + 3), 47); + $contact = Contact::get(FALSE)->addWhere('id', '=', $tokenData['contact_id'])->setSelect(['modified_date', 'employer_id'])->execute()->first(); + $expected = 'contact_type:label |Individual +do_not_email:label |Yes +do_not_phone:label |No +do_not_mail:label |Yes +do_not_sms:label |Yes +do_not_trade:label |Yes +is_opt_out:label |Yes +external_identifier |blah +sort_name |Smith, Robert +display_name |Mr. Robert Smith II +nick_name |Bob +image_URL |https://example.com +preferred_communication_method:label |Phone +preferred_language:label |French (Canada) +preferred_mail_format:label |Both +hash |xyz +source |Contact Source +first_name |Robert +middle_name |Frank +last_name |Smith +prefix_id:label |Mr. +suffix_id:label |II +formal_title |Dogsbody +communication_style_id:label |Formal +job_title |Busy person +gender_id:label |Female +birth_date |December 31st, 1998 +employer_id |' . $contact['employer_id'] . ' +is_deleted:label | +created_date |January 1st, 2020 +modified_date |' . CRM_Utils_Date::customFormat($contact['modified_date']) . ' +addressee_display |Mr. Robert Frank Smith II +email_greeting_display |Dear Robert +postal_greeting_display |Dear Robert +employer_id.display_name |Unit Test Organization +location_type_id:label |Home +primary_address.id |' . $id . ' +street_address |Street Address +street_number |123 +street_number_suffix |S +street_name |Main St +street_unit |45B +supplemental_address_1 |Round the corner +supplemental_address_2 |Up the road +supplemental_address_3 |By the big tree +city |New York +postal_code_suffix |4578 +postal_code |90210 +geo_code_1 |48.858093 +geo_code_2 |2.294694 +primary_address.name |The white house +master_id |' . $tokenData['master_id'] . ' +county_id:label | +state_province_id:abbr |TX +country_id:label |United States +phone |123-456 +phone_ext |77 +phone_type_id:label |Mobile +email |anthony_anderson@civicrm.org +on_hold:label |No +signature_text |Yours sincerely +signature_html |Yours
+provider_id:label |Yahoo +im |IM Screen Name +openid |OpenID +world_region |America South, Central, North and Caribbean +url |http://civicrm.org +custom_9 |Mr. Spider Man II +custom_7 |New Zealand +custom_8 |France, Canada +custom_4 |999 +custom_1 |Bobsled +custom_6 | +custom_2 |Red +custom_13 |Purple +custom_10 |Queensland +custom_11 |Victoria, New South Wales +custom_5 |http://civicrm.org +custom_12 |Yes +custom_3 |01/20/2021 12:00AM +checksum |cs=' . $checksum . ' +id |' . $tokenData['contact_id'] . ' +t_stuff.favourite_emoticon | '; return $expected; }