Skip to content

Commit aa22421

Browse files
authored
Merge pull request #125 from magento-hackathon/email-notification
Email notification on error
2 parents e03b798 + dfa4551 commit aa22421

17 files changed

+588
-7
lines changed

Model/ErrorNotification.php

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace EthanYehuda\CronjobManager\Model;
5+
6+
use Magento\Cron\Model\Schedule;
7+
8+
interface ErrorNotification
9+
{
10+
public function sendFor(Schedule $schedule): void;
11+
}

Model/ErrorNotificationEmail.php

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace EthanYehuda\CronjobManager\Model;
5+
6+
use Magento\Cron\Model\Schedule;
7+
use Magento\Framework\App\Area;
8+
use Magento\Framework\App\Config\ScopeConfigInterface;
9+
use Magento\Framework\Mail\Template\SenderResolverInterface;
10+
use Magento\Framework\Mail\Template\TransportBuilder;
11+
use Magento\Store\Model\StoreManagerInterface;
12+
use Psr\Log\LoggerInterface;
13+
14+
class ErrorNotificationEmail implements ErrorNotification
15+
{
16+
private const XML_PATH_EMAIL_ENABLED = 'system/cron_job_manager/email_notification';
17+
private const XML_PATH_EMAIL_TEMPLATE = 'system/cron_job_manager/email_template';
18+
private const XML_PATH_EMAIL_IDENTITY = 'system/cron_job_manager/email_identity';
19+
private const XML_PATH_EMAIL_RECIPIENTS = 'system/cron_job_manager/email_recipients';
20+
21+
/**
22+
* @var TransportBuilder
23+
*/
24+
private $mailTransportBuilder;
25+
/**
26+
* @var StoreManagerInterface
27+
*/
28+
private $storeManager;
29+
/**
30+
* @var ScopeConfigInterface
31+
*/
32+
private $scopeConfig;
33+
/**
34+
* @var SenderResolverInterface
35+
*/
36+
private $senderResolver;
37+
/**
38+
* @var LoggerInterface
39+
*/
40+
private $logger;
41+
42+
/**
43+
* ErrorNotificationEmail constructor.
44+
* @param TransportBuilder $mailTransportBuilder
45+
*/
46+
public function __construct(
47+
TransportBuilder $mailTransportBuilder,
48+
StoreManagerInterface $storeManager,
49+
ScopeConfigInterface $scopeConfig,
50+
SenderResolverInterface $senderResolver,
51+
LoggerInterface $logger
52+
) {
53+
$this->mailTransportBuilder = $mailTransportBuilder;
54+
$this->storeManager = $storeManager;
55+
$this->scopeConfig = $scopeConfig;
56+
$this->senderResolver = $senderResolver;
57+
$this->logger = $logger;
58+
}
59+
60+
public function sendFor(Schedule $schedule): void
61+
{
62+
if (!$this->scopeConfig->isSetFlag(self::XML_PATH_EMAIL_ENABLED)) {
63+
return;
64+
}
65+
try {
66+
$recipients = explode(',', (string)$this->scopeConfig->getValue(self::XML_PATH_EMAIL_RECIPIENTS));
67+
$recipients = array_map('trim', $recipients);
68+
$sender = $this->senderResolver->resolve(
69+
$this->scopeConfig->getValue(self::XML_PATH_EMAIL_IDENTITY)
70+
);
71+
72+
$this->mailTransportBuilder->setTemplateIdentifier($this->scopeConfig->getValue(self::XML_PATH_EMAIL_TEMPLATE));
73+
$this->mailTransportBuilder->setTemplateVars(['schedule' => $schedule]);
74+
$this->mailTransportBuilder->setTemplateOptions(
75+
[
76+
'area' => Area::AREA_ADMINHTML,
77+
'store' => $this->storeManager->getDefaultStoreView()->getId(),
78+
]
79+
);
80+
$this->mailTransportBuilder->setFrom($sender);
81+
$this->mailTransportBuilder->addTo($recipients);
82+
$this->mailTransportBuilder->getTransport()->sendMessage();
83+
} catch (\Exception $e) {
84+
$this->logger->error($e);
85+
}
86+
}
87+
88+
}

Plugin/Cron/Model/SchedulePlugin.php

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace EthanYehuda\CronjobManager\Plugin\Cron\Model;
5+
6+
use Magento\Cron\Model\Schedule;
7+
8+
class SchedulePlugin
9+
{
10+
public function afterTryLockJob(Schedule $subject, bool $result)
11+
{
12+
if ($result) {
13+
$subject->setData('pid', \getmypid());
14+
}
15+
return $result;
16+
}
17+
}

