281 lines
7.6 KiB
PHP
281 lines
7.6 KiB
PHP
|
<?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\ErrorHandler\Exception\FlattenException;
|
||
|
use Symfony\Component\Mime\Header\Headers;
|
||
|
use Symfony\Component\Mime\Part\AbstractPart;
|
||
|
use Symfony\Component\Mime\Part\DataPart;
|
||
|
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 string $theme = 'default';
|
||
|
private array $context = [
|
||
|
'importance' => self::IMPORTANCE_LOW,
|
||
|
'content' => '',
|
||
|
'exception' => false,
|
||
|
'action_text' => null,
|
||
|
'action_url' => null,
|
||
|
'markdown' => false,
|
||
|
'raw' => false,
|
||
|
'footer_text' => 'Notification email sent by Symfony',
|
||
|
];
|
||
|
private bool $rendered = false;
|
||
|
|
||
|
public function __construct(?Headers $headers = null, ?AbstractPart $body = null)
|
||
|
{
|
||
|
$missingPackages = [];
|
||
|
if (!class_exists(CssInlinerExtension::class)) {
|
||
|
$missingPackages['twig/cssinliner-extra'] = 'CSS Inliner';
|
||
|
}
|
||
|
|
||
|
if (!class_exists(InkyExtension::class)) {
|
||
|
$missingPackages['twig/inky-extra'] = 'Inky';
|
||
|
}
|
||
|
|
||
|
if ($missingPackages) {
|
||
|
throw new \LogicException(sprintf('You cannot use "%s" if the "%s" Twig extension%s not available. Try running "%s".', static::class, implode('" and "', $missingPackages), \count($missingPackages) > 1 ? 's are' : ' is', 'composer require '.implode(' ', array_keys($missingPackages))));
|
||
|
}
|
||
|
|
||
|
parent::__construct($headers, $body);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Creates a NotificationEmail instance that is appropriate to send to normal (non-admin) users.
|
||
|
*/
|
||
|
public static function asPublicEmail(?Headers $headers = null, ?AbstractPart $body = null): self
|
||
|
{
|
||
|
$email = new static($headers, $body);
|
||
|
$email->markAsPublic();
|
||
|
|
||
|
return $email;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return $this
|
||
|
*/
|
||
|
public function markAsPublic(): static
|
||
|
{
|
||
|
$this->context['importance'] = null;
|
||
|
$this->context['footer_text'] = null;
|
||
|
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return $this
|
||
|
*/
|
||
|
public function markdown(string $content): static
|
||
|
{
|
||
|
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): static
|
||
|
{
|
||
|
$this->context['content'] = $content;
|
||
|
$this->context['raw'] = $raw;
|
||
|
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return $this
|
||
|
*/
|
||
|
public function action(string $text, string $url): static
|
||
|
{
|
||
|
$this->context['action_text'] = $text;
|
||
|
$this->context['action_url'] = $url;
|
||
|
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return $this
|
||
|
*/
|
||
|
public function importance(string $importance): static
|
||
|
{
|
||
|
$this->context['importance'] = $importance;
|
||
|
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return $this
|
||
|
*/
|
||
|
public function exception(\Throwable|FlattenException $exception): static
|
||
|
{
|
||
|
$exceptionAsString = $this->getExceptionAsString($exception);
|
||
|
|
||
|
$this->context['exception'] = true;
|
||
|
$this->addPart(new DataPart($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): static
|
||
|
{
|
||
|
$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';
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return $this
|
||
|
*/
|
||
|
public function context(array $context): static
|
||
|
{
|
||
|
$parentContext = [];
|
||
|
|
||
|
foreach ($context as $key => $value) {
|
||
|
if (\array_key_exists($key, $this->context)) {
|
||
|
$this->context[$key] = $value;
|
||
|
} else {
|
||
|
$parentContext[$key] = $value;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
parent::context($parentContext);
|
||
|
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
public function getContext(): array
|
||
|
{
|
||
|
return array_merge($this->context, parent::getContext());
|
||
|
}
|
||
|
|
||
|
public function isRendered(): bool
|
||
|
{
|
||
|
return $this->rendered;
|
||
|
}
|
||
|
|
||
|
public function markAsRendered(): void
|
||
|
{
|
||
|
parent::markAsRendered();
|
||
|
|
||
|
$this->rendered = true;
|
||
|
}
|
||
|
|
||
|
public function getPreparedHeaders(): Headers
|
||
|
{
|
||
|
$headers = parent::getPreparedHeaders();
|
||
|
|
||
|
$importance = $this->context['importance'] ?? self::IMPORTANCE_LOW;
|
||
|
$this->priority($this->determinePriority($importance));
|
||
|
if ($this->context['importance']) {
|
||
|
$headers->setHeaderBody('Text', 'Subject', sprintf('[%s] %s', strtoupper($importance), $this->getSubject()));
|
||
|
}
|
||
|
|
||
|
return $headers;
|
||
|
}
|
||
|
|
||
|
private function determinePriority(string $importance): int
|
||
|
{
|
||
|
return match ($importance) {
|
||
|
self::IMPORTANCE_URGENT => self::PRIORITY_HIGHEST,
|
||
|
self::IMPORTANCE_HIGH => self::PRIORITY_HIGH,
|
||
|
self::IMPORTANCE_MEDIUM => self::PRIORITY_NORMAL,
|
||
|
default => self::PRIORITY_LOW,
|
||
|
};
|
||
|
}
|
||
|
|
||
|
private function getExceptionAsString(\Throwable|FlattenException $exception): string
|
||
|
{
|
||
|
if (class_exists(FlattenException::class)) {
|
||
|
$exception = $exception instanceof FlattenException ? $exception : FlattenException::createFromThrowable($exception);
|
||
|
|
||
|
return $exception->getAsString();
|
||
|
}
|
||
|
|
||
|
$message = $exception::class;
|
||
|
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, $this->theme, $this->rendered, parent::__serialize()];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @internal
|
||
|
*/
|
||
|
public function __unserialize(array $data): void
|
||
|
{
|
||
|
if (4 === \count($data)) {
|
||
|
[$this->context, $this->theme, $this->rendered, $parentData] = $data;
|
||
|
} elseif (3 === \count($data)) {
|
||
|
[$this->context, $this->theme, $parentData] = $data;
|
||
|
} else {
|
||
|
// Backwards compatibility for deserializing data structures that were serialized without the theme
|
||
|
[$this->context, $parentData] = $data;
|
||
|
}
|
||
|
|
||
|
parent::__unserialize($parentData);
|
||
|
}
|
||
|
}
|