diff --git a/CRM/Campaign/BAO/Campaign.php b/CRM/Campaign/BAO/Campaign.php index 9c1a0447c109..a72c7d3571ce 100644 --- a/CRM/Campaign/BAO/Campaign.php +++ b/CRM/Campaign/BAO/Campaign.php @@ -301,20 +301,14 @@ public static function getPermissionedCampaigns( /** * Is CiviCampaign enabled. + * * @return bool */ - public static function isCampaignEnable() { - static $isEnable = NULL; - - if (!isset($isEnable)) { - $isEnable = FALSE; - $config = CRM_Core_Config::singleton(); - if (in_array('CiviCampaign', $config->enableComponents)) { - $isEnable = TRUE; - } + public static function isCampaignEnable(): bool { + if (!isset(Civi::$statics[__CLASS__]['is_enabled'])) { + Civi::$statics[__CLASS__]['is_enabled'] = in_array('CiviCampaign', CRM_Core_Config::singleton()->enableComponents, TRUE); } - - return $isEnable; + return Civi::$statics[__CLASS__]['is_enabled']; } /** diff --git a/CRM/Contribute/BAO/Contribution.php b/CRM/Contribute/BAO/Contribution.php index 3093761de99a..b16ca325af28 100644 --- a/CRM/Contribute/BAO/Contribution.php +++ b/CRM/Contribute/BAO/Contribution.php @@ -5167,11 +5167,7 @@ public static function getContributionTokenValues($id, $messageToken) { } if (!empty($messageToken['contribution'])) { $processor = new CRM_Contribute_Tokens(); - $pseudoFields = array_keys($processor->getPseudoTokens()); - foreach ($pseudoFields as $pseudoField) { - $split = explode(':', $pseudoField); - $result['values'][$id][$pseudoField] = $processor->getPseudoValue($split[0], $split[1], $result['values'][$id][$split[0]] ?? ''); - } + $result['values'][$id] = array_merge($result['values'][$id], $processor->getPseudoValues($id)); } return $result; } diff --git a/CRM/Contribute/Tokens.php b/CRM/Contribute/Tokens.php index e0aa7b2054ee..b7fd5018cd50 100644 --- a/CRM/Contribute/Tokens.php +++ b/CRM/Contribute/Tokens.php @@ -27,13 +27,6 @@ protected function getEntityName(): string { return 'contribution'; } - /** - * @return string - */ - protected function getEntityAlias(): string { - return 'contrib_'; - } - /** * Get the entity name for api v4 calls. * @@ -47,16 +40,17 @@ protected function getApiEntityName(): string { } /** - * Get a list of tokens for the entity for which access is permitted to. + * Get a list of tokens which are loaded via APIv4. * * This list is historical and we need to question whether we * should filter out any fields (other than those fields, like api_key * on the contact entity) with permissions defined. * * @return array + * Ex: ['foo', 'bar_id', 'bar_id:name', 'bar_id:label'] */ - protected function getExposedFields(): array { - return [ + protected function getApiTokens(): array { + $fields = [ 'contribution_page_id', 'source', 'id', @@ -73,28 +67,58 @@ protected function getExposedFields(): array { 'thankyou_date', 'tax_amount', 'contribution_status_id', + 'contribution_status_id:name', + 'contribution_status_id:label', 'financial_type_id', + 'financial_type_id:name', + 'financial_type_id:label', 'payment_instrument_id', + 'payment_instrument_id:name', + 'payment_instrument_id:label', + 'cancel_reason', + 'amount_level', + 'check_number', ]; + if (CRM_Campaign_BAO_Campaign::isCampaignEnable()) { + $fields[] = 'campaign_id'; + $fields[] = 'campaign_id.name'; + $fields[] = 'campaign_id.title'; + } + + return $fields; } - /** - * 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 - * - * @return string[] - */ - public function getBasicTokens(): array { - $return = []; - foreach ($this->getExposedFields() as $fieldName) { - $return[$fieldName] = $this->getFieldMetadata()[$fieldName]['title']; + public function getAliasTokens(): array { + $aliases = []; + if (CRM_Campaign_BAO_Campaign::isCampaignEnable()) { + // Unit-tests are written to use these funny tokens - but they're not valid in APIv4. + $aliases['campaign'] = 'campaign_id.name'; + $aliases['campaign_id:name'] = 'campaign_id.name'; + $aliases['campaign_id:label'] = 'campaign_id.title'; + } + return $aliases; + } + + public function getPrefetchFields(\Civi\Token\Event\TokenValueEvent $e): array { + $result = parent::getPrefetchFields($e); + + // Always prefetch 'civicrm_contribution.currency' in case we need to format other fields (fee_amount, total_amount, etc). + $result[] = 'currency'; + + return array_unique($result); + } + + public function evaluateToken(\Civi\Token\TokenRow $row, $entity, $field, $prefetch = NULL) { + $values = $prefetch[$row->context[$this->getEntityIDField()]]; + + // Any monetary fields in a `Contribution` (`fee_amount`, `total_amount`, etc) should be formatted in matching `currency`. + // This formatting rule would be nonsensical in any other entity. + if ($this->isApiFieldType($field, 'Money')) { + return $row->format('text/plain')->tokens($entity, $field, + \CRM_Utils_Money::format($values[$field], $values['currency'])); } - return $return; + + return parent::evaluateToken($row, $entity, $field, $prefetch); } } diff --git a/CRM/Core/BAO/ActionSchedule.php b/CRM/Core/BAO/ActionSchedule.php index e804f3bc7657..1f2801829fda 100644 --- a/CRM/Core/BAO/ActionSchedule.php +++ b/CRM/Core/BAO/ActionSchedule.php @@ -267,38 +267,44 @@ public static function sendMailings($mappingID, $now) { ); $multilingual = CRM_Core_I18n::isMultilingual(); + $tokenProcessor = self::createTokenProcessor($actionSchedule, $mapping); while ($dao->fetch()) { + $row = $tokenProcessor->addRow() + ->context('contactId', $dao->contactID) + ->context('actionSearchResult', (object) $dao->toArray()); + // switch language if necessary if ($multilingual) { $preferred_language = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $dao->contactID, 'preferred_language'); - CRM_Core_BAO_ActionSchedule::setCommunicationLanguage($actionSchedule->communication_language, $preferred_language); + $row->context('locale', CRM_Core_BAO_ActionSchedule::pickLocale($actionSchedule->communication_language, $preferred_language)); } - $errors = []; - try { - $tokenProcessor = self::createTokenProcessor($actionSchedule, $mapping); - $tokenProcessor->addRow() - ->context('contactId', $dao->contactID) - ->context('actionSearchResult', (object) $dao->toArray()); - foreach ($tokenProcessor->evaluate()->getRows() as $tokenRow) { - if ($actionSchedule->mode === 'SMS' || $actionSchedule->mode === 'User_Preference') { - CRM_Utils_Array::extend($errors, self::sendReminderSms($tokenRow, $actionSchedule, $dao->contactID)); - } - - if ($actionSchedule->mode === 'Email' || $actionSchedule->mode === 'User_Preference') { - CRM_Utils_Array::extend($errors, self::sendReminderEmail($tokenRow, $actionSchedule, $dao->contactID)); - } - // insert activity log record if needed - if ($actionSchedule->record_activity && empty($errors)) { - $caseID = empty($dao->case_id) ? NULL : $dao->case_id; - CRM_Core_BAO_ActionSchedule::createMailingActivity($tokenRow, $mapping, $dao->contactID, $dao->entityID, $caseID); + foreach ($dao->toArray() as $key => $value) { + if (preg_match('/^tokenContext_(.*)/', $key, $m)) { + if (!in_array($m[1], $tokenProcessor->context['schema'])) { + $tokenProcessor->context['schema'][] = $m[1]; } + $row->context($m[1], $value); } } - catch (\Civi\Token\TokenException $e) { - $errors['token_exception'] = $e->getMessage(); + } + + $tokenProcessor->evaluate(); + foreach ($tokenProcessor->getRows() as $tokenRow) { + $dao = $tokenRow->context['actionSearchResult']; + $errors = []; + if ($actionSchedule->mode === 'SMS' || $actionSchedule->mode === 'User_Preference') { + CRM_Utils_Array::extend($errors, self::sendReminderSms($tokenRow, $actionSchedule, $dao->contactID)); } + if ($actionSchedule->mode === 'Email' || $actionSchedule->mode === 'User_Preference') { + CRM_Utils_Array::extend($errors, self::sendReminderEmail($tokenRow, $actionSchedule, $dao->contactID)); + } + // insert activity log record if needed + if ($actionSchedule->record_activity && empty($errors)) { + $caseID = empty($dao->case_id) ? NULL : $dao->case_id; + CRM_Core_BAO_ActionSchedule::createMailingActivity($tokenRow, $mapping, $dao->contactID, $dao->entityID, $caseID); + } // update action log record $logParams = [ 'id' => $dao->reminderID, @@ -308,7 +314,6 @@ public static function sendMailings($mappingID, $now) { ]; CRM_Core_BAO_ActionLog::create($logParams); } - } } @@ -401,10 +406,11 @@ public static function getRecipientListing($mappingID, $recipientType) { } /** - * @param $communication_language - * @param $preferred_language + * @param string|null $communication_language + * @param string|null $preferred_language + * @return string */ - public static function setCommunicationLanguage($communication_language, $preferred_language) { + public static function pickLocale($communication_language, $preferred_language) { $currentLocale = CRM_Core_I18n::getLocale(); $language = $currentLocale; @@ -425,8 +431,7 @@ public static function setCommunicationLanguage($communication_language, $prefer } // change the language - $i18n = CRM_Core_I18n::singleton(); - $i18n->setLocale($language); + return $language; } /** diff --git a/CRM/Core/EntityTokens.php b/CRM/Core/EntityTokens.php index 542ff711ac75..f279ef73a73f 100644 --- a/CRM/Core/EntityTokens.php +++ b/CRM/Core/EntityTokens.php @@ -16,7 +16,14 @@ use Civi\Token\TokenProcessor; /** - * Class CRM_Core_EntityTokens + * Generic base-class which loads tokens via APIv4. + * + * To write a subclass: + * + * - (MUST) Implement getEntityName() and getApiEntityName() + * - (MUST) Implement getApiTokens() + * - (MAY) Implement getAliasTokens() + * - (MAY) Override evaluateToken() * * Parent class for generic entity token functionality. * @@ -25,31 +32,29 @@ * AbstractTokenSubscriber in future. It is being used to clarify * functionality but should NOT be used from outside of core tested code. */ -class CRM_Core_EntityTokens extends AbstractTokenSubscriber { +abstract class CRM_Core_EntityTokens extends AbstractTokenSubscriber { /** * @inheritDoc * @throws \CRM_Core_Exception */ public function evaluateToken(TokenRow $row, $entity, $field, $prefetch = NULL) { - $fieldValue = $this->getFieldValue($row, $field); - - if ($this->isPseudoField($field)) { - $split = explode(':', $field); - return $row->tokens($entity, $field, $this->getPseudoValue($split[0], $split[1], $this->getFieldValue($row, $split[0]))); + $aliases = $this->getAliasTokens(); + if (isset($aliases[$field])) { + $this->evaluateToken($row, $entity, $aliases[$field], $prefetch); + return $row->copyToken("$entity." . $aliases[$field], "$entity.$field"); } - if ($this->isMoneyField($field)) { - return $row->format('text/plain')->tokens($entity, $field, - \CRM_Utils_Money::format($fieldValue, $this->getFieldValue($row, 'currency'))); - } - if ($this->isDateField($field)) { - return $row->format('text/plain')->tokens($entity, $field, \CRM_Utils_Date::customFormat($fieldValue)); + + $values = $prefetch[$row->context[$this->getEntityIDField()]]; + + if ($this->isApiFieldType($field, 'Timestamp')) { + return $row->format('text/plain')->tokens($entity, $field, \CRM_Utils_Date::customFormat($values[$field])); } if ($this->isCustomField($field)) { - $row->customToken($entity, \CRM_Core_BAO_CustomField::getKeyID($field), $this->getFieldValue($row, 'id')); + $row->customToken($entity, \CRM_Core_BAO_CustomField::getKeyID($field), $row->context[$this->getEntityIDField()]); } else { - $row->format('text/plain')->tokens($entity, $field, (string) $fieldValue); + $row->format('text/plain')->tokens($entity, $field, (string) ($values[$field])); } } @@ -61,51 +66,70 @@ public function evaluateToken(TokenRow $row, $entity, $field, $prefetch = NULL) protected $fieldMetadata = []; /** - * Get the entity name for api v4 calls. + * Get the entity name, as it appears in the token. * * @return string + * Ex: 'contribution' for token '{contribution.total_amount}'. */ - protected function getApiEntityName(): string { - return ''; - } + abstract protected function getEntityName(): string; /** - * Get the entity alias to use within queries. - * - * The default has a double underscore which should prevent any - * ambiguity with an existing table name. + * Get the entity name for api v4 calls. * * @return string + * Ex: 'Contribution' for token '{contribution.total_amount}'. */ - protected function getEntityAlias(): string { - return $this->getApiEntityName() . '__'; - } + abstract protected function getApiEntityName(): string; /** - * Get the name of the table this token class can extend. + * Get a list of tokens which are loaded via APIv4. * - * The default is based on the entity but some token classes, - * specifically the event class, latch on to other tables - ie - * the participant table. + * @return string[] + * Ex: ['foo', 'bar_id', 'bar_id:name', 'bar_id:label'] */ - public function getExtendableTableName(): string { - return CRM_Core_DAO_AllCoreTables::getTableForEntityName($this->getApiEntityName()); - } + abstract protected function getApiTokens(): array; /** - * Get the relevant bao name. + * Get a list of aliased tokens. + * + * @return array + * Ex: ['my_alias_field' => 'original_field'] */ - public function getBAOName(): string { - return CRM_Core_DAO_AllCoreTables::getFullName($this->getApiEntityName()); + public function getAliasTokens(): array { + return []; + } + + public function prefetch(\Civi\Token\Event\TokenValueEvent $e): ?array { + $entityIDs = $e->getTokenProcessor()->getContextValues($this->getEntityIDField()); + if (empty($entityIDs)) { + return []; + } + $select = $this->getPrefetchFields($e); + $result = (array) civicrm_api4($this->getApiEntityName(), 'get', [ + 'checkPermissions' => FALSE, + // Note custom fields are not yet added - I need to + // re-do the unit tests to support custom fields first. + 'select' => $select, + 'where' => [['id', 'IN', $entityIDs]], + ], 'id'); + return $result; } /** - * Get an array of fields to be requested. + * Determine which fields should be prefetched. * - * @return string[] + * @param \Civi\Token\Event\TokenValueEvent $e + * @return array + * List of API fields to prefetch. */ - public function getReturnFields(): array { - return array_keys($this->getBasicTokens()); + public function getPrefetchFields(\Civi\Token\Event\TokenValueEvent $e): array { + $activeTokens = $this->getActiveTokens($e); + foreach ($this->getAliasTokens() as $aliasToken => $aliasTarget) { + if (in_array($aliasToken, $activeTokens)) { + $activeTokens[] = $aliasTarget; + } + } + return array_intersect($this->getApiTokens(), $activeTokens); } /** @@ -114,29 +138,37 @@ public function getReturnFields(): array { * @return array|string[] */ public function getAllTokens(): array { - return array_merge($this->getBasicTokens(), $this->getPseudoTokens(), CRM_Utils_Token::getCustomFieldTokens('Contribution')); - } - - /** - * Is the given field a date field. - * - * @param string $fieldName - * - * @return bool - */ - public function isDateField(string $fieldName): bool { - return $this->getFieldMetadata()[$fieldName]['data_type'] === 'Timestamp'; + $apiTokens = $this->getApiTokens(); + $apiFields = $this->getFieldMetadata(); + $return = []; + $suffixes = ['name' => ts('(Name)'), 'label' => ts('(Label)'), 'title' => ts('(Title)')]; + foreach ($apiTokens as $apiToken) { + if (preg_match('/^(.*)([:\.])(name|label|title)$/', $apiToken, $m)) { + $fieldName = $m[1]; + $suffix = $suffixes[$m[3]]; + $return[$apiToken] = $apiFields[$fieldName]['input_attrs']['label'] . ' ' . $suffix; + } + else { + $fieldName = $apiToken; + $return[$fieldName] = $apiFields[$fieldName]['title'] ?? $fieldName; + } + } + foreach ($this->getAliasTokens() as $aliasToken => $aliasTarget) { + $return[$aliasToken] = ts('%1 (Alias)', [1 => $return[$aliasTarget]]); + } + return array_merge($return, CRM_Utils_Token::getCustomFieldTokens($this->getApiEntityName())); } /** - * Is the given field a pseudo field. + * Does the field have the given type? * * @param string $fieldName - * + * @param string $expectType + * Ex: 'Number', 'Money', 'Timestamp' * @return bool */ - public function isPseudoField(string $fieldName): bool { - return strpos($fieldName, ':') !== FALSE; + protected function isApiFieldType(string $fieldName, string $expectType): bool { + return ($this->getFieldMetadata($fieldName)['data_type'] ?? NULL) === $expectType; } /** @@ -146,27 +178,17 @@ 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); } - /** - * Is the given field a date field. - * - * @param string $fieldName - * - * @return bool - */ - public function isMoneyField(string $fieldName): bool { - return $this->getFieldMetadata()[$fieldName]['data_type'] === 'Money'; - } - /** * Get the metadata for the available fields. * - * @return array + * @param string|null $field + * @return array|null */ - protected function getFieldMetadata(): array { + protected function getFieldMetadata(?string $field = NULL): ?array { if (empty($this->fieldMetadata)) { try { // Tests fail without checkPermissions = FALSE @@ -176,78 +198,56 @@ protected function getFieldMetadata(): array { $this->fieldMetadata = []; } } - return $this->fieldMetadata; + if ($field) { + return $this->fieldMetadata[$field] ?? NULL; + } + else { + return $this->fieldMetadata; + } } /** * Get pseudoTokens - it tokens that reflect the name or label of a pseudoconstant. * - * @internal - this function will likely be made protected soon. - * + * @internal - this function is a bridge for legacy CRM_Utils_Token callers. It should be removed. + * @deprecated * @return array */ public function getPseudoTokens(): array { - $return = []; - foreach (array_keys($this->getBasicTokens()) as $fieldName) { - if ($this->isAddPseudoTokens($fieldName)) { - $return[$fieldName . ':label'] = $this->fieldMetadata[$fieldName]['input_attrs']['label']; - $return[$fieldName . ':name'] = ts('Machine name') . ': ' . $this->fieldMetadata[$fieldName]['input_attrs']['label']; + $labels = $this->getAllTokens(); + // Simpler, but doesn't currently pass: $labels = $this->tokenNames; + $r = []; + foreach ($this->getApiTokens() as $key) { + if (strpos($key, ':') !== FALSE || strpos($key, '.') !== FALSE) { + $r[$key] = $labels[$key]; } } - return $return; - } - - /** - * Is this a field we should add pseudo-tokens to? - * - * Pseudo-tokens allow access to name and label fields - e.g - * - * {contribution.contribution_status_id:name} might resolve to 'Completed' - * - * @param string $fieldName - */ - public function isAddPseudoTokens($fieldName): bool { - if ($fieldName === 'currency') { - // 'currency' is manually added to the skip list as an anomaly. - // name & label aren't that suitable for 'currency' (symbol, which - // possibly maps to 'abbr' would be) and we can't gather that - // from the metadata as yet. - return FALSE; - } - return (bool) $this->getFieldMetadata()[$fieldName]['options']; + return $r; } /** - * Get the value for the relevant pseudo field. - * - * @param string $realField e.g contribution_status_id - * @param string $pseudoKey e.g name - * @param int|string $fieldValue e.g 1 + * Get the values for all exported pseudo-fields. * - * @return string - * Eg. 'Completed' in the example above. - * - * @internal function will likely be protected soon. + * @param int $id + * @return array + * @throws \API_Exception + * @internal - this function is a bridge for legacy CRM_Utils_Token callers. It should be removed. + * @deprecated */ - public function getPseudoValue(string $realField, string $pseudoKey, $fieldValue): string { - if ($pseudoKey === 'name') { - $fieldValue = (string) CRM_Core_PseudoConstant::getName($this->getBAOName(), $realField, $fieldValue); - } - if ($pseudoKey === 'label') { - $fieldValue = (string) CRM_Core_PseudoConstant::getLabel($this->getBAOName(), $realField, $fieldValue); + public function getPseudoValues(int $id): array { + $pseudoFields = array_keys($this->getPseudoTokens()); + $api4 = civicrm_api4('Contribution', 'get', [ + 'checkPermissions' => FALSE, + 'select' => $pseudoFields, + 'where' => [['id', '=', $id]], + ]); + $result = CRM_Utils_Array::subset($api4->single(), $pseudoFields); + foreach ($this->getAliasTokens() as $aliasToken => $aliasTarget) { + if (isset($result[$aliasTarget])) { + $result[$aliasToken] = $result[$aliasTarget]; + } } - return (string) $fieldValue; - } - - /** - * @param \Civi\Token\TokenRow $row - * @param string $field - * @return string|int - */ - protected function getFieldValue(TokenRow $row, string $field) { - $actionSearchResult = $row->context['actionSearchResult']; - $aliasedField = $this->getEntityAlias() . $field; - return $actionSearchResult->{$aliasedField} ?? NULL; + return $result; } /** @@ -258,6 +258,10 @@ public function __construct() { parent::__construct($this->getEntityName(), $tokens); } + public function getEntityIDField() { + return $this->getEntityName() . 'Id'; + } + /** * Check if the token processor is active. * @@ -266,21 +270,20 @@ public function __construct() { * @return bool */ public function checkActive(TokenProcessor $processor) { - return !empty($processor->context['actionMapping']) - && $processor->context['actionMapping']->getEntity() === $this->getExtendableTableName(); + return in_array($this->getEntityIDField(), $processor->context['schema']); } /** * Alter action schedule query. * + * If there is an action-schedule that deals with our entity, then make sure the + * entity ID is passed through in `$tokenRow->context['myEntityid']`. + * * @param \Civi\ActionSchedule\Event\MailingQueryEvent $e */ public function alterActionScheduleQuery(MailingQueryEvent $e): void { - if ($e->mapping->getEntity() !== $this->getExtendableTableName()) { - return; - } - foreach ($this->getReturnFields() as $token) { - $e->query->select('e.' . $token . ' AS ' . $this->getEntityAlias() . $token); + if ($e->mapping->getEntity() === CRM_Core_DAO_AllCoreTables::getTableForEntityName($this->getApiEntityName())) { + $e->query->select('e.id' . ' AS tokenContext_' . $this->getEntityIDField()); } } diff --git a/CRM/Core/SelectValues.php b/CRM/Core/SelectValues.php index 94e6da9cdc01..ca20c9d27189 100644 --- a/CRM/Core/SelectValues.php +++ b/CRM/Core/SelectValues.php @@ -568,14 +568,7 @@ public static function contributionTokens(): array { foreach ($processor->getAllTokens() as $token => $title) { $tokens['{contribution.' . $token . '}'] = $title; } - return array_merge($tokens, [ - '{contribution.cancel_reason}' => ts('Contribution Cancel Reason'), - '{contribution.amount_level}' => ts('Amount Level'), - '{contribution.check_number}' => ts('Check Number'), - '{contribution.campaign}' => ts('Contribution Campaign'), - // @todo - we shouldn't need to include custom fields here - - // remove, with test. - ], CRM_Utils_Token::getCustomFieldTokens('Contribution', TRUE)); + return $tokens; } /** diff --git a/CRM/Utils/Token.php b/CRM/Utils/Token.php index 6e9ea5b403e9..811cc387fc4a 100644 --- a/CRM/Utils/Token.php +++ b/CRM/Utils/Token.php @@ -182,16 +182,16 @@ public static function token_replace($type, $var, $value, &$str, $escapeSmarty = } /** - * Get< the regex for token replacement + * Get the regex for token replacement * * @param string $token_type * A string indicating the the type of token to be used in the expression. * * @return string - * regular expression sutiable for using in preg_replace + * regular expression suitable for using in preg_replace */ - private static function tokenRegex($token_type) { - return '/(? [], 'financial_type' => [], 'payment_instrument' => []], self::getCustomFieldTokens('Contribution'), + $processor->getAliasTokens(), $processor->getPseudoTokens() ); foreach ($tokens as $token) { diff --git a/Civi/Token/TokenProcessor.php b/Civi/Token/TokenProcessor.php index e8ee1b8f73f5..f0336ca4963b 100644 --- a/Civi/Token/TokenProcessor.php +++ b/Civi/Token/TokenProcessor.php @@ -55,6 +55,7 @@ class TokenProcessor { * automatically from contactId.) * - actionSchedule: DAO, the rule which triggered the mailing * [for CRM_Core_BAO_ActionScheduler]. + * - locale: string, the name of a locale (eg 'fr_CA') to use for {ts} strings in the view. * - schema: array, a list of fields that will be provided for each row. * This is automatically populated with any general context * keys, but you may need to add extra keys for token-row data. @@ -255,7 +256,7 @@ public function getRow($key) { * Each row is presented with a fluent, OOP facade. */ public function getRows() { - return new TokenRowIterator($this, new \ArrayIterator($this->rowContexts)); + return new TokenRowIterator($this, new \ArrayIterator($this->rowContexts ?: [])); } /** @@ -350,6 +351,12 @@ public function render($name, $row) { if (!is_object($row)) { $row = $this->getRow($row); } + $i18n = NULL; + if (!empty($row->context['locale'])) { + $i18n = \CRM_Core_I18n::singleton(); + $oldLocale = $GLOBALS['tsLocale'] ?? NULL; + $i18n->setLocale($row->context['locale']); + } $message = $this->getMessage($name); $row->fill($message['format']); @@ -372,6 +379,11 @@ public function render($name, $row) { $event->row = $row; $event->string = strtr($message['string'], $filteredTokens); $this->dispatcher->dispatch('civi.token.render', $event); + + if ($i18n !== NULL) { + $i18n->setLocale($oldLocale); + } + return $event->string; } diff --git a/Civi/Token/TokenRow.php b/Civi/Token/TokenRow.php index fb1fda5c2182..3c63c9d365d7 100644 --- a/Civi/Token/TokenRow.php +++ b/Civi/Token/TokenRow.php @@ -232,6 +232,27 @@ public function dbToken($tokenEntity, $tokenField, $baoName, $baoField, $fieldVa throw new \CRM_Core_Exception("Cannot format token for field '$baoField' in '$baoName'"); } + /** + * Copy a token, including any/all available formats. + * + * @param string $src + * Ex: 'contact.foo_bar' + * @param string $dest + * Ex: 'contact.whiz_bang' + * @return TokenRow + */ + public function copyToken($src, $dest) { + [$srcEntity, $srcField] = explode('.', $src, 2); + [$destEntity, $destField] = explode('.', $dest, 2); + $rv = &$this->tokenProcessor->rowValues; + foreach (array_keys($rv[$this->tokenRow]) as $format) { + if (isset($rv[$this->tokenRow][$format][$srcEntity][$srcField])) { + $rv[$this->tokenRow][$format][$destEntity][$destField] = $rv[$this->tokenRow][$format][$srcEntity][$srcField]; + } + } + return $this; + } + /** * Auto-convert between different formats * diff --git a/tests/phpunit/CRM/Contribute/ActionMapping/ByTypeTest.php b/tests/phpunit/CRM/Contribute/ActionMapping/ByTypeTest.php index aa0dca703066..2acda157957c 100644 --- a/tests/phpunit/CRM/Contribute/ActionMapping/ByTypeTest.php +++ b/tests/phpunit/CRM/Contribute/ActionMapping/ByTypeTest.php @@ -9,6 +9,8 @@ +--------------------------------------------------------------------+ */ +use Civi\Api4\Contribution; + /** * Class CRM_Contribute_ActionMapping_ByTypeTest * @group ActionSchedule @@ -151,6 +153,8 @@ public function createTestCases() { * Create a contribution record for Alice with type "Member Dues". */ public function addAliceDues(): void { + $this->enableCiviCampaign(); + $campaignID = $this->campaignCreate(); $this->ids['Contribution']['alice'] = $this->callAPISuccess('Contribution', 'create', [ 'contact_id' => $this->contacts['alice']['id'], 'receive_date' => date('Ymd', strtotime($this->targetDate)), @@ -164,6 +168,7 @@ public function addAliceDues(): void { // Having a cancel date is a bit artificial here but we can test it.... 'cancel_date' => '2021-08-09', 'contribution_status_id' => 1, + 'campaign_id' => $campaignID, 'soft_credit' => [ '1' => [ 'contact_id' => $this->contacts['carol']['id'], @@ -281,7 +286,11 @@ public function testTokenRendering(): void { non_deductible_amount = {contribution.non_deductible_amount} total_amount = {contribution.total_amount} net_amount = {contribution.net_amount} - fee_amount = {contribution.fee_amount}'; + fee_amount = {contribution.fee_amount} + legacy campaign = {contribution.campaign} + campaign_id = {contribution.campaign_id} + campaign name = {contribution.campaign_id:name} + campaign label = {contribution.campaign_id:label}'; $this->schedule->save(); $this->callAPISuccess('job', 'send_reminder', []); @@ -305,6 +314,10 @@ public function testTokenRendering(): void { 'total_amount = € 100.00', 'net_amount = € 95.00', 'fee_amount = € 5.00', + 'legacy campaign = big_campaign', + 'campaign_id = 1', + 'campaign name = big_campaign', + 'campaign label = Campaign', ]; $this->mut->checkMailLog($expected); @@ -337,6 +350,10 @@ public function testTokenRendering(): void { 'total_amount = € 100.00', 'net_amount = € 95.00', 'fee_amount = € 5.00', + 'legacy campaign = big_campaign', + 'campaign_id = 1', + 'campaign name = big_campaign', + 'campaign label = Campaign', ]; foreach ($expected as $string) { $this->assertStringContainsString($string, $contributionDetails[$this->contacts['alice']['id']]['html']); @@ -354,6 +371,21 @@ public function testTokenRendering(): void { 'contribution_status_id:label', ]; $processor = new CRM_Contribute_Tokens(); + $legacyTokens = []; + $realLegacyTokens = []; + foreach (CRM_Core_SelectValues::contributionTokens() as $token => $label) { + $legacyTokens[substr($token, 14, -1)] = $label; + if (strpos($token, ':') === FALSE) { + $realLegacyTokens[substr($token, 14, -1)] = $label; + } + } + $fields = (array) Contribution::getFields()->addSelect('name', 'title')->execute()->indexBy('name'); + $allFields = []; + foreach ($fields as $field) { + $allFields[$field['name']] = $field['title']; + } + // $this->assertEquals($realLegacyTokens, $allFields); + $this->assertEquals($legacyTokens, $processor->tokenNames); foreach ($tokens as $token) { $this->assertEquals(CRM_Core_SelectValues::contributionTokens()['{contribution.' . $token . '}'], $processor->tokenNames[$token]); } diff --git a/tests/phpunit/Civi/Token/TokenProcessorTest.php b/tests/phpunit/Civi/Token/TokenProcessorTest.php index cd8dba1cf23b..429a44f3e076 100644 --- a/tests/phpunit/Civi/Token/TokenProcessorTest.php +++ b/tests/phpunit/Civi/Token/TokenProcessorTest.php @@ -139,6 +139,33 @@ public function testRowTokens() { } } + public function testRenderLocalizedSmarty() { + $this->dispatcher->addSubscriber(new TokenCompatSubscriber()); + $p = new TokenProcessor($this->dispatcher, [ + 'controller' => __CLASS__, + 'smarty' => TRUE, + ]); + $p->addMessage('text', '{ts}Yes{/ts} {ts}No{/ts}', 'text/plain'); + $p->addRow([]); + $p->addRow(['locale' => 'fr_FR']); + $p->addRow(['locale' => 'es_MX']); + + $expectText = [ + 'Yes No', + 'Oui Non', + 'Sí No', + ]; + + $rowCount = 0; + foreach ($p->evaluate()->getRows() as $key => $row) { + /** @var TokenRow */ + $this->assertTrue($row instanceof TokenRow); + $this->assertEquals($expectText[$key], $row->render('text')); + $rowCount++; + } + $this->assertEquals(3, $rowCount); + } + public function testGetMessageTokens() { $p = new TokenProcessor($this->dispatcher, [ 'controller' => __CLASS__,