Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions DTO/LoopToken.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php

declare(strict_types=1);

namespace MauticPlugin\CustomObjectsBundle\DTO;

/**
* Object that represents parsed token like this one:.
*
* {custom-object=product:sku | where=segment-filter |order=latest|limit=1 | default=Nothing to see here | format=or-list}
*/
final class LoopToken
{
private string $token;

private int $limit = 1;

private string $where = '';

private string $order = 'latest';

private string $customObjectAlias = '';

/**
* @var array<string, array<string, string>>
*/
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<string, array<string, string>>
*/
public function getLoopContentTokens(): array
{
return $this->loopContentTokens;
}

/**
* @param array<string, string> $contentTokenParams
*/
public function addLoopContentToken(string $loopContentToken, array $contentTokenParams): void
{
$this->loopContentTokens[$loopContentToken] = $contentTokenParams;
}

/**
* @param array<string, array<string, string>> $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;
}
}
99 changes: 97 additions & 2 deletions EventListener/TokenSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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())
);
}
}
}
Expand All @@ -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()) {
Expand All @@ -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;
}

Expand All @@ -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 {
Expand All @@ -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.
*
Expand Down
98 changes: 96 additions & 2 deletions Helper/TokenParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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;
}
Expand Down Expand Up @@ -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}";
Expand Down Expand Up @@ -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
Expand Down
Loading