From fa75f0642b94e1cedc5dae8fa99d4e92a71a8df2 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Thu, 11 Nov 2021 16:41:30 -0800 Subject: [PATCH] dev/core#2947 - Auto-enable `{event.*}` if `participantId` is present --- Civi/Core/Container.php | 4 + Civi/Token/ImpliedContextSubscriber.php | 114 ++++++++++++++++++ Civi/Token/TokenProcessor.php | 11 ++ .../Token/ImpliedContextSubscriberTest.php | 27 +++++ 4 files changed, 156 insertions(+) create mode 100644 Civi/Token/ImpliedContextSubscriber.php create mode 100644 tests/phpunit/Civi/Token/ImpliedContextSubscriberTest.php diff --git a/Civi/Core/Container.php b/Civi/Core/Container.php index fd7c2773d6..841f318ba5 100644 --- a/Civi/Core/Container.php +++ b/Civi/Core/Container.php @@ -338,6 +338,10 @@ class Container { [] ))->addTag('kernel.event_subscriber')->setPublic(TRUE); } + $container->setDefinition('civi_token_impliedcontext', new Definition( + 'Civi\Token\ImpliedContextSubscriber', + [] + ))->addTag('kernel.event_subscriber')->setPublic(TRUE); $container->setDefinition('crm_participant_tokens', new Definition( 'CRM_Event_ParticipantTokens', [] diff --git a/Civi/Token/ImpliedContextSubscriber.php b/Civi/Token/ImpliedContextSubscriber.php new file mode 100644 index 0000000000..3f3c5c016a --- /dev/null +++ b/Civi/Token/ImpliedContextSubscriber.php @@ -0,0 +1,114 @@ + ['onRegisterTokens', 1000], + 'civi.token.eval' => ['onEvaluateTokens', 1000], + ]; + } + + /** + * When listing tokens, ensure that implied data is visible. + * + * Ex: If `$context['participantId']` is part of the schema, then + * `$context['eventId']` will also be part of the schema. + * + * This fires early during the `civi.token.list` process to ensure that + * other listeners see the updated schema. + * + * @param \Civi\Token\Event\TokenRegisterEvent $e + */ + public function onRegisterTokens(TokenRegisterEvent $e): void { + $tokenProc = $e->getTokenProcessor(); + foreach ($this->findRelevantMappings($tokenProc) as $mapping) { + $tokenProc->addSchema($mapping['destEntityId']); + } + } + + /** + * When evaluating tokens, ensure that implied data is loaded. + * + * Ex: If `$context['participantId']` is supplied, then lookup the + * corresponding `$context['eventId']`. + * + * This fires early during the `civi.token.list` process to ensure that + * other listeners see the autoloaded values. + * + * @param \Civi\Token\Event\TokenValueEvent $e + */ + public function onEvaluateTokens(TokenValueEvent $e): void { + $tokenProc = $e->getTokenProcessor(); + foreach ($this->findRelevantMappings($tokenProc) as $mapping) { + $getSrcId = function($row) use ($mapping) { + return $row->context[$mapping['srcEntityId']] ?? $row->context[$mapping['srcEntityRec']]['id'] ?? NULL; + }; + + $ids = []; + foreach ($tokenProc->getRows() as $row) { + $ids[] = $getSrcId($row); + } + $ids = \array_diff(\array_unique($ids), [NULL]); + if (empty($ids)) { + continue; + } + + [$srcTable, $fkColumn] = explode('.', $mapping['fk']); + $fks = \CRM_Utils_SQL_Select::from($srcTable) + ->where('id in (#ids)', ['ids' => $ids]) + ->select(['id', $fkColumn]) + ->execute() + ->fetchMap('id', $fkColumn); + + $tokenProc->addSchema($mapping['destEntityId']); + foreach ($tokenProc->getRows() as $row) { + $srcId = $getSrcId($row); + if ($srcId && empty($row->context[$mapping['destEntityId']])) { + $row->context($mapping['destEntityId'], $fks[$srcId]); + } + } + } + } + + /** + * Are there any context-fields for which we should do lookups? + * + * Ex: If the `$tokenProcessor` has the `participantId`s, then we would want + * to know any rules that involve `participantId`. But we don't need to know + * rules that involve `contributionId`. + * + * @param \Civi\Token\TokenProcessor $tokenProcessor + */ + private function findRelevantMappings(TokenProcessor $tokenProcessor): iterable { + $schema = $tokenProcessor->context['schema']; + yield from []; + foreach ($this->getMappings() as $mapping) { + if (in_array($mapping['srcEntityRec'], $schema) || in_array($mapping['srcEntityId'], $schema)) { + yield $mapping; + } + } + } + + private function getMappings(): iterable { + yield [ + 'srcEntityId' => 'participantId', + 'srcEntityRec' => 'participant', + 'fk' => 'civicrm_participant.event_id', + 'destEntityId' => 'eventId', + ]; + } + +} diff --git a/Civi/Token/TokenProcessor.php b/Civi/Token/TokenProcessor.php index 0451b2ae53..93c1e01c1a 100644 --- a/Civi/Token/TokenProcessor.php +++ b/Civi/Token/TokenProcessor.php @@ -127,6 +127,17 @@ class TokenProcessor { $this->context = $context; } + /** + * Add new elements to the field schema. + * + * @param string|string[] $fieldNames + * @return TokenProcessor + */ + public function addSchema($fieldNames) { + $this->context['schema'] = array_unique(array_merge($this->context['schema'], (array) $fieldNames)); + return $this; + } + /** * Register a string for which we'll need to merge in tokens. * diff --git a/tests/phpunit/Civi/Token/ImpliedContextSubscriberTest.php b/tests/phpunit/Civi/Token/ImpliedContextSubscriberTest.php new file mode 100644 index 0000000000..9a5b0ef9d0 --- /dev/null +++ b/tests/phpunit/Civi/Token/ImpliedContextSubscriberTest.php @@ -0,0 +1,27 @@ +participantCreate(); + + $messages = \CRM_Core_TokenSmarty::render( + ['text' => 'Go to {event.title}!'], + ['participantId' => $participantId] + ); + $this->assertEquals('Go to Annual CiviCRM meet!', $messages['text']); + } + + public function testParticipant_ExplicitEvent() { + $participantId = $this->participantCreate(); + $otherEventId = $this->eventCreate(['title' => 'Alternate Event'])['id']; + + $messages = \CRM_Core_TokenSmarty::render( + ['text' => 'You may also like {event.title}!'], + ['participantId' => $participantId, 'eventId' => $otherEventId] + ); + $this->assertEquals('You may also like Alternate Event!', $messages['text']); + } + +} -- 2.25.1