├── .gitignore ├── .styleci.yml ├── README.md ├── composer.json ├── config └── laravel-email-templates.php ├── migrations └── 2017_03_29_204835_email_template_tables.php └── src ├── EmailTemplateRepository.php ├── EmailTemplates.php ├── Entities ├── EmailLayout.php └── EmailTemplate.php ├── Facades └── EmailTemplates.php ├── Helpers ├── Bindings.php └── CSS.php ├── ServiceProvider.php ├── StringView.php └── TemplateMailable.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel 2 | linting: true 3 | 4 | disabled: 5 | - not_operator_with_successor_space 6 | - concat_without_spaces 7 | - simplified_null_return 8 | 9 | enabled: 10 | - concat_with_spaces 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![StyleCI](https://styleci.io/repos/85436471/shield)](https://styleci.io/repos/85436471) 2 | 3 | # Laravel Email Templates 4 | 5 | Database driven email templates for >= Laravel 5.4 and PHP 7. 6 | 7 | ## Configuration 8 | 9 | First register the service provider in config/app.php: 10 | 11 | ```php 12 | 'providers' => [ 13 | # ... 14 | JDT\LaravelEmailTemplates\ServiceProvider::class, 15 | ], 16 | ``` 17 | 18 | Then, in the same file, add the facade to the aliases config: 19 | 20 | ```php 21 | 'aliases' => [ 22 | # ... 23 | 'EmailTemplate' => JDT\LaravelEmailTemplates\Facades\EmailTemplates::class, 24 | ] 25 | ``` 26 | 27 | Next, run the migrations: 28 | 29 | ``` 30 | $ php artisan migrate 31 | ``` 32 | This will set up 2 tables; email_template and email_layout. 33 | 34 | ## Usage 35 | 36 | The package is built in such a way that it plays nicely with the existing Laravel Mailer functionality. 37 | 38 | Given a template existing in the above table with the handle 'registration', email can be sent simply as below: 39 | 40 | ```php 41 | $mail = \EmailTemplate::fetch('registration', ['name' => 'Jon']); 42 | 43 | \Mail::to('foo@bar.com', $mail); 44 | ``` 45 | 46 | You can of course pass the language to translate the chosen email, providing you have created an email for that 47 | handle/language combination. 48 | 49 | ```php 50 | $mail = \EmailTemplate::fetch('registration', ['name' => 'Jon'], 'es'); 51 | 52 | \Mail::to('foo@bar.com', $mail); 53 | ``` 54 | 55 | This package doesn't rely on a templating engine such as Blade or Twig to handle any 56 | of the email messages, but does provide it's own view class adhering to Laravel contracts. 57 | 58 | This means that you can pass data to the email just as you would any other view, without 59 | having to worry about the choice of templating package you use elsewhere in your project. 60 | 61 | ```php 62 | $mail = \EmailTemplate::fetch('registration', ['first_name' => 'Jon']); 63 | 64 | $mail->with('last_name', 'Braud'); 65 | 66 | $mail->with([ 67 | 'verify_url'=> 'https:/....', 68 | 'signup_time' => \Carbon\Carbon::now()->toDateTimeString() 69 | ]); 70 | 71 | \Mail::to('foo@bar.com', $mail); 72 | ``` 73 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jdtsoftware/laravel-email-templates", 3 | "description": "Laravel 5 database driven email templating", 4 | "type": "library", 5 | "authors": [ 6 | { 7 | "name": "Jon Braud", 8 | "email": "jon@jdtsoftware.co.uk" 9 | } 10 | ], 11 | "require": { 12 | "illuminate/contracts": "^5.4", 13 | "illuminate/database": "^5.4", 14 | "illuminate/mail": "^5.4.17", 15 | "tijsverkoyen/css-to-inline-styles": "^2.2" 16 | }, 17 | "extra": { 18 | "laravel": { 19 | "providers": [ 20 | "JDT\\LaravelEmailTemplates\\ServiceProvider" 21 | ], 22 | "aliases": { 23 | "EmailTemplate": "JDT\\LaravelEmailTemplates\\Facades\\EmailTemplates" 24 | } 25 | } 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "JDT\\LaravelEmailTemplates\\": "src/" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /config/laravel-email-templates.php: -------------------------------------------------------------------------------- 1 | 'en', 6 | 7 | // Whether to fall back to the default language if your requested template 8 | // does not exist in your chosen language (other than default) 9 | 'defaultLanguageFallback' => true, 10 | 11 | // Variable binding anchors. The default style is {{var}} but these can be changed 12 | // if you have reason to do so. 13 | 'bindingAnchors' => [ 14 | 'open' => '{{', 15 | 'close' => '}}', 16 | ], 17 | 18 | // Whether to cache email templates 19 | 'cache' => true, 20 | 21 | // Duration in minutes 22 | 'cacheDuration' => 60, 23 | 24 | // Format for cache keys, replaced items are: handle, language, ownerId 25 | 'cacheKeyFormat' => 'email_template_%s_%s_%d', 26 | ]; 27 | -------------------------------------------------------------------------------- /migrations/2017_03_29_204835_email_template_tables.php: -------------------------------------------------------------------------------- 1 | increments('id')->unsigned(); 18 | $table->text('layout'); 19 | $table->timestamps(); 20 | $table->softDeletes(); 21 | }); 22 | 23 | Schema::create('email_template', function (Blueprint $table) { 24 | $table->increments('id')->unsigned(); 25 | $table->integer('owner_id')->unsigned()->nullable(); 26 | $table->integer('layout_id')->unsigned(); 27 | $table->string('handle', 128); 28 | $table->string('subject', 128); 29 | $table->text('content'); 30 | $table->string('language', 4)->default('en'); 31 | $table->timestamps(); 32 | $table->softDeletes(); 33 | 34 | $table->foreign('layout_id')->references('id')->on('email_layout'); 35 | }); 36 | } 37 | 38 | /** 39 | * Reverse the migrations. 40 | * 41 | * @return void 42 | */ 43 | public function down() 44 | { 45 | Schema::dropIfExists('email_template'); 46 | Schema::dropIfExists('email_layout'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/EmailTemplateRepository.php: -------------------------------------------------------------------------------- 1 | where('language', $language); 35 | 36 | if (!empty($fallbackLanguage)) { 37 | $query->orWhere('language', $fallbackLanguage); 38 | } 39 | 40 | if (!empty($ownerId)) { 41 | $query->where('owner_id', (int) $ownerId); 42 | } 43 | 44 | return $query->first(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/EmailTemplates.php: -------------------------------------------------------------------------------- 1 | repository = $emailTemplateRepository; 38 | $this->cache = $cache; 39 | $this->stylesheet = $stylesheet; 40 | } 41 | 42 | /** 43 | * @param string $handle 44 | * @param array $data 45 | * @param string|null $language 46 | * @param null $ownerId 47 | * @return TemplateMailable 48 | * @throws \Exception 49 | */ 50 | public function fetch(string $handle, array $data = [], string $language = null, $ownerId = null):TemplateMailable 51 | { 52 | // If a language wasn't passed then we check if we have a default 53 | // language to fall back to. If we have neither, don't continue. 54 | if (empty($language)) { 55 | $language = config('laravel-email-templates.defaultLanguage'); 56 | 57 | if (empty($language)) { 58 | throw new \Exception( 59 | 'No language passed to fetch(). Either pass a language or set the defaultLanguage config item.' 60 | ); 61 | } 62 | } 63 | 64 | $entity = null; 65 | $caching = config('laravel-email-templates.cache'); 66 | if ($caching) { 67 | $entity = $this->cache->get($this->getCacheKey($handle, $language, $ownerId)); 68 | } 69 | 70 | if (empty($entity)) { 71 | $entity = $this->repository->findByHandle( 72 | $handle, 73 | $language, 74 | config('laravel-email-templates.defaultLanguageFallback'), 75 | $ownerId 76 | ); 77 | } 78 | 79 | if (empty($entity)) { 80 | return null; 81 | } 82 | 83 | if ($caching) { 84 | $this->cache->put( 85 | $this->getCacheKey($handle, $language, $ownerId), 86 | $entity, 87 | config('laravel-email-templates.cacheDuration') 88 | ); 89 | } 90 | 91 | return new TemplateMailable($entity, $data, $this->stylesheet); 92 | } 93 | 94 | /** 95 | * Generate the cache key for the given template data. 96 | * 97 | * @param $handle 98 | * @param $language 99 | * @param $ownerId 100 | * @return string 101 | */ 102 | protected function getCacheKey($handle, $language, $ownerId) 103 | { 104 | return sprintf(config('laravel-email-templates.cacheKeyFormat'), $handle, $language, $ownerId); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Entities/EmailLayout.php: -------------------------------------------------------------------------------- 1 | hasOne(EmailLayout::class, 'id', 'layout_id'); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Facades/EmailTemplates.php: -------------------------------------------------------------------------------- 1 | {{first_name}} (default anchors). 15 | * 16 | * @param $data 17 | * @return array 18 | */ 19 | public static function normaliseKeys(array $data) : array 20 | { 21 | $open = preg_quote(config('laravel-email-templates.bindingAnchors.open', '{{')); 22 | $close = preg_quote(config('laravel-email-templates.bindingAnchors.open', '}}')); 23 | $result = []; 24 | 25 | foreach ($data as $key => $val) { 26 | $pattern = '/' . $open . '.+' . $close . '/'; 27 | 28 | if (($ret = preg_match($pattern, $key)) === false) { 29 | throw new \RuntimeException("Invalid pattern '{$pattern} with current email anchor configuration"); 30 | } elseif ($ret === 0) { 31 | $key = '{{' . trim($key) . '}}'; 32 | } 33 | 34 | $result[$key] = $val; 35 | } 36 | 37 | return $result; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Helpers/CSS.php: -------------------------------------------------------------------------------- 1 | convert($content, $css); 23 | } 24 | 25 | return $content; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerViews(); 27 | 28 | $this->registerMigrations(); 29 | } 30 | 31 | /** 32 | * Register view paths. 33 | */ 34 | public function registerViews() 35 | { 36 | $this->loadViewsFrom(__DIR__ . '/../views', 'laravel-email-templates'); 37 | 38 | $this->publishes([ 39 | __DIR__ . '/../views' => resource_path('views/vendor/laravel-email-templates'), 40 | ]); 41 | } 42 | 43 | /** 44 | * Register config paths. 45 | */ 46 | public function registerConfig() 47 | { 48 | $this->publishes([ 49 | __DIR__ . '/../config/laravel-email-templates.php' => config_path('laravel-email-templates.php'), 50 | ]); 51 | 52 | $this->mergeConfigFrom( 53 | __DIR__ . '/../config/laravel-email-templates.php', 54 | 'laravel-email-templates' 55 | ); 56 | } 57 | 58 | /** 59 | * Register migrations. 60 | */ 61 | public function registerMigrations() 62 | { 63 | $this->loadMigrationsFrom(__DIR__ . '/../migrations'); 64 | } 65 | 66 | /** 67 | * Register the service provider. 68 | * 69 | * @return void 70 | */ 71 | public function register() 72 | { 73 | $this->registerConfig(); 74 | $this->app->bind('laravel-email-templates', function ($app) { 75 | return new EmailTemplates( 76 | new EmailTemplateRepository(), 77 | $app->make('cache.store'), 78 | \Config::get('laravel-email-templates.css_file') 79 | ); 80 | }); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/StringView.php: -------------------------------------------------------------------------------- 1 | email = $email; 45 | $this->data = $data; 46 | $this->stylesheet = $stylesheet; 47 | $this->html = $html; 48 | } 49 | 50 | /** 51 | * Get content as a string of HTML. Falls back to text if HTML not requested. 52 | * 53 | * @return string 54 | */ 55 | public function toHtml() : string 56 | { 57 | if ($this->html) { 58 | $emailLayout = $this->email->layout; 59 | $layout = ''; 60 | 61 | if (!empty($emailLayout)) { 62 | $layout = $emailLayout->getOriginal()['layout']; 63 | } 64 | 65 | $content = !empty($layout) 66 | ? str_replace('{{content}}', nl2br($this->render()), $layout) 67 | : nl2br($this->render()); 68 | 69 | if (!empty($this->stylesheet)) { 70 | return CSS::transform($this->translateBindings($content), $this->stylesheet); 71 | } 72 | 73 | return $this->translateBindings($content); 74 | } 75 | 76 | return $this->render(); 77 | } 78 | 79 | /** 80 | * Get the evaluated contents of the object. 81 | * 82 | * @return string 83 | */ 84 | public function render() : string 85 | { 86 | return $this->translateBindings($this->email->content); 87 | } 88 | 89 | /** 90 | * Get the name of the view. 91 | * 92 | * @return string 93 | */ 94 | public function name() : string 95 | { 96 | return 'laravel-email-templates'; 97 | } 98 | 99 | /** 100 | * @param array|string $key 101 | * @param null $value 102 | * @return StringView 103 | */ 104 | public function with($key, $value = null) : StringView 105 | { 106 | if (is_array($key)) { 107 | $this->data = array_merge($this->data, $key); 108 | } else { 109 | $this->data[$key] = $value; 110 | } 111 | 112 | return $this; 113 | } 114 | 115 | /** 116 | * @param string $target 117 | * @return string 118 | */ 119 | protected function translateBindings(string $target):string 120 | { 121 | $treated = Bindings::normaliseKeys($this->data); 122 | 123 | return str_replace( 124 | array_keys($treated), 125 | array_values($treated), 126 | $target 127 | ); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/TemplateMailable.php: -------------------------------------------------------------------------------- 1 | email = $email; 37 | $this->subject = $email->subject; 38 | $this->viewData = $data + ['subject' => $this->subject]; 39 | $this->stylesheet = $stylesheet; 40 | } 41 | 42 | /** 43 | * @return array 44 | */ 45 | public function build() : array 46 | { 47 | $this->view = [ 48 | 'html' => new StringView($this->email, $this->viewData, $this->stylesheet, true), 49 | 'text' => new StringView($this->email, $this->viewData, $this->stylesheet), 50 | ]; 51 | 52 | return $this->view; 53 | } 54 | 55 | /** 56 | * Apply the view data to the subject of the email. 57 | * 58 | * @param \Illuminate\Mail\Message $message 59 | * @return $this 60 | */ 61 | protected function buildSubject($message) 62 | { 63 | $bindings = Bindings::normaliseKeys($this->viewData); 64 | 65 | $bound = str_replace( 66 | array_keys($bindings), 67 | array_values($bindings), 68 | $this->email->subject 69 | ); 70 | 71 | $message->subject($bound); 72 | 73 | return $this; 74 | } 75 | } 76 | --------------------------------------------------------------------------------