diff --git a/CRM/Contact/Tokens.php b/CRM/Contact/Tokens.php index b630d0995f62..7134b01aa852 100644 --- a/CRM/Contact/Tokens.php +++ b/CRM/Contact/Tokens.php @@ -57,12 +57,18 @@ 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, - ]); + $relatedTokens = array_flip($this->getTokenMappingsForRelatedEntities()); + foreach ($this->getTokenMetadata() as $tokenName => $field) { + if ($field['audience'] === 'user') { + $e->register([ + 'entity' => $this->entity, + // Preserve legacy token names. It generally feels like + // it would be good to switch to the more specific token names + // but other code paths are still in use which can't handle them. + 'field' => $relatedTokens[$tokenName] ?? $tokenName, + 'label' => $field['title'], + ]); + } } foreach ($this->getLegacyHookTokens() as $legacyHookToken) { $e->register([ @@ -90,7 +96,7 @@ public function checkActive(TokenProcessor $processor): bool { /** * @return string */ - public function getEntityIDField(): string { + protected function getEntityIDField(): string { return 'contactId'; } @@ -104,7 +110,7 @@ public function getEntityIDField(): string { * * @return array */ - public function getLegacyHookTokens(): array { + protected function getLegacyHookTokens(): array { $tokens = []; $hookTokens = []; \CRM_Utils_Hook::tokens($hookTokens); @@ -133,106 +139,12 @@ public function getLegacyHookTokens(): array { return $tokens; } - /** - * @return array - * @throws \CRM_Core_Exception - */ - 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; - } - - /** - * Get all tokens advertised as contact tokens. - * - * @return string[] - */ - public function getContactTokens(): 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', - ]; - } - /** * Get all tokens advertised as contact tokens. * * @return string[] */ - public function getExposedFields(): array { + protected function getExposedFields(): array { return [ 'contact_type', 'do_not_email', @@ -375,7 +287,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'])) { @@ -399,28 +310,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 { - $value = $this->getFieldValue($row, $token); - if (is_array($value)) { - $value = implode(',', $value); - } - $row->format('text/html') - ->tokens('contact', $token, $value); + parent::evaluateToken($row, $this->entity, $token, $row->context['contact']); } } } @@ -455,41 +349,6 @@ protected function getFieldValue(TokenRow $row, string $field) { return ''; } - /** - * Is the given field a boolean field. - * - * @param string $fieldName - * - * @return bool - */ - 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; - } - if (empty($this->getFieldMetadata()[$fieldName])) { - return FALSE; - } - 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; - } - return in_array($this->getFieldMetadata()[$fieldName]['data_type'], ['Timestamp', 'Date'], TRUE); - } - /** * Get the metadata for the available fields. * @@ -516,10 +375,17 @@ protected function getTokenMetadata(): array { foreach ($metadata as $field) { $this->addFieldToTokenMetadata($field, $exposedFields, 'primary_' . $entity); } - // Manually add in the abbreviated state province as that maps to - // what has traditionally been delivered. - $this->tokensMetadata['primary_address.state_province_id:abbr'] = $this->tokensMetadata['primary_address.state_province_id:label']; } + // Manually add in the abbreviated state province as that maps to + // what has traditionally been delivered. + $this->tokensMetadata['primary_address.state_province_id:abbr'] = $this->tokensMetadata['primary_address.state_province_id:label']; + $this->tokensMetadata['primary_address.state_province_id:abbr']['name'] = 'state_province_id:abbr'; + $this->tokensMetadata['primary_address.state_province_id:abbr']['audience'] = 'user'; + // Hide the label for now because we are not sure if there are paths + // where legacy token resolution is in play where this could not be resolved. + $this->tokensMetadata['primary_address.state_province_id:label']['audience'] = 'sysadmin'; + // Hide this really obscure one. Just cos it annoys me. + $this->tokensMetadata['primary_address.manual_geo_code:label']['audience'] = 'sysadmin'; Civi::cache('metadata')->set($this->getCacheKey(), $this->tokensMetadata); return $this->tokensMetadata; } @@ -686,6 +552,7 @@ protected function getDeprecatedTokens(): array { return [ 'individual_prefix' => 'prefix_id:label', 'individual_suffix' => 'suffix_id:label', + 'contact_type' => 'contact_type:label', 'gender' => 'gender_id:label', 'communication_style' => 'communication_style_id:label', 'preferred_communication_method' => 'preferred_communication_method:label', @@ -694,7 +561,7 @@ protected function getDeprecatedTokens(): array { 'addressee' => 'addressee_display', 'contact_id' => 'id', 'contact_source' => 'source', - 'contact_is_deleted' => 'is_deleted:label', + 'contact_is_deleted' => 'is_deleted', 'current_employer_id' => 'employer_id', ]; } @@ -716,7 +583,7 @@ protected function getDeprecatedTokens(): array { */ protected function getTokenMappingsForRelatedEntities(): array { return [ - 'on_hold' => 'primary_email.on_hold:label', + 'on_hold' => 'primary_email.on_hold', 'on_hold:label' => 'primary_email.on_hold:label', 'phone_type_id' => 'primary_phone.phone_type_id', 'phone_type_id:label' => 'primary_phone.phone_type_id:label', @@ -743,12 +610,10 @@ protected function getTokenMappingsForRelatedEntities(): array { 'master_id' => 'primary_address.master_id', 'county' => 'primary_address.county_id:label', 'county_id' => 'primary_address.county_id', - 'county_id:label' => 'primary_address.county_id:label', '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', - 'country_id:label' => 'primary_address.country_id:label', 'world_region' => 'primary_address.country_id.region_id:name', 'phone_type' => 'primary_phone.phone_type_id:label', 'phone' => 'primary_phone.phone', diff --git a/CRM/Core/EntityTokens.php b/CRM/Core/EntityTokens.php index 1aee6c9ffc3a..c1cb2fcbc414 100644 --- a/CRM/Core/EntityTokens.php +++ b/CRM/Core/EntityTokens.php @@ -177,19 +177,15 @@ 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. * + * @todo this function should look up tokenMetadata that + * is already loaded. + * * @return string[] */ - public function getReturnFields(): array { + protected function getReturnFields(): array { return array_keys($this->getBasicTokens()); } @@ -200,7 +196,7 @@ public function getReturnFields(): array { * * @return bool */ - public function isBooleanField(string $fieldName): bool { + protected function isBooleanField(string $fieldName): bool { return $this->getMetadataForField($fieldName)['data_type'] === 'Boolean'; } @@ -211,7 +207,7 @@ public function isBooleanField(string $fieldName): bool { * * @return bool */ - public function isDateField(string $fieldName): bool { + protected function isDateField(string $fieldName): bool { return in_array($this->getMetadataForField($fieldName)['data_type'], ['Timestamp', 'Date'], TRUE); } @@ -222,7 +218,7 @@ public function isDateField(string $fieldName): bool { * * @return bool */ - public function isPseudoField(string $fieldName): bool { + protected function isPseudoField(string $fieldName): bool { return strpos($fieldName, ':') !== FALSE; } @@ -233,7 +229,7 @@ public function isPseudoField(string $fieldName): bool { * * @return bool */ - public function isCustomField(string $fieldName) : bool { + protected function isCustomField(string $fieldName) : bool { return (bool) \CRM_Core_BAO_CustomField::getKeyID($fieldName); } @@ -244,7 +240,7 @@ public function isCustomField(string $fieldName) : bool { * * @return bool */ - public function isMoneyField(string $fieldName): bool { + protected function isMoneyField(string $fieldName): bool { return $this->getMetadataForField($fieldName)['data_type'] === 'Money'; } @@ -286,11 +282,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; } @@ -365,18 +366,12 @@ protected function getHiddenTokens(): array { } /** - * Get tokens supporting the syntax we are migrating to. - * - * In general these are tokens that were not previously supported - * so we can add them in the preferred way or that we have - * undertaken some, as yet to be written, db update. - * - * See https://lab.civicrm.org/dev/core/-/issues/2650 + * @todo remove this function & use the metadata that is loaded. * * @return string[] * @throws \API_Exception */ - public function getBasicTokens(): array { + protected function getBasicTokens(): array { $return = []; foreach ($this->getExposedFields() as $fieldName) { // Custom fields are still added v3 style - we want to keep v4 naming 'unpoluted' @@ -428,7 +423,7 @@ protected function getEntityName(): string { return CRM_Core_DAO_AllCoreTables::convertEntityNameToLower($this->getApiEntityName()); } - public function getEntityIDField(): string { + protected function getEntityIDField(): string { return $this->getEntityName() . 'Id'; } @@ -448,7 +443,7 @@ public function prefetch(TokenValueEvent $e): ?array { return $result; } - public function getCurrencyFieldName() { + protected function getCurrencyFieldName() { return []; } @@ -458,7 +453,7 @@ public function getCurrencyFieldName() { * * @return string */ - public function getCurrency($row): string { + protected function getCurrency($row): string { if (!empty($this->getCurrencyFieldName())) { return $this->getFieldValue($row, $this->getCurrencyFieldName()[0]); } @@ -495,7 +490,7 @@ public function getPrefetchFields(TokenValueEvent $e): array { * * @return array */ - public function getDependencies(): array { + protected function getDependencies(): array { return []; } @@ -605,7 +600,10 @@ public function getActiveTokens(TokenValueEvent $e) { * @param array $exposedFields * @param string $prefix */ - protected function addFieldToTokenMetadata(array $field, array $exposedFields, $prefix = ''): void { + 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 diff --git a/tests/phpunit/CRM/Core/BAO/MessageTemplateTest.php b/tests/phpunit/CRM/Core/BAO/MessageTemplateTest.php index 321a2b8cb3b0..207fde7e679f 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); @@ -457,24 +457,31 @@ 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); - // the right thing to use is the (advertised) label now. - $expected = str_replace("preferred_communication_method:Phone\n", "preferred_communication_method:1\n", $expected); $this->assertEquals($expected, $rendered); + $this->assertEquals($this->getExpectedContactOutputNewStyle($address['id'], $tokenData, $newStyleRendered), $newStyleRendered); + } /** @@ -510,13 +517,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', @@ -535,9 +542,9 @@ 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', @@ -545,7 +552,7 @@ public function getAdvertisedTokens(): array { '{contact.email_greeting_display}' => 'Email Greeting', '{contact.postal_greeting_display}' => 'Postal Greeting', '{contact.current_employer}' => 'Current Employer', - '{contact.location_type}' => 'Location Type', + '{contact.location_type_id:label}' => 'Address Location Type', '{contact.address_id}' => 'Address ID', '{contact.street_address}' => 'Street Address', '{contact.street_number}' => 'Street Number', @@ -560,21 +567,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.master_id}' => 'Master Address ID', '{contact.county}' => 'County', - '{contact.state_province}' => 'State', + '{contact.state_province}' => 'State/Province', '{contact.country}' => 'Country', '{contact.phone}' => 'Phone', '{contact.phone_ext}' => 'Phone Extension', - '{contact.phone_type_id}' => 'Phone Type ID', '{contact.phone_type}' => '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', @@ -593,7 +598,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', ]; } @@ -614,7 +619,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, @@ -786,7 +791,7 @@ protected function getExpectedContactOutput($id, array $tokenData, string $actua birth_date:December 31st, 1998 current_employer_id:' . $contact['employer_id'] . ' contact_is_deleted: -created_date:January 1st, 2020 12:00 AM +created_date:January 1st, 2020 modified_date:' . CRM_Utils_Date::customFormat($contact['modified_date']) . ' addressee:Mr. Robert Frank Smith II email_greeting:Dear Robert @@ -818,7 +823,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 @@ -841,6 +846,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 |No +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 +current_employer |Unit Test Organization +location_type_id:label |Home +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 +address_name |The white house +master_id |' . $tokenData['master_id'] . ' +county | +state_province |TX +country |United States +phone |123-456 +phone_ext |77 +phone_type |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; }