From cfaa9a5cb582dcb1e46a8f10749156571ccaac66 Mon Sep 17 00:00:00 2001 From: Fred Carlsen Date: Tue, 10 May 2022 08:58:07 +0200 Subject: [PATCH] Added include method and better error handling (#18) * Added include method and better error handling * Cleanup * Update changelog * Add docs for using api in include method * Add mj-include support --- CHANGELOG.md | 9 ++++ README.md | 52 ++++++++++++++++++++ composer.json | 2 +- src/MJML.php | 12 ----- src/exceptions/MJMLException.php | 33 +++++++++++++ src/models/Settings.php | 13 ++++- src/services/MJMLService.php | 83 +++++++++++++++++++++++--------- src/variables/MJMLVariable.php | 13 ++++- 8 files changed, 178 insertions(+), 39 deletions(-) create mode 100644 src/exceptions/MJMLException.php diff --git a/CHANGELOG.md b/CHANGELOG.md index cbab19b..2ad3ca8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## 1.0.6 - 2021-03-17 +### Added +- Added `include` method similar to Twig's `include` method so we can cache the MJML template once and then render the dynamic parts with Twig +- Added support for `` tags for the CLI option +- Added `mjmlCliIncludesPath` config setting + +### Changed +- Changed error handling to log more detailed error messages + ## 1.0.5 - 2021-03-14 ### Added - Allow for optional CLI config settings (e.g. minify) diff --git a/README.md b/README.md index 535e9d1..e1f722e 100644 --- a/README.md +++ b/README.md @@ -115,4 +115,56 @@ Dynamic example with MJML CLI: To use the API instead, swap `mjmlCli` with `mjml`. +### Caching + +The above examples will be cached. If you are passing Twig variables, each output will however be unique, rendering the cache ineffective. + +In this instance you probably would like to use the `include` method: + +```twig +{{ craft.mjml.include('path/to/template.twig', { + subject: 'Static subject', + email: contact.email, +}) }} +``` + +The `include` method uses the CLI option by default, but you can set it to use the MJML API by passing `api` as the third option, like so: + +```twig +{{ craft.mjml.include('path/to/template.twig', { + subject: 'Static subject', + email: contact.email, +}, 'api') }} +``` + +Here is an example passing a contact in a [newsletter template inside the Campaign plugin](https://putyourlightson.com/plugins/campaign#mjml). The template path here is relative to your site templates root. + +This will first render the MJML template once, cache it, then it will render the dynamic parts with Twig for each instance. + +### Includes + +A caveat if you want to use includes: + +Twig's built-in `include` method won't work in combination with MJML inside the template passed to the plugin `include` method. + +This is because MJML is rendered first, before Twig, so if you include MJML in a partial that won't be rendered. + +#### Workaround for MJML includes + +_Note that this is only supported for the CLI option._ + +A workaround for partials is to use the `` tag to. Any partials referenced here will be relative to the Site templates root. + +```html + +``` + +Note that you have to append the file extension here. This will resolve to `/templates/mjml-partial.twig`. + +Another caveat with `mj-include` is that the content of partials isn't currently included when checking the cache of a rendered MJML template. + +This means that if you render a MJML template that in turns has a `` partial, then changes the content of the partial, the cache will be stale and your template won't reflect the changes. + +A workaround for now is to clear the `storage/runtime/temp/mjml` folder in case this happens to you. + Brought to you by [Superbig](https://superbig.co) diff --git a/composer.json b/composer.json index 5a7e21d..868892a 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "superbig/craft-mjml", "description": "Render Twig emails with MJML, the only framework that makes responsive email easy.", "type": "craft-plugin", - "version": "1.0.5", + "version": "1.0.6", "keywords": [ "craft", "cms", diff --git a/src/MJML.php b/src/MJML.php index 7c406be..08549c8 100644 --- a/src/MJML.php +++ b/src/MJML.php @@ -37,25 +37,16 @@ */ class MJML extends Plugin { - // Static Properties - // ========================================================================= - /** * @var MJML */ public static $plugin; - // Public Properties - // ========================================================================= - /** * @var string */ public $schemaVersion = '1.0.0'; - // Public Methods - // ========================================================================= - /** * @inheritdoc */ @@ -98,9 +89,6 @@ function(Event $event) { ); } - // Protected Methods - // ========================================================================= - /** * @inheritdoc */ diff --git a/src/exceptions/MJMLException.php b/src/exceptions/MJMLException.php new file mode 100644 index 0000000..51c7b2c --- /dev/null +++ b/src/exceptions/MJMLException.php @@ -0,0 +1,33 @@ +cliCommand = $cmd; + } + + public function getCliCommand() + { + return $this->cliCommand; + } +} diff --git a/src/models/Settings.php b/src/models/Settings.php index d887b71..393eaf0 100644 --- a/src/models/Settings.php +++ b/src/models/Settings.php @@ -40,6 +40,11 @@ class Settings extends Model */ public $mjmlCliConfigArgs = ''; + /** + * @var string + */ + public $mjmlCliIncludesPath = ''; + /** * @var string */ @@ -50,8 +55,12 @@ class Settings extends Model */ public $secretKey = ''; - // Public Methods - // ========================================================================= + public function init() + { + $this->mjmlCliIncludesPath = Craft::$app->getView()->getTemplatesPath(); + + parent::init(); + } /** * @inheritdoc diff --git a/src/services/MJMLService.php b/src/services/MJMLService.php index 91affc2..29cfa8e 100644 --- a/src/services/MJMLService.php +++ b/src/services/MJMLService.php @@ -13,12 +13,14 @@ use craft\helpers\FileHelper; use craft\helpers\Json; use craft\helpers\Template; +use craft\web\View; use GuzzleHttp\Client; use mikehaertl\shellcommand\Command; use superbig\mjml\MJML; use Craft; use craft\base\Component; +use superbig\mjml\exceptions\MJMLException; use superbig\mjml\models\MJMLModel; /** @@ -39,10 +41,10 @@ class MJMLService extends Component public function parse($html) { $settings = MJML::$plugin->getSettings(); - $hash = md5($html); - $client = new Client([ + $hash = md5($html); + $client = new Client([ 'base_uri' => 'https://api.mjml.io/v1/', - 'auth' => [$settings->appId, $settings->secretKey], + 'auth' => [$settings->appId, $settings->secretKey], ]); try { @@ -73,32 +75,70 @@ public function parse($html) } } + public function include(string $template = '', $variables = [], $renderMethod = 'cli') + { + try { + $templatePath = Craft::$app->getView()->resolveTemplate($template, View::TEMPLATE_MODE_SITE); + + if (!$templatePath) { + throw new MJMLException('Could not find template: ' . $template); + } + + $html = file_get_contents($templatePath); + $hash = md5($html); + /** @var MJMLModel|null $output */ + $output = Craft::$app->getCache()->getOrSet("mjml-{$hash}-{$renderMethod}", function() use ($html, $renderMethod) { + return $renderMethod === 'cli' ? $this->parseCli($html) : $this->parse($html); + }); + + if (!$output) { + throw new MJMLException('Could not render template: ' . $template); + } + + return Craft::$app->getView()->renderString($output->output(), $variables); + } catch (MJMLException $e) { + Craft::error('Could not generate output: ' . $e->getMessage(), 'mjml'); + } + } + /** * @param null $html * - * @return MJMLModel + * @return MJMLModel|null * @throws \yii\base\ErrorException */ public function parseCli($html = null) { - $settings = MJML::$plugin->getSettings(); - $mjmlPath = "{$settings->nodePath} {$settings->mjmlCliPath}"; - $hash = md5($html); - $configArgs = "{$settings->mjmlCliConfigArgs}"; - $tempPath = Craft::$app->getPath()->getTempPath() . "/mjml/mjml-{$hash}.html"; + $settings = MJML::$plugin->getSettings(); + $configArgs = "{$settings->mjmlCliConfigArgs}"; + + if (!empty($settings->mjmlCliIncludesPath)) { + $configArgs = "{$configArgs} --config.filePath {$settings->mjmlCliIncludesPath}"; + } + + $mjmlPath = "{$settings->nodePath} {$settings->mjmlCliPath}"; + $hash = md5($html); + $tempPath = Craft::$app->getPath()->getTempPath() . "/mjml/mjml-{$hash}.html"; $tempOutputPath = Craft::$app->getPath()->getTempPath() . "/mjml/mjml-output-{$hash}.html"; - if (file_exists($tempOutputPath) == false or Craft::$app->request->getIsPreview()) { - FileHelper::writeToFile($tempPath, $html); + try { + if (!file_exists($tempOutputPath)) { + FileHelper::writeToFile($tempPath, $html); - $cmd = "$mjmlPath $tempPath $configArgs -o $tempOutputPath"; + $cmd = "$mjmlPath $tempPath $configArgs -o $tempOutputPath"; - $message = $this->executeShellCommand($cmd); - - // Log Cli output if in devMode - if(Craft::$app->getConfig()->general->devMode){ - Craft::info($message, 'mjml'); + $this->executeShellCommand($cmd); } + } catch (MJMLException $e) { + Craft::error('Could not generate output: ' . $e->getMessage(), 'mjml'); + + return null; + } + + if (!file_exists($tempOutputPath)) { + Craft::error('Could not find generated output: ' . $tempOutputPath, 'mjml'); + + return null; } $output = file_get_contents($tempOutputPath); @@ -132,13 +172,10 @@ protected function executeShellCommand(string $command): string } // Return the result of the command's output or error - if ($shellCommand->execute()) { - $result = $shellCommand->getOutput(); - } - else { - $result = $shellCommand->getError(); + if (!$shellCommand->execute()) { + throw new MJMLException("Failed to run {$command}: " . $shellCommand->getError()); } - return $result; + return $shellCommand->getOutput(); } } diff --git a/src/variables/MJMLVariable.php b/src/variables/MJMLVariable.php index 33a7b32..63813a1 100644 --- a/src/variables/MJMLVariable.php +++ b/src/variables/MJMLVariable.php @@ -10,6 +10,7 @@ namespace superbig\mjml\variables; +use craft\helpers\Template; use superbig\mjml\MJML; use Craft; @@ -26,7 +27,7 @@ class MJMLVariable // ========================================================================= /** - * @param null $html + * @param null|string $html * * @return MJMLModel|null */ @@ -35,6 +36,16 @@ public function parse($html = null) return MJML::$plugin->mjmlService->parse($html); } + /** + * @param string $template + * + * @return MJMLModel|null + */ + public function include(string $template = '', $variables = null, $renderMethod = 'cli') + { + return Template::raw(MJML::$plugin->mjmlService->include($template, $variables, $renderMethod)); + } + /** * @param null $html *