From 4c7a12b0264f5d073343d34ae6d8b59c38304371 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sat, 7 May 2022 13:05:55 +0200 Subject: [PATCH] Add SymfonyMailerHandler, deprecate SwiftMailerHandler (#1663) --- composer.json | 8 +- src/Monolog/Handler/SwiftMailerHandler.php | 3 + src/Monolog/Handler/SymfonyMailerHandler.php | 111 ++++++++++++++++++ .../Handler/SymfonyMailerHandlerTest.php | 100 ++++++++++++++++ 4 files changed, 219 insertions(+), 3 deletions(-) create mode 100644 src/Monolog/Handler/SymfonyMailerHandler.php create mode 100644 tests/Monolog/Handler/SymfonyMailerHandlerTest.php diff --git a/composer.json b/composer.json index b151c654..1c0a5c22 100644 --- a/composer.json +++ b/composer.json @@ -20,17 +20,19 @@ "aws/aws-sdk-php": "^2.4.9 || ^3.0", "doctrine/couchdb": "~1.0@dev", "elasticsearch/elasticsearch": "^7", - "mongodb/mongodb": "^1.8", "graylog2/gelf-php": "^1.4.2", + "mongodb/mongodb": "^1.8", "php-amqplib/php-amqplib": "~2.4 || ^3", "php-console/php-console": "^3.1.3", - "phpspec/prophecy": "^1.6.1", + "phpspec/prophecy": "^1.15", + "phpstan/phpstan": "^0.12.91", "phpunit/phpunit": "^8.5", "predis/predis": "^1.1", "rollbar/rollbar": "^1.3 || ^2 || ^3", "ruflin/elastica": ">=0.90@dev", "swiftmailer/swiftmailer": "^5.3|^6.0", - "phpstan/phpstan": "^0.12.91" + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" }, "suggest": { "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", diff --git a/src/Monolog/Handler/SwiftMailerHandler.php b/src/Monolog/Handler/SwiftMailerHandler.php index be7e2a58..fae92514 100644 --- a/src/Monolog/Handler/SwiftMailerHandler.php +++ b/src/Monolog/Handler/SwiftMailerHandler.php @@ -24,6 +24,7 @@ use Swift; * @author Gyula Sallai * * @phpstan-import-type Record from \Monolog\Logger + * @deprecated Since Monolog 2.6. Use SymfonyMailerHandler instead. */ class SwiftMailerHandler extends MailHandler { @@ -42,6 +43,8 @@ class SwiftMailerHandler extends MailHandler { parent::__construct($level, $bubble); + @trigger_error('The SwiftMailerHandler is deprecated since Monolog 2.6. Use SymfonyMailerHandler instead.', E_USER_DEPRECATED); + $this->mailer = $mailer; $this->messageTemplate = $message; } diff --git a/src/Monolog/Handler/SymfonyMailerHandler.php b/src/Monolog/Handler/SymfonyMailerHandler.php new file mode 100644 index 00000000..130e6f1f --- /dev/null +++ b/src/Monolog/Handler/SymfonyMailerHandler.php @@ -0,0 +1,111 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\Utils; +use Monolog\Formatter\FormatterInterface; +use Monolog\Formatter\LineFormatter; +use Symfony\Component\Mailer\MailerInterface; +use Symfony\Component\Mailer\Transport\TransportInterface; +use Symfony\Component\Mime\Email; + +/** + * SymfonyMailerHandler uses Symfony's Mailer component to send the emails + * + * @author Jordi Boggiano + * + * @phpstan-import-type Record from \Monolog\Logger + */ +class SymfonyMailerHandler extends MailHandler +{ + /** @var MailerInterface|TransportInterface */ + protected $mailer; + /** @var Email|callable(string, Record[]): Email */ + private $emailTemplate; + + /** + * @psalm-param Email|callable(string, Record[]): Email $email + * + * @param MailerInterface|TransportInterface $mailer The mailer to use + * @param callable|Email $email An email template, the subject/body will be replaced + */ + public function __construct($mailer, $email, $level = Logger::ERROR, bool $bubble = true) + { + parent::__construct($level, $bubble); + + $this->mailer = $mailer; + $this->emailTemplate = $email; + } + + /** + * {@inheritDoc} + */ + protected function send(string $content, array $records): void + { + $this->mailer->send($this->buildMessage($content, $records)); + } + + /** + * Gets the formatter for the Swift_Message subject. + * + * @param string|null $format The format of the subject + */ + protected function getSubjectFormatter(?string $format): FormatterInterface + { + return new LineFormatter($format); + } + + /** + * Creates instance of Email to be sent + * + * @param string $content formatted email body to be sent + * @param array $records Log records that formed the content + * + * @phpstan-param Record[] $records + */ + protected function buildMessage(string $content, array $records): Email + { + $message = null; + if ($this->emailTemplate instanceof Email) { + $message = clone $this->emailTemplate; + } elseif (is_callable($this->emailTemplate)) { + $message = ($this->emailTemplate)($content, $records); + } + + if (!$message instanceof Email) { + $record = reset($records); + throw new \InvalidArgumentException('Could not resolve message as instance of Email or a callable returning it' . ($record ? Utils::getRecordMessageForException($record) : '')); + } + + if ($records) { + $subjectFormatter = $this->getSubjectFormatter($message->getSubject()); + $message->subject($subjectFormatter->format($this->getHighestRecord($records))); + } + + if ($this->isHtmlBody($content)) { + if (null !== ($charset = $message->getHtmlCharset())) { + $message->html($content, $charset); + } else { + $message->html($content); + } + } else { + if (null !== ($charset = $message->getTextCharset())) { + $message->text($content, $charset); + } else { + $message->text($content); + } + } + + return $message->date(new \DateTimeImmutable()); + } +} diff --git a/tests/Monolog/Handler/SymfonyMailerHandlerTest.php b/tests/Monolog/Handler/SymfonyMailerHandlerTest.php new file mode 100644 index 00000000..af1e926a --- /dev/null +++ b/tests/Monolog/Handler/SymfonyMailerHandlerTest.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Logger; +use Monolog\Test\TestCase; +use PHPUnit\Framework\MockObject\MockObject; +use Symfony\Component\Mailer\MailerInterface; +use Symfony\Component\Mime\Email; + +class SymfonyMailerHandlerTest extends TestCase +{ + /** @var MailerInterface&MockObject */ + private $mailer; + + public function setUp(): void + { + $this->mailer = $this + ->getMockBuilder(MailerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + } + + 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 SymfonyMailerHandler($this->mailer, $callback); + + $records = [ + $this->getRecord(Logger::DEBUG), + $this->getRecord(Logger::INFO), + ]; + $handler->handleBatch($records); + } + + public function testMessageCanBeCustomizedGivenLoggedData() + { + // Wire Mailer to expect a specific Email with a customized Subject + $expectedMessage = new Email(); + $this->mailer->expects($this->once()) + ->method('send') + ->with($this->callback(function ($value) use ($expectedMessage) { + return $value instanceof Email + && $value->getSubject() === 'Emergency' + && $value === $expectedMessage; + })); + + // Callback dynamically changes subject based on number of logged records + $callback = function ($content, array $records) use ($expectedMessage) { + $subject = count($records) > 0 ? 'Emergency' : 'Normal'; + return $expectedMessage->subject($subject); + }; + $handler = new SymfonyMailerHandler($this->mailer, $callback); + + // Logging 1 record makes this an Emergency + $records = [ + $this->getRecord(Logger::EMERGENCY), + ]; + $handler->handleBatch($records); + } + + public function testMessageSubjectFormatting() + { + // Wire Mailer to expect a specific Email with a customized Subject + $messageTemplate = new Email(); + $messageTemplate->subject('Alert: %level_name% %message%'); + $receivedMessage = null; + + $this->mailer->expects($this->once()) + ->method('send') + ->with($this->callback(function ($value) use (&$receivedMessage) { + $receivedMessage = $value; + + return true; + })); + + $handler = new SymfonyMailerHandler($this->mailer, $messageTemplate); + + $records = [ + $this->getRecord(Logger::EMERGENCY), + ]; + $handler->handleBatch($records); + + $this->assertEquals('Alert: EMERGENCY test', $receivedMessage->getSubject()); + } +}