From 04d5f47097978d24347ebc8952362fa30c23513d Mon Sep 17 00:00:00 2001 From: MarketSmart <85239715+MarketSmart@users.noreply.github.com> Date: Tue, 14 Mar 2023 14:00:04 -0400 Subject: [PATCH] 1.2 beta (#31) * Mautic 4 (#28) * feat: :sparkles: mautic v4 port * Update TemplateProcessor.php bring pixel tracking back * Update EmailSubscriber.php modify content and add pixel tracking back * Update README.md Co-authored-by: zerodois * Simple bug fixes * logger, lead tags, sms support, update readme * update readme * Update README.md --------- Co-authored-by: zerodois Co-authored-by: Luis Rodriguez --- Config/config.php | 13 +++- EventListener/EmailSubscriber.php | 83 +++++++++++++++++++++----- EventListener/SmsSubscriber.php | 85 +++++++++++++++++++++++++++ Helper/TemplateProcessor.php | 55 ++++++++++++----- Helper/Twig_Loader_DynamicContent.php | 19 +++--- README.md | 68 ++++++++++++++++++--- 6 files changed, 276 insertions(+), 47 deletions(-) create mode 100644 EventListener/SmsSubscriber.php diff --git a/Config/config.php b/Config/config.php index abfe5f5..ef15497 100644 --- a/Config/config.php +++ b/Config/config.php @@ -3,7 +3,7 @@ return [ 'name' => 'Advanced Templates', 'description' => 'Plugin extends default email template capabilities with TWIG block so you can use advanced scripting techniques like conditions, loops etc', - 'version' => '1.0', + 'version' => '1.2', 'author' => 'Dmitry Berezovsky', 'services' => [ 'events' => [ @@ -11,7 +11,16 @@ 'mautic.plugin.advanced_templates.email.subscriber' => [ 'class' => \MauticPlugin\MauticAdvancedTemplatesBundle\EventListener\EmailSubscriber::class, 'arguments' => [ - 'mautic.plugin.advanced_templates.helper.template_processor' + 'mautic.plugin.advanced_templates.helper.template_processor', + 'mautic.lead.model.lead', + 'monolog.logger.mautic', + ] + ], + 'mautic.plugin.advanced_templates.sms.subscriber' => [ + 'class' => \MauticPlugin\MauticAdvancedTemplatesBundle\EventListener\SmsSubscriber::class, + 'arguments' => [ + 'mautic.plugin.advanced_templates.helper.template_processor', + 'monolog.logger.mautic', ] ] ], diff --git a/EventListener/EmailSubscriber.php b/EventListener/EmailSubscriber.php index 66e01a7..19c6ab9 100644 --- a/EventListener/EmailSubscriber.php +++ b/EventListener/EmailSubscriber.php @@ -2,33 +2,47 @@ namespace MauticPlugin\MauticAdvancedTemplatesBundle\EventListener; use Mautic\CampaignBundle\Entity\Lead; -use Mautic\CoreBundle\EventListener\CommonSubscriber; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Mautic\EmailBundle\EmailEvents; use Mautic\EmailBundle\Event as Events; use Mautic\EmailBundle\Helper\PlainTextHelper; use Mautic\CoreBundle\Exception as MauticException; +use Mautic\LeadBundle\Model\LeadModel; use MauticPlugin\MauticAdvancedTemplatesBundle\Helper\TemplateProcessor; use Psr\Log\LoggerInterface; +use Monolog\Logger; /** * Class EmailSubscriber. */ -class EmailSubscriber extends CommonSubscriber +class EmailSubscriber implements EventSubscriberInterface { /** - * @var TokenHelper $tokenHelper ; + * @var LeadModel + */ + private $leadModel; + + /** + * @var TemplateProcessor $templateProcessor ; */ protected $templateProcessor; + /** + * @var LoggerInterface $logger ; + */ + protected $logger; + /** * EmailSubscriber constructor. * * @param TokenHelper $tokenHelper */ - public function __construct(TemplateProcessor $templateProcessor) + public function __construct(TemplateProcessor $templateProcessor, LeadModel $leadModel, Logger $logger) { $this->templateProcessor = $templateProcessor; + $this->leadModel = $leadModel; + $this->logger = $logger; } /** * @return array @@ -41,6 +55,40 @@ public static function getSubscribedEvents() ]; } + private function getProperties(Events\EmailSendEvent $event) { + $tokens = []; + + if (!$event->getEmail()) { + return [ + 'subject' => $event->getSubject(), + 'content' => $event->getContent(), + 'tokens' => $tokens, + ]; + } + + $email = $event->getEmail(); + + $subject = $email->getSubject(); + $content = $email->getCustomHtml(); + $dynamic = $email->getDynamicContent(); + + foreach ($dynamic as $prop) { + $tokens[$prop['tokenName']] = $prop['content']; + } + + //Add arbritrary tokens when using the email send api + $originalTokens = $event->getTokens(); + foreach($originalTokens as $k => $v) { + $tokens[preg_replace('/^{(.*)}$/', '${1}', $k)] = $v; + } + + return [ + 'subject' => $subject, + 'content' => $content, + 'tokens' => $tokens, + ]; + } + /** * Search and replace tokens with content * @@ -53,23 +101,30 @@ public function onEmailGenerate(Events\EmailSendEvent $event) { $this->logger->info('onEmailGenerate MauticAdvancedTemplatesBundle\EmailSubscriber'); - if ($event->getEmail()) { - $subject = $event->getEmail()->getSubject(); - $content = $event->getEmail()->getCustomHtml(); - }else{ - $subject = $event->getSubject(); - $content = $event->getContent(); + if ($event->isDynamicContentParsing()) { + return; } - $subject = $this->templateProcessor->processTemplate($subject, $event->getLead()); + $props = $this->getProperties($event); + + $lead = $event->getLead(); + $leadmodel = $this->leadModel->getEntity($lead['id']); + $lead['tags'] = []; + if ($leadmodel && count($leadmodel->getTags()) > 0) { + foreach ($leadmodel->getTags() as $tag) { + $lead['tags'][] = $tag->getTag(); + } + } + + $subject = $this->templateProcessor->processTemplate($props['subject'], $lead); $event->setSubject($subject); - $content = $this->templateProcessor->processTemplate($content, $event->getLead()); + $content = $this->templateProcessor->processTemplate($props['content'], $lead, $props['tokens']); + $content = $this->templateProcessor->addTrackingPixel($content); $event->setContent($content); - if ( empty( trim($event->getPlainText()) ) ) { $event->setPlainText( (new PlainTextHelper($content))->getText() ); } } -} \ No newline at end of file +} diff --git a/EventListener/SmsSubscriber.php b/EventListener/SmsSubscriber.php new file mode 100644 index 0000000..04217e1 --- /dev/null +++ b/EventListener/SmsSubscriber.php @@ -0,0 +1,85 @@ +templateProcessor = $templateProcessor; + $this->leadModel = $leadModel; + $this->logger = $logger; + } + /** + * @return array + */ + public static function getSubscribedEvents() + { + return [ + SmsEvents::SMS_ON_SEND => ['onSmsGenerate', 300], + // I dont know how to do this without editing core. + // since there does not seem to be a simular way to call it yet. + // SmsEvents::SMS_ON_DISPLAY => ['onSmsGenerate', 0], + ]; + } + + /** + * Search and replace tokens with content + * + * @param Events\SmsSendEvent $event + * @throws \Throwable + * @throws \Twig_Error_Loader + * @throws \Twig_Error_Syntax + */ + public function onSmsGenerate(Events\SmsSendEvent $event) + { + $this->logger->info('onSmsGenerate MauticAdvancedTemplatesBundle\SmsSubscriber'); + + $content = $event->getContent(); + + $lead = $event->getLead(); + $leadmodel = $this->leadModel->getEntity($lead['id']); + $lead['tags'] = []; + if ($leadmodel && count($leadmodel->getTags()) > 0) { + foreach ($leadmodel->getTags() as $tag) { + $lead['tags'][] = $tag->getTag(); + } + } + + $content = $this->templateProcessor->processTemplate($content, $lead); + $event->setContent($content); + } +} \ No newline at end of file diff --git a/Helper/TemplateProcessor.php b/Helper/TemplateProcessor.php index e287f6a..426f066 100644 --- a/Helper/TemplateProcessor.php +++ b/Helper/TemplateProcessor.php @@ -5,6 +5,10 @@ use MauticPlugin\MauticAdvancedTemplatesBundle\Feed\FeedFactory; use MauticPlugin\MauticCrmBundle\Integration\Salesforce\Object\Lead; use Psr\Log\LoggerInterface; +use Twig\Environment as Twig_Environment; +use Twig\Loader\ChainLoader as Twig_Loader_Chain; +use Twig\Loader\ArrayLoader as Twig_Loader_Array; +use Twig\TwigFilter as Twig_SimpleFilter; class TemplateProcessor { @@ -15,7 +19,7 @@ class TemplateProcessor protected $logger; /** - * @var \Twig_Environment + * @var Twig_Environment */ private $twigEnv; private $twigDynamicContentLoader; @@ -42,8 +46,8 @@ public function __construct(LoggerInterface $logger, Twig_Loader_DynamicContent $this->logger = $logger; $this->twigDynamicContentLoader = $twigDynamicContentLoader; $logger->debug('TemplateProcessor: created $twigDynamicContentLoader'); - $this->twigEnv = new \Twig_Environment(new \Twig_Loader_Chain([ - $twigDynamicContentLoader, new \Twig_Loader_Array([]) + $this->twigEnv = new Twig_Environment(new Twig_Loader_Chain([ + $twigDynamicContentLoader, new Twig_Loader_Array([]) ])); $this->configureTwig($this->twigEnv); $this->feedFactory = $feedFactory; @@ -56,43 +60,64 @@ public function __construct(LoggerInterface $logger, Twig_Loader_DynamicContent * @return string * @throws \Throwable */ - public function processTemplate($content, $lead) + public function processTemplate($content, $lead, $tokens = null) { $this->logger->debug('TemplateProcessor: Processing template'); - $this->logger->debug('LEAD: ' . var_export($lead, true)); + // This was causing huge memory usage. Uncomment to debug. + // $this->logger->debug('LEAD: ' . var_export($lead, true)); $content = preg_replace_callback_array([ - TemplateProcessor::$matchTwigBlockRegex => $this->processTwigBlock($lead) + TemplateProcessor::$matchTwigBlockRegex => $this->processTwigBlock($lead, $tokens) ], $content); $this->logger->debug('TemplateProcessor: Template processed'); return $content; } - protected function configureTwig(\Twig_Environment $twig) + protected function configureTwig(Twig_Environment $twig) { // You might want to register some custom TWIG tags or functions here // TWIG filter json_decode - $twig->addFilter(new \Twig_SimpleFilter('json_decode', function ($string) { + $twig->addFilter(new Twig_SimpleFilter('json_decode', function ($string) { return json_decode($string, true); })); - $twig->addFilter(new \Twig_SimpleFilter('rss', function () { + $twig->addFilter(new Twig_SimpleFilter('json_encode', function ($obj) { + return json_encode($obj); + })); + + $twig->addFilter(new Twig_SimpleFilter('rss', function () { return $this->feedFactory->getItems($this->lead['id'], func_get_args()); })); } - private function processTwigBlock($lead) + private function processTwigBlock($lead, $tokens = null) { $this->lead = $lead; - return function ($matches) use ($lead) { + return function ($matches) use ($lead, $tokens) { $templateSource = $matches[1]; - $this->logger->debug('BLOCK SOURCE: ' . var_export($templateSource, true)); + // Uncomment to debug. This causes high memory usage with var_export. + // $this->logger->debug('BLOCK SOURCE: ' . var_export($templateSource, true)); $template = $this->twigEnv->createTemplate($templateSource); $renderedTemplate = $template->render([ - 'lead' => $lead + 'lead' => $lead, + 'tokens' => $tokens ]); - $this->logger->debug('RENDERED BLOCK: ' . var_export($renderedTemplate, true)); + // Uncomment to debug. This causes high memory usage with var_export. + // $this->logger->debug('RENDERED BLOCK: ' . var_export($renderedTemplate, true)); return $renderedTemplate; }; } -} \ No newline at end of file + + public function addTrackingPixel($content) + { + // Append tracking pixel + $trackingImg = ''; + if (strpos($content, '') !== false) { + $content = str_replace('', $trackingImg.'', $content); + } else { + $content .= $trackingImg; + } + + return $content; + } +} diff --git a/Helper/Twig_Loader_DynamicContent.php b/Helper/Twig_Loader_DynamicContent.php index 7538908..49399f6 100644 --- a/Helper/Twig_Loader_DynamicContent.php +++ b/Helper/Twig_Loader_DynamicContent.php @@ -5,10 +5,11 @@ use Mautic\CoreBundle\Factory\ModelFactory; use Mautic\DynamicContentBundle\Entity\DynamicContent; use Psr\Log\LoggerInterface; -use Twig_Error_Loader; -use Twig_Source; +use Twig\Loader\LoaderInterface as Twig_LoaderInterface; +use Twig\Error\LoaderError as Twig_Error_Loader; +use Twig\Source as Twig_Source; -class Twig_Loader_DynamicContent implements \Twig_LoaderInterface, \Twig_ExistsLoaderInterface, \Twig_SourceContextLoaderInterface +class Twig_Loader_DynamicContent implements Twig_LoaderInterface { private static $NAME_PREFIX = 'dc:'; @@ -58,7 +59,7 @@ public function getSource($name) * @return string The cache key * */ - public function getCacheKey($name) + public function getCacheKey(string $name): string { return $name; } @@ -73,7 +74,7 @@ public function getCacheKey($name) * @return bool true if the template is fresh, false otherwise * */ - public function isFresh($name, $time) + public function isFresh(string $name, int $time): bool { // TODO: Implement isFresh() method. $this->logger->debug('Twig_Loader_DynamicContent: Is Fresh: ' . $time . ', ' . $name); @@ -85,11 +86,11 @@ public function isFresh($name, $time) * * @param string $name The template logical name * - * @return Twig_Source + * @return Twig\Source * - * @throws Twig_Error_Loader When $name is not found + * @throws Twig\Error\LoaderError When $name is not found */ - public function getSourceContext($name) + public function getSourceContext(string $name): Twig_Source { $dynamicContent = $this->findTemplate($this->aliasForTemplateName($name)); if ($dynamicContent == null) { @@ -147,7 +148,7 @@ private function findTemplate($resourceAlias) * * @return bool If the template source code is handled by this loader or not */ - public function exists($name) + public function exists(string $name) { return $this->supports($name) && $this->findTemplate($this->aliasForTemplateName($name)) !== null; } diff --git a/README.md b/README.md index 836e2d1..806dad7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Mautic Advanced Templates Bundle -Plugin extends default email template capabilities with TWIG block so you can use advanced templating techniques like conditions, loops etc. +Plugin extends default email template capabilities with TWIG block so you can use advanced templating techniques like conditions, loops etc. Support has also been extended to sms templates. ### Purpose @@ -12,10 +12,12 @@ Another example: you might want to include dynamic content to your email. Let's This plugin was tested with: -* Mautic v2.14.2 -* PHP v7.1.23 +* Mautic v4.4.0 +* PHP v8.0 -There is a high probability it is compatible with other environments, but we never tested it. +There is a high probability it is compatible with other 4.x versions but it is untested. Will not work with versions lower than 4.x. + +* Mautic 2.x - [Release 1.1](https://github.com/Logicify/mautic-advanced-templates-bundle/releases/tag/1.1) ### Features @@ -28,12 +30,14 @@ There is a high probability it is compatible with other environments, but we nev * Reusable TWIG snippets could be loaded form Dynamic Content entities. * TWIG extended with some useful functions and filters (see below). * RSS support -* RSS items related to contact's segment preferences center and RSS category +* RSS items related to contact's segment preferences center and RSS category +* json_encode, json_decode twig implementations +* ## Installation 1. Download or clone this bundle into your Mautic `/plugins` folder. **Make sure the name of the folder containing plugin files is** `MauticAdvancedTemplatesBundle` (case sensitive). Rename it if it isn't, otherwise it will not be recognized. -2. Delete your cache (`app/cache/prod`). +2. Delete your cache with the command (`php bin/console cache:clear`). 3. In the Mautic GUI, go to the gear and then to Plugins. 4. Click "Install/Upgrade Plugins". 5. You should see the Advanced Templates Bundle in your list of plugins. @@ -169,9 +173,59 @@ https://www.w3schools.com/xml/rss_tag_category_item.asp {% END_TWIG_BLOCK %} ``` +### Example 6: Using `lead.tags` + +```twig + {% TWIG_BLOCK %} + {% set tags = lead.tags %} + Tags: +
    + {% for item in tags %} +
  • {{item}}
  • + {% endfor %} +
+ {% END_TWIG_BLOCK %} +``` + +### Example 7: Rendering structured data from tokens + +Instead of pushing data to a custom field, you can specify dynamic data when using the Email Send API. When making the API call, set your POST body to a JSON object including a `tokens` key like below: + +```json +{ + "tokens": { + "{cart}": [{"sku":"A100","name":"Item 1"},{"sku":"Z200","name":"Item 2"}] + } +} +``` + +To render, code something like this: + +```twig +{% TWIG_BLOCK %} + Your cart: +
    + {% for item in cart %} +
  • Item Name: {{ item.name }} (SKU: {{ item.sku }})
  • + {% endfor %} +
+{% END_TWIG_BLOCK %} +``` + ## Credits -Dmitry Berezovsky, Logicify ([http://logicify.com/](https://logicify.com/?utm_source=github&utm_campaign=mautic-templates&utm_medium=opensource)) + - Dmitry Berezovsky, Logicify ([http://logicify.com/](https://logicify.com/?utm_source=github&utm_campaign=mautic-templates&utm_medium=opensource)) + - Luis Rodriguez, ldrrp/MarketSmart ([https://github.com/ldrrp](https://github.com/ldrrp)) - [Contact me on slack](https://www.mautic.org/slack) + +## Contributors + +Thanks goes to these wonderful people + + - [Leo Giovanetti, leog](https://github.com/leog) - Lead tags implementation + - [Ben U, bobsburgers](https://github.com/bobsburgers) - SMS implementation + - [Felipe J. L. Rita, zerodois](https://github.com/zerodois) - Mautic 4.x compatibility + - [Nick Pappas, radicand](https://github.com/radicand) - Arbritrary Send Mail API tokens + ## Disclaimer