diff --git a/DTO/LoopToken.php b/DTO/LoopToken.php new file mode 100644 index 000000000..464ab6563 --- /dev/null +++ b/DTO/LoopToken.php @@ -0,0 +1,114 @@ +> + */ + private array $loopContentTokens = []; + + private string $loopContent = ''; + + public function __construct(string $token) + { + $this->token = $token; + } + + public function getLoopContent(): string + { + return $this->loopContent; + } + + public function setLoopContent(string $loopContent): void + { + $this->loopContent = $loopContent; + } + + /** + * @return array> + */ + public function getLoopContentTokens(): array + { + return $this->loopContentTokens; + } + + /** + * @param array $contentTokenParams + */ + public function addLoopContentToken(string $loopContentToken, array $contentTokenParams): void + { + $this->loopContentTokens[$loopContentToken] = $contentTokenParams; + } + + /** + * @param array> $loopContentTokens + */ + public function setLoopContentTokens(array $loopContentTokens): void + { + $this->loopContentTokens = $loopContentTokens; + } + + public function getOrder(): string + { + return $this->order; + } + + public function setOrder(string $order): void + { + $this->order = $order; + } + + public function getWhere(): string + { + return $this->where; + } + + public function setWhere(string $where): void + { + $this->where = $where; + } + + public function getLimit(): int + { + return $this->limit; + } + + public function setLimit(int $limit): void + { + $this->limit = $limit; + } + + public function getToken(): string + { + return $this->token; + } + + public function getCustomObjectAlias(): string + { + return $this->customObjectAlias; + } + + public function setCustomObjectAlias(string $customObjectAlias): void + { + $this->customObjectAlias = $customObjectAlias; + } +} diff --git a/EventListener/TokenSubscriber.php b/EventListener/TokenSubscriber.php index 61b7cb8b6..80706b02e 100644 --- a/EventListener/TokenSubscriber.php +++ b/EventListener/TokenSubscriber.php @@ -17,6 +17,7 @@ use Mautic\LeadBundle\Segment\OperatorOptions; use MauticPlugin\CustomObjectsBundle\CustomItemEvents; use MauticPlugin\CustomObjectsBundle\CustomObjectEvents; +use MauticPlugin\CustomObjectsBundle\DTO\LoopToken; use MauticPlugin\CustomObjectsBundle\DTO\TableConfig; use MauticPlugin\CustomObjectsBundle\DTO\Token; use MauticPlugin\CustomObjectsBundle\Entity\CustomField; @@ -95,12 +96,28 @@ public function onBuilderBuild(BuilderEvent $event): void $this->tokenParser->buildTokenWithDefaultOptions($customObject->getAlias(), 'name'), $this->tokenParser->buildTokenLabel($customObject->getName(), 'Name') ); + + $event->addToken( + $this->tokenParser->buildTokenCustomObjectLoop($customObject->getAlias()), + $this->tokenParser->buildTokenCustomObjectLoopLabel($customObject->getName()) + ); + + $event->addToken( + $this->tokenParser->buildTokenCustomObjectFieldInLoop($customObject->getAlias(), 'name'), + $this->tokenParser->buildTokenCustomObjectFieldInLoopLabel($customObject->getName(), 'Name') + ); + /** @var CustomField $customField */ foreach ($customObject->getCustomFields() as $customField) { $event->addToken( $this->tokenParser->buildTokenWithDefaultOptions($customObject->getAlias(), $customField->getAlias()), $this->tokenParser->buildTokenLabel($customObject->getName(), $customField->getLabel()) ); + + $event->addToken( + $this->tokenParser->buildTokenCustomObjectFieldInLoop($customObject->getAlias(), $customField->getAlias()), + $this->tokenParser->buildTokenCustomObjectFieldInLoopLabel($customObject->getName(), $customField->getLabel()) + ); } } } @@ -111,6 +128,31 @@ public function decodeTokens(EmailSendEvent $event): void return; } + $this->addDefaultTokens($event); + $this->addCustomObjectLoopTokens($event); + } + + public function addCustomObjectLoopTokens(EmailSendEvent $event): void + { + $tokens = $this->tokenParser->findCustomObjectLoopTokens($event->getContent()); + + if (0 === $tokens->count()) { + return; + } + $tokens->map(function (LoopToken $token) use ($event): void { + try { + $customObject = $this->customObjectModel->fetchEntityByAlias($token->getCustomObjectAlias()); + $tokenContent = $this->getLoopTokenContent($customObject, $token, $event); + } catch (NotFoundException $e) { + $tokenContent = ''; + } + + $event->addToken($token->getToken(), $tokenContent); + }); + } + + public function addDefaultTokens(EmailSendEvent $event): void + { $tokens = $this->tokenParser->findTokens($event->getContent()); if (0 === $tokens->count()) { @@ -121,7 +163,7 @@ public function decodeTokens(EmailSendEvent $event): void try { $customObject = $this->customObjectModel->fetchEntityByAlias($token->getCustomObjectAlias()); $fieldValues = $this->getCustomFieldValues($customObject, $token, $event); - } catch (NotFoundException) { + } catch (NotFoundException $e) { $fieldValues = null; } @@ -140,7 +182,7 @@ public function decodeTokens(EmailSendEvent $event): void $result = $formatEvent->hasBeenFormatted() ? $formatEvent->getFormattedString() : $this->tokenFormatter->format($fieldValues, TokenFormatter::DEFAULT_FORMAT); - } catch (InvalidCustomObjectFormatListException) { + } catch (InvalidCustomObjectFormatListException $e) { $result = $this->tokenFormatter->format($fieldValues, TokenFormatter::DEFAULT_FORMAT); } } else { @@ -152,6 +194,59 @@ public function decodeTokens(EmailSendEvent $event): void }); } + public function getLoopTokenContent(CustomObject $customObject, LoopToken $token, EmailSendEvent $event): string + { + $loopTokenContent = ''; + $orderBy = CustomItem::TABLE_ALIAS.'.id'; + $orderDir = 'DESC'; + + if ('latest' === $token->getOrder()) { + // There is no other ordering option implemented at the moment. + // Use the default order and direction. + } + + $tableConfig = new TableConfig($token->getLimit(), 1, $orderBy, $orderDir); + $tableConfig->addParameter('customObjectId', $customObject->getId()); + $tableConfig->addParameter('filterEntityType', 'contact'); + $tableConfig->addParameter('filterEntityId', (int) $event->getLead()['id']); + $tableConfig->addParameter('token', $token); + $tableConfig->addParameter('email', $event->getEmail()); + $tableConfig->addParameter('source', $event->getSource()); + $customItems = $this->customItemModel->getArrayTableData($tableConfig); + + foreach ($customItems as $customItemData) { + $loopTokenContent .= $token->getLoopContent(); + $customItem = new CustomItem($customObject); + $customItem->populateFromArray($customItemData); + $customItem = $this->customItemModel->populateCustomFields($customItem); + + foreach ($token->getLoopContentTokens() as $loopContentToken => $loopContentTokenParams) { + $field = $loopContentTokenParams['field']; + + if ('name' === $field) { + $fieldValue = $customItemData['name']; + } else { + try { + $customFieldValue = $customItem->findCustomFieldValueForFieldAlias($field); + $fieldValue = $customFieldValue->getValue(); + } catch (NotFoundException) { + $fieldValue = null; + } + } + + if (empty($fieldValue)) { + $fieldValue = $loopContentTokenParams['default']; + } + + $fieldValue = (string) $fieldValue; + + $loopTokenContent = str_replace($loopContentToken, $fieldValue, $loopTokenContent); + } + } + + return $loopTokenContent; + } + /** * Add some where conditions to the query requesting the right custom items for the token replacement. * diff --git a/Helper/TokenParser.php b/Helper/TokenParser.php index f97d67f9c..cb25dbf89 100644 --- a/Helper/TokenParser.php +++ b/Helper/TokenParser.php @@ -5,12 +5,16 @@ namespace MauticPlugin\CustomObjectsBundle\Helper; use Doctrine\Common\Collections\ArrayCollection; +use MauticPlugin\CustomObjectsBundle\DTO\LoopToken; use MauticPlugin\CustomObjectsBundle\DTO\Token; class TokenParser { public const TOKEN = '{custom-object=(.*?)}'; + public const TOKEN_CUSTOM_OBJECT_LOOP = '{custom-object-loop\s+([^}]*)\}([\s\S]*?)\{\/custom-object-loop\}'; + public const TOKEN_CUSTOM_OBJECT_LOOP_VALUE = '{custom-object-loop-value\s+([^}]*)\}'; + public function findTokens(string $content): ArrayCollection { $tokens = new ArrayCollection(); @@ -27,7 +31,7 @@ public function findTokens(string $content): ArrayCollection try { $this->extractAliases($parts[0], $token); - } catch (\LengthException) { + } catch (\LengthException $e) { // Invalid token, pretend like we did not see it. continue; } @@ -69,11 +73,101 @@ public function findTokens(string $content): ArrayCollection return $tokens; } + public function findCustomObjectLoopTokens(string $content): ArrayCollection + { + $tokens = new ArrayCollection(); + + preg_match_all('/'.self::TOKEN_CUSTOM_OBJECT_LOOP.'/', $content, $matches); + + if (empty($matches[1])) { + return $tokens; + } + + foreach ($matches[1] as $key => $loopParams) { + $loopToken = new LoopToken($matches[0][$key]); + $rawParams = $this->getPartsDividedByPipe($loopParams); + foreach ($rawParams as $rawParam) { + $options = $this->trimArrayElements(explode('=', $rawParam)); + $keyword = $options[0]; + $value = $options[1]; + + if ('object' === $keyword) { + $loopToken->setCustomObjectAlias($value); + } + + if ('where' === $keyword) { + $loopToken->setWhere($value); + } + + if ('order' === $keyword) { + $loopToken->setOrder($value); + } + + if ('limit' === $keyword) { + $loopToken->setLimit((int) $value); + } + } + + $loopContent = $matches[2][$key] ?? ''; + $loopToken->setLoopContent($loopContent); + preg_match_all('/'.self::TOKEN_CUSTOM_OBJECT_LOOP_VALUE.'/', $loopContent, $fieldMatches); + + foreach ($fieldMatches[1] as $key => $fieldMatch) { + $contentToken = $fieldMatches[0][$key]; + $rawFieldParams = $this->getPartsDividedByPipe($fieldMatch); + $contentTokenParams = []; + foreach ($rawFieldParams as $rawFieldParam) { + $options = $this->trimArrayElements(explode('=', $rawFieldParam)); + $keyword = $options[0]; + $value = $options[1]; + + if ('object' === $keyword && $loopToken->getCustomObjectAlias() !== $value) { + continue; + } + + if ('field' === $keyword) { + $contentTokenParams['field'] = $value; + } + + if ('default' === $keyword) { + $contentTokenParams['default'] = $value; + } + } + + $loopToken->addLoopContentToken($contentToken, $contentTokenParams); + } + + $tokens->set($loopToken->getToken(), $loopToken); + } + + return $tokens; + } + public function buildTokenWithDefaultOptions(string $customObjectAlias, string $customFieldAlias): string { return "{custom-object={$customObjectAlias}:{$customFieldAlias} | where=segment-filter | order=latest | limit=1 | default= | format=default}"; } + public function buildTokenCustomObjectLoop(string $customObjectAlias): string + { + return "{custom-object-loop object={$customObjectAlias} | where=segment-filter | order=latest | limit=1}\nThe content added here will be repeated for each item in the custom object.\n{/custom-object-loop}"; + } + + public function buildTokenCustomObjectFieldInLoop(string $customObjectAlias, string $customFieldAlias): string + { + return "{custom-object-loop-value object={$customObjectAlias} | field={$customFieldAlias} | default=}"; + } + + public function buildTokenCustomObjectFieldInLoopLabel(string $customObjectLabel, string $customFieldLabel): string + { + return "{$customObjectLabel}: {$customFieldLabel} in Loop"; + } + + public function buildTokenCustomObjectLoopLabel(string $customObjectLabel): string + { + return "{$customObjectLabel}: For Loop"; + } + public function buildTokenLabel(string $customObjectName, string $customFieldLabel): string { return "{$customObjectName}: {$customFieldLabel}"; @@ -109,7 +203,7 @@ private function getPartsDividedByPipe(string $tokenDataRaw): array private function trimArrayElements(array $array): array { return array_map( - function ($part): string { + function ($part) { return trim($part); }, $array