[Twig] Add NotificationEmail
This commit is contained in:
parent
1efae63e64
commit
f6c6cf7dc9
@ -29,6 +29,7 @@ install:
|
||||
- echo max_execution_time=1200 >> php.ini-min
|
||||
- echo date.timezone="America/Los_Angeles" >> php.ini-min
|
||||
- echo extension_dir=ext >> php.ini-min
|
||||
- echo extension=php_xsl.dll >> php.ini-min
|
||||
- copy /Y php.ini-min php.ini-max
|
||||
- echo zend_extension=php_opcache.dll >> php.ini-max
|
||||
- echo opcache.enable_cli=1 >> php.ini-max
|
||||
|
@ -119,7 +119,10 @@
|
||||
"egulias/email-validator": "~1.2,>=1.2.8|~2.0",
|
||||
"symfony/phpunit-bridge": "^3.4.31|^4.3.4|~5.0",
|
||||
"symfony/security-acl": "~2.8|~3.0",
|
||||
"phpdocumentor/reflection-docblock": "^3.0|^4.0"
|
||||
"phpdocumentor/reflection-docblock": "^3.0|^4.0",
|
||||
"twig/cssinliner-extra": "^2.12",
|
||||
"twig/inky-extra": "^2.12",
|
||||
"twig/markdown-extra": "^2.12"
|
||||
},
|
||||
"conflict": {
|
||||
"masterminds/html5": "<2.6",
|
||||
|
@ -47,7 +47,7 @@ final class BodyRenderer implements BodyRendererInterface
|
||||
|
||||
$messageContext = $message->getContext();
|
||||
if (isset($messageContext['email'])) {
|
||||
throw new InvalidArgumentException(sprintf('A "%s" context cannot have an "email" entry as this is a reserved variable.', TemplatedEmail::class));
|
||||
throw new InvalidArgumentException(sprintf('A "%s" context cannot have an "email" entry as this is a reserved variable.', \get_class($message)));
|
||||
}
|
||||
|
||||
$vars = array_merge($this->context, $messageContext, [
|
||||
|
216
src/Symfony/Bridge/Twig/Mime/NotificationEmail.php
Normal file
216
src/Symfony/Bridge/Twig/Mime/NotificationEmail.php
Normal file
@ -0,0 +1,216 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bridge\Twig\Mime;
|
||||
|
||||
use Symfony\Component\ErrorRenderer\Exception\FlattenException;
|
||||
use Symfony\Component\Mime\Header\Headers;
|
||||
use Symfony\Component\Mime\Part\AbstractPart;
|
||||
use Twig\Extra\CssInliner\CssInlinerExtension;
|
||||
use Twig\Extra\Inky\InkyExtension;
|
||||
use Twig\Extra\Markdown\MarkdownExtension;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class NotificationEmail extends TemplatedEmail
|
||||
{
|
||||
public const IMPORTANCE_URGENT = 'urgent';
|
||||
public const IMPORTANCE_HIGH = 'high';
|
||||
public const IMPORTANCE_MEDIUM = 'medium';
|
||||
public const IMPORTANCE_LOW = 'low';
|
||||
|
||||
private $theme = 'default';
|
||||
private $context = [
|
||||
'importance' => self::IMPORTANCE_LOW,
|
||||
'content' => '',
|
||||
'exception' => false,
|
||||
'action_text' => null,
|
||||
'action_url' => null,
|
||||
'markdown' => false,
|
||||
'raw' => false,
|
||||
];
|
||||
|
||||
public function __construct(Headers $headers = null, AbstractPart $body = null)
|
||||
{
|
||||
if (!class_exists(CssInlinerExtension::class)) {
|
||||
throw new \LogicException(sprintf('You cannot use "%s" if the CSS Inliner Twig extension is not available; try running "composer require twig/cssinliner-extra".', static::class));
|
||||
}
|
||||
|
||||
if (!class_exists(InkyExtension::class)) {
|
||||
throw new \LogicException(sprintf('You cannot use "%s" if the Inky Twig extension is not available; try running "composer require twig/inky-extra".', static::class));
|
||||
}
|
||||
|
||||
parent::__construct($headers, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function markdown(string $content)
|
||||
{
|
||||
if (!class_exists(MarkdownExtension::class)) {
|
||||
throw new \LogicException(sprintf('You cannot use "%s" if the Markdown Twig extension is not available; try running "composer require twig/markdown-extra".', __METHOD__));
|
||||
}
|
||||
|
||||
$this->context['markdown'] = true;
|
||||
|
||||
return $this->content($content);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function content(string $content, bool $raw = false)
|
||||
{
|
||||
$this->context['content'] = $content;
|
||||
$this->context['raw'] = $raw;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function action(string $text, string $url)
|
||||
{
|
||||
$this->context['action_text'] = $text;
|
||||
$this->context['action_url'] = $url;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return self
|
||||
*/
|
||||
public function importance(string $importance)
|
||||
{
|
||||
$this->context['importance'] = $importance;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Throwable|FlattenException
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function exception($exception)
|
||||
{
|
||||
$exceptionAsString = $this->getExceptionAsString($exception);
|
||||
|
||||
$this->context['exception'] = true;
|
||||
$this->attach($exceptionAsString, 'exception.txt', 'text/plain');
|
||||
$this->importance(self::IMPORTANCE_URGENT);
|
||||
|
||||
if (!$this->getSubject()) {
|
||||
$this->subject($exception->getMessage());
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function theme(string $theme)
|
||||
{
|
||||
$this->theme = $theme;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTextTemplate(): ?string
|
||||
{
|
||||
if ($template = parent::getTextTemplate()) {
|
||||
return $template;
|
||||
}
|
||||
|
||||
return '@email/'.$this->theme.'/notification/body.txt.twig';
|
||||
}
|
||||
|
||||
public function getHtmlTemplate(): ?string
|
||||
{
|
||||
if ($template = parent::getHtmlTemplate()) {
|
||||
return $template;
|
||||
}
|
||||
|
||||
return '@email/'.$this->theme.'/notification/body.html.twig';
|
||||
}
|
||||
|
||||
public function getContext(): array
|
||||
{
|
||||
return array_merge($this->context, parent::getContext());
|
||||
}
|
||||
|
||||
public function getPreparedHeaders(): Headers
|
||||
{
|
||||
$headers = parent::getPreparedHeaders();
|
||||
|
||||
$importance = $this->context['importance'] ?? IMPORTANCE_LOW;
|
||||
$this->priority($this->determinePriority($importance));
|
||||
$headers->setHeaderBody('Text', 'Subject', sprintf('[%s] %s', strtoupper($importance), $this->getSubject()));
|
||||
|
||||
return $headers;
|
||||
}
|
||||
|
||||
private function determinePriority(string $importance): int
|
||||
{
|
||||
switch ($importance) {
|
||||
case self::IMPORTANCE_URGENT:
|
||||
return self::PRIORITY_HIGHEST;
|
||||
case self::IMPORTANCE_HIGH:
|
||||
return self::PRIORITY_HIGH;
|
||||
case self::IMPORTANCE_MEDIUM:
|
||||
return self::PRIORITY_NORMAL;
|
||||
case self::IMPORTANCE_LOW:
|
||||
default:
|
||||
return self::PRIORITY_LOW;
|
||||
}
|
||||
}
|
||||
|
||||
private function getExceptionAsString($exception): string
|
||||
{
|
||||
if (class_exists(FlattenException::class)) {
|
||||
$exception = $exception instanceof FlattenException ? $exception : FlattenException::createFromThrowable($exception);
|
||||
|
||||
return $exception->getAsString();
|
||||
}
|
||||
|
||||
$message = \get_class($exception);
|
||||
if ('' != $exception->getMessage()) {
|
||||
$message .= ': '.$exception->getMessage();
|
||||
}
|
||||
|
||||
$message .= ' in '.$exception->getFile().':'.$exception->getLine()."\n";
|
||||
$message .= "Stack trace:\n".$exception->getTraceAsString()."\n\n";
|
||||
|
||||
return rtrim($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function __serialize(): array
|
||||
{
|
||||
return [$this->context, parent::__serialize()];
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function __unserialize(array $data): void
|
||||
{
|
||||
[$this->context, $parentData] = $data;
|
||||
|
||||
parent::__unserialize($parentData);
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
{% extends "@email/zurb_2/notification/body.html.twig" %}
|
@ -0,0 +1 @@
|
||||
{% extends "@email/zurb_2/notification/body.txt.twig" %}
|
1667
src/Symfony/Bridge/Twig/Resources/views/Email/zurb_2/main.css
Normal file
1667
src/Symfony/Bridge/Twig/Resources/views/Email/zurb_2/main.css
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,63 @@
|
||||
{% filter inky_to_html|inline_css %}
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
{% block style %}
|
||||
{{ source("@email/zurb_2/main.css") }}
|
||||
{{ source("@email/zurb_2/notification/local.css") }}
|
||||
{% endblock %}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<spacer size="32"></spacer>
|
||||
<container class="body_{{ ("urgent" == importance ? "alert" : ("high" == importance ? "warning" : "default")) }}">
|
||||
<spacer size="16"></spacer>
|
||||
<row>
|
||||
<columns large="12" small="12">
|
||||
{% block lead %}
|
||||
<small><strong>{{ importance|upper }}</strong></small>
|
||||
<p class="lead">
|
||||
{{ email.subject }}
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if markdown %}
|
||||
{{ include('@email/zurb_2/notification/content_markdown.html.twig') }}
|
||||
{% else %}
|
||||
{{ (raw ? content|raw : content)|nl2br }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block action %}
|
||||
{% if action_url %}
|
||||
<spacer size="16"></spacer>
|
||||
<button href="{{ action_url }}">{{ action_text }}</button>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block exception %}
|
||||
{% if exception %}
|
||||
<spacer size="16"></spacer>
|
||||
<p><em>Exception stack trace attached.</em></p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</columns>
|
||||
</row>
|
||||
|
||||
<wrapper class="secondary">
|
||||
<spacer size="16"></spacer>
|
||||
{% block footer %}
|
||||
<row>
|
||||
<columns small="12" large="6">
|
||||
{% block footer_content %}
|
||||
<p><small>Notification e-mail sent by Symfony</small></p>
|
||||
{% endblock %}
|
||||
</columns>
|
||||
</row>
|
||||
{% endblock %}
|
||||
</wrapper>
|
||||
</container>
|
||||
</body>
|
||||
</html>
|
||||
{% endfilter %}
|
@ -0,0 +1,20 @@
|
||||
{% block lead %}
|
||||
{{ email.subject }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ content }}
|
||||
{% endblock %}
|
||||
|
||||
{% block action %}
|
||||
{% if action_url %}
|
||||
{{ action_url }}: {{ action_text }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block exception %}
|
||||
{% if exception %}
|
||||
Exception stack trace attached.
|
||||
{{ exception }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
@ -0,0 +1 @@
|
||||
{{ content|markdown_to_html }}
|
@ -0,0 +1,19 @@
|
||||
body {
|
||||
background: #f3f3f3;
|
||||
}
|
||||
|
||||
.wrapper.secondary {
|
||||
background: #f3f3f3;
|
||||
}
|
||||
|
||||
.container.body_alert {
|
||||
border-top: 8px solid #ec5840;
|
||||
}
|
||||
|
||||
.container.body_warning {
|
||||
border-top: 8px solid #ffae00;
|
||||
}
|
||||
|
||||
.container.body_default {
|
||||
border-top: 8px solid #aaaaaa;
|
||||
}
|
66
src/Symfony/Bridge/Twig/Tests/Mime/NotificationEmailTest.php
Normal file
66
src/Symfony/Bridge/Twig/Tests/Mime/NotificationEmailTest.php
Normal file
@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Bridge\Twig\Tests\Mime;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Bridge\Twig\Mime\NotificationEmail;
|
||||
|
||||
class NotificationEmailTest extends TestCase
|
||||
{
|
||||
public function test()
|
||||
{
|
||||
$email = (new NotificationEmail())
|
||||
->markdown('Foo')
|
||||
->exception(new \Exception())
|
||||
->importance(NotificationEmail::IMPORTANCE_HIGH)
|
||||
->action('Bar', 'http://example.com/')
|
||||
->context(['a' => 'b'])
|
||||
;
|
||||
|
||||
$this->assertEquals([
|
||||
'importance' => NotificationEmail::IMPORTANCE_HIGH,
|
||||
'content' => 'Foo',
|
||||
'exception' => true,
|
||||
'action_text' => 'Bar',
|
||||
'action_url' => 'http://example.com/',
|
||||
'markdown' => true,
|
||||
'raw' => false,
|
||||
'a' => 'b',
|
||||
], $email->getContext());
|
||||
}
|
||||
|
||||
public function testSerialize()
|
||||
{
|
||||
$email = unserialize(serialize((new NotificationEmail())
|
||||
->content('Foo', true)
|
||||
->exception(new \Exception())
|
||||
->importance(NotificationEmail::IMPORTANCE_HIGH)
|
||||
->action('Bar', 'http://example.com/')
|
||||
->context(['a' => 'b'])
|
||||
));
|
||||
$this->assertEquals([
|
||||
'importance' => NotificationEmail::IMPORTANCE_HIGH,
|
||||
'content' => 'Foo',
|
||||
'exception' => true,
|
||||
'action_text' => 'Bar',
|
||||
'action_url' => 'http://example.com/',
|
||||
'markdown' => false,
|
||||
'raw' => true,
|
||||
'a' => 'b',
|
||||
], $email->getContext());
|
||||
}
|
||||
|
||||
public function testTheme()
|
||||
{
|
||||
$email = (new NotificationEmail())->theme('mine');
|
||||
$this->assertSame('@email/mine/notification/body.html.twig', $email->getHtmlTemplate());
|
||||
$this->assertSame('@email/mine/notification/body.txt.twig', $email->getTextTemplate());
|
||||
}
|
||||
|
||||
public function testSubject()
|
||||
{
|
||||
$email = (new NotificationEmail())->from('me@example.com')->subject('Foo');
|
||||
$headers = $email->getPreparedHeaders();
|
||||
$this->assertSame('[LOW] Foo', $headers->get('Subject')->getValue());
|
||||
}
|
||||
}
|
@ -43,7 +43,10 @@
|
||||
"symfony/var-dumper": "^3.4|^4.0|^5.0",
|
||||
"symfony/expression-language": "^3.4|^4.0|^5.0",
|
||||
"symfony/web-link": "^4.4|^5.0",
|
||||
"symfony/workflow": "^4.3|^5.0"
|
||||
"symfony/workflow": "^4.3|^5.0",
|
||||
"twig/cssinliner-extra": "^2.12",
|
||||
"twig/inky-extra": "^2.12",
|
||||
"twig/markdown-extra": "^2.12"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/console": "<3.4",
|
||||
|
@ -40,24 +40,42 @@ class ExtensionPass implements CompilerPassInterface
|
||||
$container->removeDefinition('twig.extension.yaml');
|
||||
}
|
||||
|
||||
$viewDir = \dirname((new \ReflectionClass('Symfony\Bridge\Twig\Extension\FormExtension'))->getFileName(), 2).'/Resources/views';
|
||||
$templateIterator = $container->getDefinition('twig.template_iterator');
|
||||
$templatePaths = $templateIterator->getArgument(2);
|
||||
$cacheWarmer = null;
|
||||
if ($container->hasDefinition('twig.cache_warmer')) {
|
||||
$cacheWarmer = $container->getDefinition('twig.cache_warmer');
|
||||
$cacheWarmerPaths = $cacheWarmer->getArgument(2);
|
||||
}
|
||||
$loader = $container->getDefinition('twig.loader.native_filesystem');
|
||||
|
||||
if ($container->has('mailer')) {
|
||||
$emailPath = $viewDir.'/Email';
|
||||
$loader->addMethodCall('addPath', [$emailPath, 'email']);
|
||||
$loader->addMethodCall('addPath', [$emailPath, '!email']);
|
||||
$templatePaths[$emailPath] = 'email';
|
||||
if ($cacheWarmer) {
|
||||
$cacheWarmerPaths[$emailPath] = 'email';
|
||||
}
|
||||
}
|
||||
|
||||
if ($container->has('form.extension')) {
|
||||
$container->getDefinition('twig.extension.form')->addTag('twig.extension');
|
||||
$reflClass = new \ReflectionClass('Symfony\Bridge\Twig\Extension\FormExtension');
|
||||
|
||||
$coreThemePath = \dirname($reflClass->getFileName(), 2).'/Resources/views/Form';
|
||||
$container->getDefinition('twig.loader.native_filesystem')->addMethodCall('addPath', [$coreThemePath]);
|
||||
|
||||
$paths = $container->getDefinition('twig.template_iterator')->getArgument(2);
|
||||
$paths[$coreThemePath] = null;
|
||||
$container->getDefinition('twig.template_iterator')->replaceArgument(2, $paths);
|
||||
|
||||
if ($container->hasDefinition('twig.cache_warmer')) {
|
||||
$paths = $container->getDefinition('twig.cache_warmer')->getArgument(2);
|
||||
$paths[$coreThemePath] = null;
|
||||
$container->getDefinition('twig.cache_warmer')->replaceArgument(2, $paths);
|
||||
$coreThemePath = $viewDir.'/Form';
|
||||
$loader->addMethodCall('addPath', [$coreThemePath]);
|
||||
$templatePaths[$coreThemePath] = null;
|
||||
if ($cacheWarmer) {
|
||||
$cacheWarmerPaths[$coreThemePath] = null;
|
||||
}
|
||||
}
|
||||
|
||||
$templateIterator->replaceArgument(2, $templatePaths);
|
||||
if ($cacheWarmer) {
|
||||
$container->getDefinition('twig.cache_warmer')->replaceArgument(2, $cacheWarmerPaths);
|
||||
}
|
||||
|
||||
if ($container->has('router')) {
|
||||
$container->getDefinition('twig.extension.routing')->addTag('twig.extension');
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ namespace Symfony\Bundle\TwigBundle\Tests\DependencyInjection\Compiler;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Bundle\TwigBundle\DependencyInjection\Compiler\ExtensionPass;
|
||||
use Symfony\Bundle\TwigBundle\TemplateIterator;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Definition;
|
||||
|
||||
@ -38,6 +39,9 @@ class ExtensionPassTest extends TestCase
|
||||
$filesystemLoader->addMethodCall('addPath', []);
|
||||
$container->setDefinition('twig.loader.filesystem', $filesystemLoader);
|
||||
|
||||
$templateIterator = new Definition(TemplateIterator::class, [null, null, null]);
|
||||
$container->setDefinition('twig.template_iterator', $templateIterator);
|
||||
|
||||
$extensionPass = new ExtensionPass();
|
||||
$extensionPass->process($container);
|
||||
|
||||
|
Reference in New Issue
Block a user