From 9c7326b67c5640799012850f75b4b8aca7e7801a Mon Sep 17 00:00:00 2001 From: Eileen McNaughton Date: Fri, 6 Aug 2021 11:08:19 +1200 Subject: [PATCH 01/11] Reconcile remaining fields between scheduled reminders and legacy tokens This adds the last fields that were in legacy tokens but not scheduled reminders and now the same code is deriving the list for each. I wound up UI testing rather than unit testing that custom fields are still advertised without this line as the test uses rollback & adding custom fields would break that. --- CRM/Campaign/BAO/Campaign.php | 16 +++------- CRM/Contribute/Tokens.php | 9 +++++- CRM/Core/EntityTokens.php | 28 +++++++++++++++++ CRM/Core/SelectValues.php | 9 +----- .../Contribute/ActionMapping/ByTypeTest.php | 31 ++++++++++++++++++- 5 files changed, 72 insertions(+), 21 deletions(-) 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/Tokens.php b/CRM/Contribute/Tokens.php index e0aa7b2054ee..ca1b952cc7fb 100644 --- a/CRM/Contribute/Tokens.php +++ b/CRM/Contribute/Tokens.php @@ -56,7 +56,7 @@ protected function getApiEntityName(): string { * @return array */ protected function getExposedFields(): array { - return [ + $fields = [ 'contribution_page_id', 'source', 'id', @@ -75,7 +75,14 @@ protected function getExposedFields(): array { 'contribution_status_id', 'financial_type_id', 'payment_instrument_id', + 'cancel_reason', + 'amount_level', + 'check_number', ]; + if (CRM_Campaign_BAO_Campaign::isCampaignEnable()) { + $fields[] = 'campaign_id'; + } + return $fields; } /** diff --git a/CRM/Core/EntityTokens.php b/CRM/Core/EntityTokens.php index 542ff711ac75..24a594cf050e 100644 --- a/CRM/Core/EntityTokens.php +++ b/CRM/Core/EntityTokens.php @@ -10,6 +10,7 @@ +--------------------------------------------------------------------+ */ +use Civi\Api4\Campaign; use Civi\Token\AbstractTokenSubscriber; use Civi\Token\TokenRow; use Civi\ActionSchedule\Event\MailingQueryEvent; @@ -60,6 +61,15 @@ public function evaluateToken(TokenRow $row, $entity, $field, $prefetch = NULL) */ protected $fieldMetadata = []; + /** + * Loaded campaigns. + * + * As campaigns are not a true pseudoconstant we stash them here as we load them. + * + * @var array + */ + protected $campaigns; + /** * Get the entity name for api v4 calls. * @@ -214,6 +224,14 @@ public function isAddPseudoTokens($fieldName): bool { // from the metadata as yet. return FALSE; } + if ($fieldName === 'campaign_id') { + // Ah campaign_id - let me count the ways you drive me crazy. + // campaign_id is the pseudo-constant that isn't. Unnecessarily loading + // all campaigns can be a huge performance drag. + // Hence it is not defined in the metadata as a pseudoconstant. + // but we still want it to be usable like one. We brute force it... + return TRUE; + } return (bool) $this->getFieldMetadata()[$fieldName]['options']; } @@ -227,9 +245,19 @@ public function isAddPseudoTokens($fieldName): bool { * @return string * Eg. 'Completed' in the example above. * + * @throws \API_Exception * @internal function will likely be protected soon. */ public function getPseudoValue(string $realField, string $pseudoKey, $fieldValue): string { + if ($realField === 'campaign_id') { + if (!isset($this->campaigns[$fieldValue])) { + $campaign = Campaign::get(FALSE)->addWhere('id', '=', (int) $fieldValue) + ->addSelect('name', 'title')->execute()->first(); + $this->campaigns[$fieldValue]['name'] = (string) $campaign['name']; + $this->campaigns[$fieldValue]['label'] = (string) $campaign['title']; + } + return $this->campaigns[$fieldValue][$pseudoKey]; + } if ($pseudoKey === 'name') { $fieldValue = (string) CRM_Core_PseudoConstant::getName($this->getBAOName(), $realField, $fieldValue); } 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/tests/phpunit/CRM/Contribute/ActionMapping/ByTypeTest.php b/tests/phpunit/CRM/Contribute/ActionMapping/ByTypeTest.php index aa0dca703066..d10f687bd719 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,10 @@ 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} + 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 +313,9 @@ public function testTokenRendering(): void { 'total_amount = € 100.00', 'net_amount = € 95.00', 'fee_amount = € 5.00', + 'campaign_id = 1', + 'campaign name = big_campaign', + 'campaign label = Campaign', ]; $this->mut->checkMailLog($expected); @@ -337,6 +348,9 @@ public function testTokenRendering(): void { 'total_amount = € 100.00', 'net_amount = € 95.00', 'fee_amount = € 5.00', + '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 +368,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]); } From 0725c3f4f18249f01d0bd55ce26998768a77dc4d Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Mon, 9 Aug 2021 20:49:30 -0700 Subject: [PATCH 02/11] (REF) ActionSchedule - Convert setCommunicationLanguage(...) to setLocale(pickLocale(...)) --- CRM/Core/BAO/ActionSchedule.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/CRM/Core/BAO/ActionSchedule.php b/CRM/Core/BAO/ActionSchedule.php index e804f3bc7657..abcd747f4448 100644 --- a/CRM/Core/BAO/ActionSchedule.php +++ b/CRM/Core/BAO/ActionSchedule.php @@ -271,7 +271,7 @@ public static function sendMailings($mappingID, $now) { // 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); + CRM_Core_I18n::singleton()->setLocale(CRM_Core_BAO_ActionSchedule::pickLocale($actionSchedule->communication_language, $preferred_language)); } $errors = []; @@ -401,10 +401,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 +426,7 @@ public static function setCommunicationLanguage($communication_language, $prefer } // change the language - $i18n = CRM_Core_I18n::singleton(); - $i18n->setLocale($language); + return $language; } /** From ced4ac6bc6075db04db0fd46535ace05da80b626 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Mon, 9 Aug 2021 20:55:44 -0700 Subject: [PATCH 03/11] (REF) TokenProcessor::render() - If there is a `locale`, then use it --- Civi/Token/TokenProcessor.php | 12 +++++++++ .../phpunit/Civi/Token/TokenProcessorTest.php | 27 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/Civi/Token/TokenProcessor.php b/Civi/Token/TokenProcessor.php index e8ee1b8f73f5..5bc97d6a5a00 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. @@ -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/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__, From 4344436adaafbec9d6366ea0cf6d44b4cc238571 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Mon, 9 Aug 2021 21:13:57 -0700 Subject: [PATCH 04/11] ActionSchedule - Convert from global `setLocale()` to `$context['locale']` Before: Runs `setLocale()` and then executes the entire pipeline for `TokenProcessor` After: Leaves the global locale alone. Instead, rely on `TokenProcessor` to switch locale as it visits each recipient. --- CRM/Core/BAO/ActionSchedule.php | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/CRM/Core/BAO/ActionSchedule.php b/CRM/Core/BAO/ActionSchedule.php index abcd747f4448..00ac176a983e 100644 --- a/CRM/Core/BAO/ActionSchedule.php +++ b/CRM/Core/BAO/ActionSchedule.php @@ -268,18 +268,19 @@ public static function sendMailings($mappingID, $now) { $multilingual = CRM_Core_I18n::isMultilingual(); while ($dao->fetch()) { - // switch language if necessary - if ($multilingual) { - $preferred_language = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $dao->contactID, 'preferred_language'); - CRM_Core_I18n::singleton()->setLocale(CRM_Core_BAO_ActionSchedule::pickLocale($actionSchedule->communication_language, $preferred_language)); - } - $errors = []; try { $tokenProcessor = self::createTokenProcessor($actionSchedule, $mapping); - $tokenProcessor->addRow() + $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'); + $row->context('locale', CRM_Core_BAO_ActionSchedule::pickLocale($actionSchedule->communication_language, $preferred_language)); + } + 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)); From c359783cc29de811d27d3df0b933309e33e2e970 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Mon, 9 Aug 2021 21:19:09 -0700 Subject: [PATCH 05/11] (REF) ActionSchedule - Remove unused try/catch This block purports to catch any exceptions of type `TokenException`. However, if you grep the source tree, you will find that it is never thrown. --- CRM/Core/BAO/ActionSchedule.php | 47 +++++++++++++++------------------ 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/CRM/Core/BAO/ActionSchedule.php b/CRM/Core/BAO/ActionSchedule.php index 00ac176a983e..e5c882db5b63 100644 --- a/CRM/Core/BAO/ActionSchedule.php +++ b/CRM/Core/BAO/ActionSchedule.php @@ -269,35 +269,30 @@ public static function sendMailings($mappingID, $now) { $multilingual = CRM_Core_I18n::isMultilingual(); while ($dao->fetch()) { $errors = []; - try { - $tokenProcessor = self::createTokenProcessor($actionSchedule, $mapping); - $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'); - $row->context('locale', CRM_Core_BAO_ActionSchedule::pickLocale($actionSchedule->communication_language, $preferred_language)); + $tokenProcessor = self::createTokenProcessor($actionSchedule, $mapping); + $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'); + $row->context('locale', CRM_Core_BAO_ActionSchedule::pickLocale($actionSchedule->communication_language, $preferred_language)); + } + + 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)); } - 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); - } + 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); } - } - catch (\Civi\Token\TokenException $e) { - $errors['token_exception'] = $e->getMessage(); } // update action log record From ef07f29fdb6f31d079940787c340e0237d4ce3ad Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Mon, 9 Aug 2021 21:27:31 -0700 Subject: [PATCH 06/11] ActionSchedule - Allow TokenProcessor prefetching to work Overview: `ActionSchedule::sendMailings()` fetches a batch of pending reminders (per some specific schedule/rule). This improves support for prefetching related data. Before: For each item in the batch, it makes a new `TokenProcessor`. This means that the `TokenProcessor` only sees one pending reminder -- and it cannot meaningfully fetch batched data. After: It creates one `TokenProcessor` and adds rows for each pending reminder. This means that `TokenProcessor` can fetch batched data. --- CRM/Core/BAO/ActionSchedule.php | 32 ++++++++++++++++---------------- Civi/Token/TokenProcessor.php | 2 +- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/CRM/Core/BAO/ActionSchedule.php b/CRM/Core/BAO/ActionSchedule.php index e5c882db5b63..ed8feb886095 100644 --- a/CRM/Core/BAO/ActionSchedule.php +++ b/CRM/Core/BAO/ActionSchedule.php @@ -267,9 +267,8 @@ public static function sendMailings($mappingID, $now) { ); $multilingual = CRM_Core_I18n::isMultilingual(); + $tokenProcessor = self::createTokenProcessor($actionSchedule, $mapping); while ($dao->fetch()) { - $errors = []; - $tokenProcessor = self::createTokenProcessor($actionSchedule, $mapping); $row = $tokenProcessor->addRow() ->context('contactId', $dao->contactID) ->context('actionSearchResult', (object) $dao->toArray()); @@ -279,22 +278,24 @@ public static function sendMailings($mappingID, $now) { $preferred_language = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $dao->contactID, 'preferred_language'); $row->context('locale', CRM_Core_BAO_ActionSchedule::pickLocale($actionSchedule->communication_language, $preferred_language)); } + } - 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); - } + $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, @@ -304,7 +305,6 @@ public static function sendMailings($mappingID, $now) { ]; CRM_Core_BAO_ActionLog::create($logParams); } - } } diff --git a/Civi/Token/TokenProcessor.php b/Civi/Token/TokenProcessor.php index 5bc97d6a5a00..f0336ca4963b 100644 --- a/Civi/Token/TokenProcessor.php +++ b/Civi/Token/TokenProcessor.php @@ -256,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 ?: [])); } /** From 192bb4f18c2f0c43499b068f5810eb9e1b60f61a Mon Sep 17 00:00:00 2001 From: Eileen McNaughton Date: Mon, 9 Aug 2021 22:59:13 -0700 Subject: [PATCH 07/11] CRM_Utils_Token - Accept multiple dots in token names --- CRM/Utils/Token.php | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/CRM/Utils/Token.php b/CRM/Utils/Token.php index 6e9ea5b403e9..9c068c744b27 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 '/(? Date: Mon, 9 Aug 2021 22:59:54 -0700 Subject: [PATCH 08/11] (REF) ActionSchedule - Pass through SQL fields named "tokenContext_*" This will help us to consolidate the prefetching logic in `CRM_*_Tokens`. Currently, `CRM_*_Token::alterActionScheduleQuery()` updates the query to select all fields for the entity. This is regardless of how many fields there are, whether they are needed, etc. This is also prone to naming conflicts. With this patch, `CRM_*_Token::alterActionScheduleQuery()` only needs to select one field (`tokenContext_fooId`). This will then propagate to the `TokenProcessor` which can do its own optimized data-loading. --- CRM/Core/BAO/ActionSchedule.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CRM/Core/BAO/ActionSchedule.php b/CRM/Core/BAO/ActionSchedule.php index ed8feb886095..1f2801829fda 100644 --- a/CRM/Core/BAO/ActionSchedule.php +++ b/CRM/Core/BAO/ActionSchedule.php @@ -278,6 +278,15 @@ public static function sendMailings($mappingID, $now) { $preferred_language = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $dao->contactID, 'preferred_language'); $row->context('locale', CRM_Core_BAO_ActionSchedule::pickLocale($actionSchedule->communication_language, $preferred_language)); } + + 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); + } + } } $tokenProcessor->evaluate(); From 2d9464665b79d8182c624cf7e492b758bc3c147d Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Tue, 10 Aug 2021 01:23:20 -0700 Subject: [PATCH 09/11] Contribution Tokens - Add legacy token {contribution.campaign} to test --- tests/phpunit/CRM/Contribute/ActionMapping/ByTypeTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/phpunit/CRM/Contribute/ActionMapping/ByTypeTest.php b/tests/phpunit/CRM/Contribute/ActionMapping/ByTypeTest.php index d10f687bd719..2acda157957c 100644 --- a/tests/phpunit/CRM/Contribute/ActionMapping/ByTypeTest.php +++ b/tests/phpunit/CRM/Contribute/ActionMapping/ByTypeTest.php @@ -287,6 +287,7 @@ public function testTokenRendering(): void { total_amount = {contribution.total_amount} net_amount = {contribution.net_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}'; @@ -313,6 +314,7 @@ 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', @@ -348,6 +350,7 @@ 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', From 6b548faf18c5c04621aaab5ace1af26c511ac4ef Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Tue, 10 Aug 2021 01:24:52 -0700 Subject: [PATCH 10/11] (REF) TokenRow - Add copyToken() helper --- Civi/Token/TokenRow.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) 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 * From 41d741c089de1471809b6265acc8ec03418046ea Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Tue, 10 Aug 2021 01:57:29 -0700 Subject: [PATCH 11/11] `EntityTokens` and `Contribution` Tokens - Multiple updates: * All data-loading uses `prefetch()` and an ID field (eg `contributionId`). * All special APIv4 values (pseudoconstants/joins/etc) are loaded by `civicrm_api4()`. No need to duplicate this stuff. * When using `TokenProcessor`, columns will only be prefetched if they are actually needed. * `alterActionScheduleQuery()` no longer prefetches data. Instead, it populates `tokenContext_contributionId` which feeds into `prefetch()`. * Properly declare `abstract` methods. * Subclasses (i.e. `CRM_Contribute_Tokens`) should primarily use these methods: - `getApiTokens()` - List of visible/exported fields. - `getAliasTokens()` - List of funny aliases for API fields. This adds support for the legacy token `{contribution.campaign}`, and it also * The special `currency` rules cannot work in other entities. Move them to `CRM_Contribution_Tokens` specifically. --- CRM/Contribute/BAO/Contribution.php | 6 +- CRM/Contribute/Tokens.php | 67 ++++--- CRM/Core/EntityTokens.php | 287 +++++++++++++--------------- CRM/Utils/Token.php | 1 + 4 files changed, 175 insertions(+), 186 deletions(-) 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 ca1b952cc7fb..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,15 +40,16 @@ 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 { + protected function getApiTokens(): array { $fields = [ 'contribution_page_id', 'source', @@ -73,35 +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/EntityTokens.php b/CRM/Core/EntityTokens.php index 24a594cf050e..f279ef73a73f 100644 --- a/CRM/Core/EntityTokens.php +++ b/CRM/Core/EntityTokens.php @@ -10,14 +10,20 @@ +--------------------------------------------------------------------+ */ -use Civi\Api4\Campaign; use Civi\Token\AbstractTokenSubscriber; use Civi\Token\TokenRow; use Civi\ActionSchedule\Event\MailingQueryEvent; 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. * @@ -26,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])); } } @@ -62,60 +66,70 @@ public function evaluateToken(TokenRow $row, $entity, $field, $prefetch = NULL) protected $fieldMetadata = []; /** - * Loaded campaigns. - * - * As campaigns are not a true pseudoconstant we stash them here as we load them. + * Get the entity name, as it appears in the token. * - * @var array + * @return string + * Ex: 'contribution' for token '{contribution.total_amount}'. */ - protected $campaigns; + abstract protected function getEntityName(): string; /** * Get the entity name for api v4 calls. * * @return string + * Ex: 'Contribution' for token '{contribution.total_amount}'. */ - protected function getApiEntityName(): string { - return ''; - } + abstract protected function getApiEntityName(): string; /** - * Get the entity alias to use within queries. + * Get a list of tokens which are loaded via APIv4. * - * The default has a double underscore which should prevent any - * ambiguity with an existing table name. - * - * @return string + * @return string[] + * Ex: ['foo', 'bar_id', 'bar_id:name', 'bar_id:label'] */ - protected function getEntityAlias(): string { - return $this->getApiEntityName() . '__'; - } + abstract protected function getApiTokens(): array; /** - * Get the name of the table this token class can extend. + * Get a list of aliased tokens. * - * 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 array + * Ex: ['my_alias_field' => 'original_field'] */ - public function getExtendableTableName(): string { - return CRM_Core_DAO_AllCoreTables::getTableForEntityName($this->getApiEntityName()); + public function getAliasTokens(): array { + return []; } - /** - * Get the relevant bao name. - */ - public function getBAOName(): string { - return CRM_Core_DAO_AllCoreTables::getFullName($this->getApiEntityName()); + 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); } /** @@ -124,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; } /** @@ -156,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 @@ -186,96 +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; + return $r; } /** - * 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; - } - if ($fieldName === 'campaign_id') { - // Ah campaign_id - let me count the ways you drive me crazy. - // campaign_id is the pseudo-constant that isn't. Unnecessarily loading - // all campaigns can be a huge performance drag. - // Hence it is not defined in the metadata as a pseudoconstant. - // but we still want it to be usable like one. We brute force it... - return TRUE; - } - return (bool) $this->getFieldMetadata()[$fieldName]['options']; - } - - /** - * 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 - * - * @return string - * Eg. 'Completed' in the example above. + * Get the values for all exported pseudo-fields. * + * @param int $id + * @return array * @throws \API_Exception - * @internal function will likely be protected soon. + * @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 ($realField === 'campaign_id') { - if (!isset($this->campaigns[$fieldValue])) { - $campaign = Campaign::get(FALSE)->addWhere('id', '=', (int) $fieldValue) - ->addSelect('name', 'title')->execute()->first(); - $this->campaigns[$fieldValue]['name'] = (string) $campaign['name']; - $this->campaigns[$fieldValue]['label'] = (string) $campaign['title']; + 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 $this->campaigns[$fieldValue][$pseudoKey]; - } - 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); - } - 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; } /** @@ -286,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. * @@ -294,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/Utils/Token.php b/CRM/Utils/Token.php index 9c068c744b27..811cc387fc4a 100644 --- a/CRM/Utils/Token.php +++ b/CRM/Utils/Token.php @@ -1567,6 +1567,7 @@ protected static function _buildContributionTokens() { $tokens = array_merge(CRM_Contribute_BAO_Contribution::exportableFields('All'), ['campaign' => [], 'financial_type' => [], 'payment_instrument' => []], self::getCustomFieldTokens('Contribution'), + $processor->getAliasTokens(), $processor->getPseudoTokens() ); foreach ($tokens as $token) {