diff --git a/src/Symfony/Bridge/Monolog/CHANGELOG.md b/src/Symfony/Bridge/Monolog/CHANGELOG.md index de5980cddaf0..2a4d31a2ab34 100644 --- a/src/Symfony/Bridge/Monolog/CHANGELOG.md +++ b/src/Symfony/Bridge/Monolog/CHANGELOG.md @@ -1,6 +1,10 @@ CHANGELOG ========= +5.1.0 +----- + * Added `MailerHandler` + 5.0.0 ----- diff --git a/src/Symfony/Bridge/Monolog/Handler/MailerHandler.php b/src/Symfony/Bridge/Monolog/Handler/MailerHandler.php new file mode 100644 index 000000000000..1970d7085f0a --- /dev/null +++ b/src/Symfony/Bridge/Monolog/Handler/MailerHandler.php @@ -0,0 +1,143 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Monolog\Handler; + +use Monolog\Formatter\FormatterInterface; +use Monolog\Formatter\HtmlFormatter; +use Monolog\Formatter\LineFormatter; +use Monolog\Handler\AbstractProcessingHandler; +use Monolog\Logger; +use Symfony\Component\Mailer\MailerInterface; +use Symfony\Component\Mime\Email; + +/** + * @author Alexander Borisov + */ +class MailerHandler extends AbstractProcessingHandler +{ + private $mailer; + + private $messageTemplate; + + /** + * @param callable|Email $messageTemplate + */ + public function __construct(MailerInterface $mailer, $messageTemplate, int $level = Logger::DEBUG, bool $bubble = true) + { + parent::__construct($level, $bubble); + + $this->mailer = $mailer; + $this->messageTemplate = $messageTemplate; + } + + /** + * {@inheritdoc} + */ + public function handleBatch(array $records): void + { + $messages = []; + + foreach ($records as $record) { + if ($record['level'] < $this->level) { + continue; + } + $messages[] = $this->processRecord($record); + } + + if (!empty($messages)) { + $this->send((string) $this->getFormatter()->formatBatch($messages), $messages); + } + } + + /** + * {@inheritdoc} + */ + protected function write(array $record): void + { + $this->send((string) $record['formatted'], [$record]); + } + + /** + * Send a mail with the given content. + * + * @param string $content formatted email body to be sent + * @param array $records the array of log records that formed this content + */ + protected function send(string $content, array $records) + { + $this->mailer->send($this->buildMessage($content, $records)); + } + + /** + * Gets the formatter for the Message subject. + * + * @param string $format The format of the subject + */ + protected function getSubjectFormatter(string $format): FormatterInterface + { + return new LineFormatter($format); + } + + /** + * Creates instance of Message to be sent. + * + * @param string $content formatted email body to be sent + * @param array $records Log records that formed the content + */ + protected function buildMessage(string $content, array $records): Email + { + $message = null; + if ($this->messageTemplate instanceof Email) { + $message = clone $this->messageTemplate; + } elseif (\is_callable($this->messageTemplate)) { + $message = \call_user_func($this->messageTemplate, $content, $records); + if (!$message instanceof Email) { + throw new \InvalidArgumentException(sprintf('Could not resolve message from a callable. Instance of "%s" is expected', Email::class)); + } + } else { + throw new \InvalidArgumentException('Could not resolve message as instance of Email or a callable returning it'); + } + + if ($records) { + $subjectFormatter = $this->getSubjectFormatter($message->getSubject()); + $message->subject($subjectFormatter->format($this->getHighestRecord($records))); + } + + if ($this->getFormatter() instanceof HtmlFormatter) { + if ($message->getHtmlCharset()) { + $message->html($content, $message->getHtmlCharset()); + } else { + $message->html($content); + } + } else { + if ($message->getTextCharset()) { + $message->text($content, $message->getTextCharset()); + } else { + $message->text($content); + } + } + + return $message; + } + + protected function getHighestRecord(array $records): array + { + $highestRecord = null; + foreach ($records as $record) { + if (null === $highestRecord || $highestRecord['level'] < $record['level']) { + $highestRecord = $record; + } + } + + return $highestRecord; + } +} diff --git a/src/Symfony/Bridge/Monolog/Tests/Handler/MailerHandlerTest.php b/src/Symfony/Bridge/Monolog/Tests/Handler/MailerHandlerTest.php new file mode 100644 index 000000000000..24aaa6b95cdd --- /dev/null +++ b/src/Symfony/Bridge/Monolog/Tests/Handler/MailerHandlerTest.php @@ -0,0 +1,123 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Monolog\Tests\Handler; + +use Monolog\Formatter\HtmlFormatter; +use Monolog\Formatter\LineFormatter; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Monolog\Handler\MailerHandler; +use Symfony\Bridge\Monolog\Logger; +use Symfony\Component\Mailer\MailerInterface; +use Symfony\Component\Mime\Email; + +class MailerHandlerTest extends TestCase +{ + /** @var MockObject|MailerInterface */ + private $mailer = null; + + protected function setUp(): void + { + $this->mailer = $this->createMock(MailerInterface::class); + } + + public function testHandle() + { + $handler = new MailerHandler($this->mailer, (new Email())->subject('Alert: %level_name% %message%')); + $handler->setFormatter(new LineFormatter()); + $this->mailer + ->expects($this->once()) + ->method('send') + ->with($this->callback(function (Email $email) { + return 'Alert: WARNING message' === $email->getSubject() && null === $email->getHtmlBody(); + })) + ; + $handler->handle($this->getRecord(Logger::WARNING, 'message')); + } + + public function testHandleBatch() + { + $handler = new MailerHandler($this->mailer, (new Email())->subject('Alert: %level_name% %message%')); + $handler->setFormatter(new LineFormatter()); + $this->mailer + ->expects($this->once()) + ->method('send') + ->with($this->callback(function (Email $email) { + return 'Alert: ERROR error' === $email->getSubject() && null === $email->getHtmlBody(); + })) + ; + $handler->handleBatch($this->getMultipleRecords()); + } + + public function testMessageCreationIsLazyWhenUsingCallback() + { + $this->mailer + ->expects($this->never()) + ->method('send') + ; + + $callback = function () { + throw new \RuntimeException('Email creation callback should not have been called in this test'); + }; + $handler = new MailerHandler($this->mailer, $callback, Logger::ALERT); + + $records = [ + $this->getRecord(Logger::DEBUG), + $this->getRecord(Logger::INFO), + ]; + $handler->handleBatch($records); + } + + public function testHtmlContent() + { + $handler = new MailerHandler($this->mailer, (new Email())->subject('Alert: %level_name% %message%')); + $handler->setFormatter(new HtmlFormatter()); + $this->mailer + ->expects($this->once()) + ->method('send') + ->with($this->callback(function (Email $email) { + return 'Alert: WARNING message' === $email->getSubject() && null === $email->getTextBody(); + })) + ; + $handler->handle($this->getRecord(Logger::WARNING, 'message')); + } + + /** + * @return array Record + */ + protected function getRecord($level = Logger::WARNING, $message = 'test', $context = []) + { + return [ + 'message' => $message, + 'context' => $context, + 'level' => $level, + 'level_name' => Logger::getLevelName($level), + 'channel' => 'test', + 'datetime' => \DateTime::createFromFormat('U.u', sprintf('%.6F', microtime(true))), + 'extra' => [], + ]; + } + + /** + * @return array + */ + protected function getMultipleRecords() + { + return [ + $this->getRecord(Logger::DEBUG, 'debug message 1'), + $this->getRecord(Logger::DEBUG, 'debug message 2'), + $this->getRecord(Logger::INFO, 'information'), + $this->getRecord(Logger::WARNING, 'warning'), + $this->getRecord(Logger::ERROR, 'error'), + ]; + } +} diff --git a/src/Symfony/Bridge/Monolog/composer.json b/src/Symfony/Bridge/Monolog/composer.json index 6a0777aac13b..e3c0874f929b 100644 --- a/src/Symfony/Bridge/Monolog/composer.json +++ b/src/Symfony/Bridge/Monolog/composer.json @@ -25,7 +25,9 @@ "symfony/console": "^4.4|^5.0", "symfony/http-client": "^4.4|^5.0", "symfony/security-core": "^4.4|^5.0", - "symfony/var-dumper": "^4.4|^5.0" + "symfony/var-dumper": "^4.4|^5.0", + "symfony/mailer": "^4.4|^5.0", + "symfony/mime": "^4.4|^5.0" }, "conflict": { "symfony/console": "<4.4",