Plugin/Cron/Model/ScheduleResourcePlugin.php

+30
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,38 @@
33

44
namespace EthanYehuda\CronjobManager\Plugin\Cron\Model;
55

6+
use EthanYehuda\CronjobManager\Model\ErrorNotification;
7+
use Magento\Cron\Model\Schedule;
8+
use Magento\Cron\Model\ResourceModel;
9+
610
class ScheduleResourcePlugin
711
{
12+
/**
13+
* @var ErrorNotification
14+
*/
15+
private $errorNotification;
16+
17+
public function __construct(ErrorNotification $errorNotification)
18+
{
19+
$this->errorNotification = $errorNotification;
20+
}
21+
22+
/**
23+
* Email notification if status has been set to ERROR
24+
*/
25+
public function afterSave(
26+
ResourceModel\Schedule $subject,
27+
ResourceModel\Schedule $result,
28+
Schedule $object
29+
) {
30+
if ($object->getOrigData('status') !== $object->getStatus()
31+
&& $object->getStatus() === Schedule::STATUS_ERROR
32+
) {
33+
$this->errorNotification->sendFor($object);
34+
}
35+
return $result;
36+
}
37+
838
/**
939
* Replace method to update pid column together with status column
1040
*

README.md

+5
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,11 @@ For example: `php bin/magento cronmanager:showjobs`
8888

8989
<img src="https://user-images.githubusercontent.com/6549623/39410837-41f1b060-4bcc-11e8-8b98-7d7253662d5c.png"/>
9090

91+
### Email notifications
92+
93+
You can configure email addresses to be notified if a job has an error:
94+
95+
![email-configuration](https://user-images.githubusercontent.com/367320/60760081-a3970000-a02f-11e9-9615-3eb6c3bd9adb.png)
9196

9297
### And Much More...
9398

Test/Integration/CleanRunningJobsTest.php

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
namespace EthanYehuda\CronjobManager\Test\Integration;
55

66
use EthanYehuda\CronjobManager\Model\Clock;
7+
use EthanYehuda\CronjobManager\Model\ErrorNotification;
78
use EthanYehuda\CronjobManager\Test\Util\FakeClock;
89
use Magento\Cron\Model\Schedule;
910
use Magento\Framework\ObjectManager\ObjectManager;
@@ -37,6 +38,7 @@ protected function setUp()
3738
{
3839
$this->objectManager = Bootstrap::getObjectManager();
3940
$this->objectManager->configure(['preferences' => [Clock::class => FakeClock::class]]);
41+
$this->objectManager->addSharedInstance($this->createMock(ErrorNotification::class), ErrorNotification::class);
4042
$this->clock = $this->objectManager->get(Clock::class);
4143
$this->clock->setTimestamp(strtotime(self::NOW));
4244
$this->eventManager = $this->objectManager->get(Event\ManagerInterface::class);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace EthanYehuda\CronjobManager\Test\Integration;
5+
6+
use EthanYehuda\CronjobManager\Model\ErrorNotificationEmail;
7+
use Magento\Cron\Model\Schedule;
8+
use Magento\Framework\Mail\Message;
9+
use Magento\Framework\Mail\Template\TransportBuilder;
10+
use Magento\TestFramework\ObjectManager;
11+
use Magento\TestFramework\Helper\Bootstrap;
12+
use Magento\TestFramework\Mail\Template\TransportBuilderMock;
13+
use PHPUnit\Framework\TestCase;
14+
15+
/**
16+
* @magentoAppArea crontab
17+
* @magentoAppIsolation enabled
18+
*/
19+
class ErrorNotificationEmailTest extends TestCase
20+
{
21+
/**
22+
* @var ObjectManager
23+
*/
24+
private $objectManager;
25+
/**
26+
* @var ErrorNotificationEmail
27+
*/
28+
private $errorNotificationEmail;
29+
/**
30+
* @var \Magento\TestFramework\Mail\Template\TransportBuilderMock
31+
*/
32+
private $transportBuilder;
33+
34+
protected function setUp()
35+
{
36+
$this->objectManager = Bootstrap::getObjectManager();
37+
$this->transportBuilder = $this->objectManager->get(TransportBuilderMock::class);
38+
$this->objectManager->addSharedInstance(
39+
$this->transportBuilder,
40+
TransportBuilder::class
41+
);
42+
$this->errorNotificationEmail = $this->objectManager->get(ErrorNotificationEmail::class);
43+
}
44+
45+
/**
46+
* @magentoConfigFixture default_store system/cron_job_manager/email_notification 0
47+
* @magentoAdminConfigFixture system/cron_job_manager/email_recipients errors@example.com,other@example.com
48+
* @magentoAdminConfigFixture system/cron_job_manager/email_identity general
49+
* @magentoConfigFixture current_store trans_email/ident_general/name No-Reply
50+
* @magentoConfigFixture current_store trans_email/ident_general/email noreply@example.com
51+
*/
52+
public function testDoNotSendIfConfigurationDisabled()
53+
{
54+
$this->givenScheduleWithData(
55+
[
56+
'job_code' => 'dummy_job_code',
57+
'executed_at' => '1999-12-31 23:59:00',
58+
'finished_at' => '1900-01-01 00:00:00',
59+
'status' => Schedule::STATUS_ERROR,
60+
'messages' => "Hello, I am the <Y2K> Bug\n\nHere be stacktrace",
61+
],
62+
$schedule
63+
);
64+
$this->whenNotificationIsSent($schedule, $sentMessage);
65+
$this->thenEmailShouldNotBeSent($sentMessage);
66+
}
67+
68+
/**
69+
* @magentoAdminConfigFixture system/cron_job_manager/email_notification 1
70+
* @magentoAdminConfigFixture system/cron_job_manager/email_recipients errors@example.com,other@example.com
71+
* @magentoAdminConfigFixture system/cron_job_manager/email_identity general
72+
* @magentoConfigFixture current_store trans_email/ident_general/name No-Reply
73+
* @magentoConfigFixture current_store trans_email/ident_general/email noreply@example.com
74+
*/
75+
public function testSentWithTemplateToConfiguredAddresses()
76+
{
77+
$this->givenScheduleWithData(
78+
[
79+
'job_code' => 'dummy_job_code',
80+
'executed_at' => '1999-12-31 23:59:00',
81+
'finished_at' => '1900-01-01 00:00:00',
82+
'status' => Schedule::STATUS_ERROR,
83+
'messages' => "Hello, I am the <Y2K> Bug\n\nHere be stacktrace",
84+
],
85+
$schedule
86+
);
87+
$this->whenNotificationIsSent($schedule, $sentMessage);
88+
$this->thenEmailShouldBeSent($sentMessage, 'noreply@example.com', ['errors@example.com', 'other@example.com']);
89+
$this->andEmailShouldHaveContents(
90+
$sentMessage,
91+
[
92+
'job_code' => '<td>dummy_job_code</td>',
93+
'messages' => "<td>Hello, I am the &lt;Y2K&gt; Bug<br />\n<br />\nHere be stacktrace</td>",
94+
'exeuted_at' => '<td>1999-12-31 23:59:00</td>',
95+
'finished_at' => '<td>1900-01-01 00:00:00</td>',
96+
]
97+
);
98+
}
99+
100+
private function givenScheduleWithData(array $scheduleData, &$schedule): void
101+
{
102+
$schedule = $this->objectManager->create(
103+
Schedule::class,
104+
[
105+
'data' => $scheduleData,
106+
]
107+
);
108+
}
109+
110+
private function whenNotificationIsSent($schedule, &$sentMessage): void
111+
{
112+
$this->errorNotificationEmail->sendFor($schedule);
113+
$sentMessage = $this->transportBuilder->getSentMessage();
114+
}
115+
116+
private function thenEmailShouldBeSent(?Message $sentMessage, string $expectedSender, array $expectedRecipients)
117+
{
118+
$this->assertNotNull($sentMessage, 'A mail should have been sent');
119+
$messageDetails = \Zend\Mail\Message::fromString($sentMessage->getRawMessage());
120+
$this->assertEquals([$expectedSender], \array_keys(\iterator_to_array($messageDetails->getFrom())));
121+
$this->assertEquals($expectedRecipients, \array_keys(\iterator_to_array($messageDetails->getTo())));
122+
}
123+
124+
private function thenEmailShouldNotBeSent(?Message $sentMessage)
125+
{
126+
$this->assertNull($sentMessage, 'A mail should not have been sent');
127+
}
128+
129+
private function andEmailShouldHaveContents(Message $sentMessage, array $expectedContents): void
130+
{
131+
$content = $sentMessage->getBody()->getParts()[0]->getContent();
132+
foreach ($expectedContents as $expectedKey => $expectedContent) {
133+
$this->assertContains($expectedContent, $content, "Content should contain $expectedKey");
134+
}
135+
}
136+
}

0 commit comments

Comments
 (0)