├── .coveralls.yml ├── .github └── dependabot.yml ├── .gitignore ├── .styleci.yml ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── composer.lock ├── phpunit.xml └── src ├── Mail └── Mailable.php ├── Process └── MJML.php ├── Providers └── MJMLServiceProvider.php └── config └── mjml.php /.coveralls.yml: -------------------------------------------------------------------------------- 1 | coverage_clover: coverage.xml 2 | json_path: coverage-upload.json 3 | service_name: travis-ci -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: composer 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "23:30" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: laravel/framework 11 | versions: 12 | - 6.20.15 13 | - 6.20.17 14 | - 6.20.18 15 | - 6.20.19 16 | - 6.20.20 17 | - 6.20.22 18 | - 6.20.23 19 | - 6.20.24 20 | - 6.20.25 21 | - dependency-name: orchestra/testbench 22 | versions: 23 | - 4.14.0 24 | - 4.15.0 25 | - dependency-name: symfony/process 26 | versions: 27 | - 4.4.19 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | node_modules/ 3 | npm-debug.log 4 | 5 | # Laravel 4 specific 6 | bootstrap/compiled.php 7 | app/storage/ 8 | 9 | # Laravel 5 & Lumen specific 10 | public/storage 11 | public/hot 12 | storage/*.key 13 | .env.*.php 14 | .env.php 15 | .env 16 | Homestead.yaml 17 | Homestead.json 18 | 19 | # Rocketeer PHP task runner and deployment package. https://github.com/rocketeers/rocketeer 20 | .rocketeer/ 21 | composer.phar 22 | .idea -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel 2 | 3 | enabled: 4 | - align_double_arrow 5 | - align_equals 6 | - no_useless_else 7 | 8 | disabled: 9 | - concat_without_spaces 10 | - unalign_equals -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | install: 3 | composer install 4 | php: 5 | - 7.1 6 | - 7.2 7 | 8 | script: 9 | - mkdir -p build/logs 10 | - php vendor/bin/phpunit -c phpunit.xml --coverage-clover=coverage.xml --testdox 11 | 12 | after_success: 13 | - bash <(curl -s https://codecov.io/bash) -t 03ae5c0f-95ef-4671-a35b-0a9cea99f56c 14 | 15 | after_script: 16 | - php vendor/bin/php-coveralls -v -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ajitem Sahasrabuddhe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel MJML 2 | 3 | Build responsive e-mails easily using MJML and Laravel Mailables. 4 | 5 | ### MJML 6 | 7 | MJML is an awesome tool from MailJet that allows us to create responsive emails very easily. For more information on how to use it, head to their documentation [here](https://mjml.io/documentation/#mjml-guides) 8 | 9 | ## Installation 10 | 11 | To install this package, require this package using composer as follows: 12 | 13 | `composer require asahasrabuddhe/laravel-mjml` 14 | 15 | After composer installs the packages and all the dependencies, publish the package configuration using artisan command: 16 | 17 | `php artisan vendor:publish` 18 | 19 | Select the laravel-mjml in the list. You will also need to install the MJML CLI: 20 | 21 | `npm install --save mjml` 22 | 23 | ## Getting Started 24 | 25 | 1. Create a view containing MJML in your resources/views directory. 26 | 2. Create a mailable class using artisan command: `php artisan make:mail MJMLEmail` 27 | 3. In the mailable class, replace 28 | 29 | ``` use Illuminate\Mail\Mailable;``` 30 | 31 | with 32 | 33 | ```use Asahasrabuddhe\LaravelMJML\Mail\Mailable;``` 34 | 4. For Laravel 8 and below, in the `build` method, use: 35 | 36 | ```php 37 | public function build() 38 | { 39 | return $this->mjml('view.name') 40 | } 41 | ``` 42 | 43 | For Laravel 9, in the `content` method:, use: 44 | 45 | ```php 46 | public function content() 47 | { 48 | return new Content( 49 | view: $this->mjml('view.name')->buildMjmlView()['html'], 50 | ); 51 | } 52 | ``` 53 | 54 | ## Configuration 55 | 56 | By default, the package will automatically detect the path of the MJML CLI installed locally in the project. In case this does not happen or the MJML CLI is installed globally, please update the configuration file likewise. 57 | 58 | That's it! You have successfully installed and configured the MJML package for use. Just create new views and use them in the mailables class. -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "asahasrabuddhe/laravel-mjml", 3 | "description": "A package that enables using MJML with Laravel Mailables.", 4 | "license" : "MIT", 5 | "authors": [ 6 | { 7 | "name": "Ajitem Sahasrabuddhe", 8 | "email": "ajitem.s@outlook.com" 9 | } 10 | ], 11 | "autoload": { 12 | "psr-4": { 13 | "Asahasrabuddhe\\LaravelMJML\\": "src/" 14 | } 15 | }, 16 | "autoload-dev": { 17 | "psr-4": { 18 | "Asahasrabuddhe\\LaravelMJML\\Tests\\": "tests/" 19 | } 20 | }, 21 | "extra": { 22 | "laravel": { 23 | "providers": [ 24 | "Asahasrabuddhe\\LaravelMJML\\Providers\\MJMLServiceProvider" 25 | ] 26 | } 27 | }, 28 | "require-dev": { 29 | "orchestra/testbench": "~6.41.0", 30 | "php-coveralls/php-coveralls": "^2.5.2" 31 | }, 32 | "require": { 33 | "symfony/process": "^4.0.0|^5.0.0|^6.0.0|^7.0.0", 34 | "soundasleep/html2text": "~2.1.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 16 | ./tests/ 17 | 18 | 19 | 20 | 21 | 22 | src/ 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/Mail/Mailable.php: -------------------------------------------------------------------------------- 1 | mjml = $view; 35 | $this->viewData = array_merge($this->buildViewData(), $data); 36 | 37 | return $this; 38 | } 39 | 40 | /** 41 | * Set the MJML content for the message. 42 | * 43 | * @param string $mjmlContent 44 | * @return $this 45 | */ 46 | public function mjmlContent($mjmlContent) 47 | { 48 | $this->mjmlContent = $mjmlContent; 49 | 50 | return $this; 51 | } 52 | 53 | /** 54 | * Build the view for the message. 55 | * 56 | * @return array|string 57 | */ 58 | protected function buildView() 59 | { 60 | if (isset($this->mjml) || isset($this->mjmlContent)) { 61 | return $this->buildMjmlView(); 62 | } 63 | return parent::buildView(); 64 | } 65 | 66 | /** 67 | * Build the MJML view for the message. 68 | * 69 | * @return array 70 | */ 71 | protected function buildMjmlView() 72 | { 73 | if (isset($this->mjml)) { 74 | $this->mjmlContent = View::make($this->mjml, $this->buildViewData()); 75 | } 76 | $mjml = new MJML($this->mjmlContent); 77 | 78 | return [ 79 | 'html' => $mjml->renderHTML(), 80 | 'text' => $mjml->renderText(), 81 | ]; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Process/MJML.php: -------------------------------------------------------------------------------- 1 | mjmlContent = $mjmlViewOrMjml; 43 | // Hash combined data and path. If either change, new pre-compiled file is generated. 44 | $dataPathChecksum = hash('sha256', $mjmlViewOrMjml); 45 | } else { 46 | $this->view = $mjmlViewOrMjml; 47 | // Hash combined data and path. If either change, new pre-compiled file is generated. 48 | $dataPathChecksum = hash('sha256', json_encode([ 49 | 'path' => $this->view->getPath(), 50 | 'data' => $this->view->getData(), 51 | ])); 52 | } 53 | 54 | $this->path = rtrim(config('view.compiled'), '/') . "/{$dataPathChecksum}.mjml.php"; 55 | } 56 | 57 | /** 58 | * Build the mjml command. 59 | * 60 | * @return string 61 | */ 62 | public function buildCmdLineFromConfig() 63 | { 64 | return implode(' ', [ 65 | config('mjml.node_path'), 66 | config('mjml.auto_detect_path') ? $this->detectBinaryPath() : config('mjml.path_to_binary'), 67 | $this->path, 68 | $this->view ? '--config.filePath=' . dirname($this->view->getPath()) : '', 69 | '-o', 70 | $this->compiledPath, 71 | ]); 72 | } 73 | 74 | /** 75 | * Render the html content. 76 | * 77 | * @return HtmlString 78 | * 79 | * @throws \Throwable 80 | */ 81 | public function renderHTML() 82 | { 83 | if ($this->view) { 84 | $this->mjmlContent = $this->view->render(); 85 | } 86 | 87 | File::put($this->path, $this->mjmlContent); 88 | 89 | $contentChecksum = hash('sha256', $this->mjmlContent); 90 | $this->compiledPath = rtrim(config('view.compiled'), '/') . "/{$contentChecksum}.php"; 91 | 92 | if (! File::exists($this->compiledPath)) { 93 | $this->process = Process::fromShellCommandline($this->buildCmdLineFromConfig()); 94 | $this->process->run(); 95 | 96 | if (! $this->process->isSuccessful()) { 97 | throw new ProcessFailedException($this->process); 98 | } 99 | } 100 | 101 | return new HtmlString(File::get($this->compiledPath)); 102 | } 103 | 104 | /** 105 | * Render the text content. 106 | * 107 | * @return HtmlString 108 | * 109 | * @throws \Html2Text\Html2TextException 110 | * @throws \Throwable 111 | */ 112 | public function renderText() 113 | { 114 | return new HtmlString(html_entity_decode(preg_replace("/[\r\n]{2,}/", "\n\n", Html2Text::convert($this->renderHTML(), ['ignore_errors' => true])), ENT_QUOTES, 'UTF-8')); 115 | } 116 | 117 | /** 118 | * Detect the path to the mjml executable. 119 | * 120 | * @return string 121 | */ 122 | public function detectBinaryPath() 123 | { 124 | return base_path('node_modules/.bin/mjml'); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Providers/MJMLServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 19 | __DIR__ . '/../config/mjml.php' => config_path('mjml.php'), 20 | ]); 21 | } 22 | 23 | /** 24 | * Register the service provider. 25 | * 26 | * @return void 27 | */ 28 | public function register() 29 | { 30 | $this->mergeConfigFrom( 31 | __DIR__ . '/../config/mjml.php', 32 | 'mjml' 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/config/mjml.php: -------------------------------------------------------------------------------- 1 | env('MJML_AUTO_DETECT_PATH', true), 13 | /* 14 | * The path to the MJML binary 15 | */ 16 | 'path_to_binary' => env('MJML_PATH_TO_BINARY', ''), 17 | /* 18 | * The path to the NODE binary 19 | */ 20 | 'node_path' => env('MJML_NODE_PATH', 'node'), 21 | ]; 22 | --------------------------------------------------------------------------------