From 5cf35f67fe9b1fb80ad0f97d7b342bcef92de3cb Mon Sep 17 00:00:00 2001 From: Anton Tuyakhov Date: Thu, 22 Nov 2018 17:00:36 +0100 Subject: [PATCH 1/5] add support for custom data in database notifications --- README.md | 33 ++++++++++-- src/channels/ActiveRecordChannel.php | 2 + src/messages/DatabaseMessage.php | 10 ++++ .../m181112_171335_add_data_column.php | 23 ++++++++ src/models/Notification.php | 39 +++++++++++--- tests/ActiveRecordChannelTest.php | 53 +++++++++++++++++++ tests/TestCase.php | 39 ++++++++++++++ tests/bootstrap.php | 1 + 8 files changed, 189 insertions(+), 11 deletions(-) create mode 100644 src/migrations/m181112_171335_add_data_column.php diff --git a/README.md b/README.md index 1001a1b..034b107 100644 --- a/README.md +++ b/README.md @@ -71,8 +71,8 @@ $notification = new InvoicePaid($invoice); Yii::$app->notifier->send($recipient, $nofitication); ``` -Each notification class should implement NotificationInterface and contains a via method and a variable number of message building methods (such as `exportForMail`) that convert the notification to a message optimized for that particular channel. -Example of notification that covers the case when an invoice has been paid: +Each notification class should implement `NotificationInterface` and contain a `viaChannels` method and a variable number of message building methods (such as `exportForMail`) that convert the notification to a message optimized for that particular channel. +Example of a notification that covers the case when an invoice has been paid: ```php use tuyakhov\notifications\NotificationInterface; @@ -88,7 +88,10 @@ class InvoicePaid implements NotificationInterface { $this->invoice = $invoice; } - + + /** + * Prepares notification for 'mail' channel + */ public function exportForMail() { return Yii::createObject([ 'class' => '\tuyakhov\notifications\messages\MailMessage', @@ -100,6 +103,9 @@ class InvoicePaid implements NotificationInterface ]) } + /** + * Prepares notification for 'sms' channel + */ public function exportForSms() { return \Yii::createObject([ @@ -107,6 +113,21 @@ class InvoicePaid implements NotificationInterface 'text' => "Your invoice #{$this->invoice->id} has been paid" ]); } + + /** + * Prepares notification for 'database' channel + */ + public function exportForDatabase() + { + return \Yii::createObject([ + 'class' => '\tuyakhov\notifications\messages\DatabaseChannel', + 'subject' => "Invoice has been paid", + 'body' => "Your invoice #{$this->invoice->id} has been paid", + 'data' => [ + 'actionUrl' => ['href' => '/invoice/123/view', 'label' => 'View Details'] + ] + ]); + } } ``` @@ -169,6 +190,12 @@ foreach($model->unreadNotifications as $notification) { echo $notification->subject; } ``` +You can access custom JSON data that describes the notification and was added using `DatabaseMessage`: +```php +/** @var $notificatiion tuyakhov\notifications\models\Notificatios */ +$actionUrl = $notification->data('actionUrl'); // ['href' => '/invoice/123/pay', 'label' => 'Pay Invoice'] +``` + **Marking Notifications As Read** Typically, you will want to mark a notification as "read" when a user views it. The `ReadableBehavior` in `Notification` model provides a `markAsRead` method, which updates the read_at column on the notification's database record: ```php diff --git a/src/channels/ActiveRecordChannel.php b/src/channels/ActiveRecordChannel.php index 53b6bbd..1d5fbe5 100644 --- a/src/channels/ActiveRecordChannel.php +++ b/src/channels/ActiveRecordChannel.php @@ -12,6 +12,7 @@ use yii\base\Component; use yii\base\InvalidConfigException; use yii\db\BaseActiveRecord; +use yii\helpers\Json; class ActiveRecordChannel extends Component implements ChannelInterface { @@ -37,6 +38,7 @@ public function send(NotifiableInterface $recipient, NotificationInterface $noti 'body' => $message->body, 'notifiable_type' => $notifiableType, 'notifiable_id' => $notifiableId, + 'data' => Json::encode($message->data), ]; if ($model->load($data, '')) { diff --git a/src/messages/DatabaseMessage.php b/src/messages/DatabaseMessage.php index 81b4ab5..be776f0 100644 --- a/src/messages/DatabaseMessage.php +++ b/src/messages/DatabaseMessage.php @@ -8,4 +8,14 @@ class DatabaseMessage extends AbstractMessage { + /** + * @var array additional data + * Example: + * [ + * 'data' => [ + * 'actionUrl' => ['href' => '/invoice/123/pay', 'label' => 'Pay Invoice'] + * ] + * ] + */ + public $data = []; } \ No newline at end of file diff --git a/src/migrations/m181112_171335_add_data_column.php b/src/migrations/m181112_171335_add_data_column.php new file mode 100644 index 0000000..aa01571 --- /dev/null +++ b/src/migrations/m181112_171335_add_data_column.php @@ -0,0 +1,23 @@ + + */ + +namespace tuyakhov\notifications\migrations; + +use yii\db\Migration; + +class m181112_171335_add_data_column extends Migration +{ + + public function up() + { + $this->addColumn('notification', 'data', $this->text()); + } + + public function down() + { + $this->dropColumn('notification', 'data'); + } + +} \ No newline at end of file diff --git a/src/models/Notification.php b/src/models/Notification.php index ce0bec8..68c4bfd 100644 --- a/src/models/Notification.php +++ b/src/models/Notification.php @@ -5,18 +5,23 @@ use tuyakhov\notifications\behaviors\ReadableBehavior; +use tuyakhov\notifications\messages\DatabaseMessage; use yii\behaviors\TimestampBehavior; use yii\db\ActiveRecord; use yii\db\Expression; +use yii\helpers\ArrayHelper; +use yii\helpers\Json; /** * Database notification model - * @property $level string - * @property $subject string - * @property $notifiable_type string - * @property $notifiable_id int - * @property $body string - * @property $read_at string + * @property string $level + * @property string $subject + * @property string $notifiable_type + * @property int $notifiable_id + * @property string $body + * @property string $data + * @property DatabaseMessage $message + * @property string $read_at * @property $notifiable * @method void markAsRead() * @method void markAsUnread() @@ -32,7 +37,7 @@ class Notification extends ActiveRecord public function rules() { return [ - [['level', 'notifiable_type', 'subject', 'body'], 'string'], + [['level', 'notifiable_type', 'subject', 'body', 'data'], 'string'], ['notifiable_id', 'integer'], ]; } @@ -45,14 +50,32 @@ public function behaviors() return [ [ 'class' => TimestampBehavior::className(), - 'value' => new Expression('NOW()'), + 'value' => new Expression('CURRENT_TIMESTAMP'), ], ReadableBehavior::className() ]; } + /** + * @return \yii\db\ActiveQuery + */ public function getNotifiable() { return $this->hasOne($this->notifiable_type, ['id' => 'notifiable_id']); } + + + /** + * @param null $key + * @return mixed + */ + public function data($key = null) + { + $data = Json::decode($this->data); + if ($key === null) { + return $data; + } + return ArrayHelper::getValue($data, $key); + } + } \ No newline at end of file diff --git a/tests/ActiveRecordChannelTest.php b/tests/ActiveRecordChannelTest.php index b868bdc..878f83a 100644 --- a/tests/ActiveRecordChannelTest.php +++ b/tests/ActiveRecordChannelTest.php @@ -7,6 +7,7 @@ use tuyakhov\notifications\messages\DatabaseMessage; +use tuyakhov\notifications\models\Notification; class ActiveRecordChannelTest extends TestCase { @@ -38,6 +39,7 @@ public function testSend() 'body' => $message->body, 'notifiable_type' => 'yii\base\DynamicModel', 'notifiable_id' => 123, + 'data' => '[]' ], ''); $notificationModel->method('insert')->willReturn(true); $notificationModel->expects($this->once()) @@ -54,4 +56,55 @@ public function testSend() $channel->send($recipient, $notification); } + + public function testWithDB() + { + $recipient = $this->createMock('tuyakhov\notifications\NotifiableInterface'); + $recipient->expects($this->atLeastOnce()) + ->method('routeNotificationFor') + ->with('database') + ->willReturn(['yii\base\DynamicModel', 123]); + + $message = \Yii::createObject([ + 'class' => DatabaseMessage::className(), + 'level' => 'debug', + 'subject' => 'It', + 'body' => 'Works', + ]); + + $channel = \Yii::createObject([ + 'class' => 'tuyakhov\notifications\channels\ActiveRecordChannel', + ]); + + $notification = $this->createMock('tuyakhov\notifications\NotificationInterface'); + $notification->expects($this->once()) + ->method('exportFor') + ->with('database') + ->willReturn($message); + $channel->send($recipient, $notification); + + $this->assertEquals(1, Notification::find()->count()); + + $body = 'Does not work'; + $actionUrl = ['href' => '/invoice/123/pay', 'label' => 'Pay Invoice']; + $differentMessage = \Yii::createObject([ + 'class' => DatabaseMessage::className(), + 'level' => 'error', + 'subject' => 'It', + 'body' => $body, + 'data' => ['actionUrl' => $actionUrl] + ]); + $anotherNotification = $this->createMock('tuyakhov\notifications\NotificationInterface'); + $anotherNotification->expects($this->once()) + ->method('exportFor') + ->with('database') + ->willReturn($differentMessage); + $channel->send($recipient, $anotherNotification); + $this->assertEquals(2, Notification::find()->count()); + /** @var $savedError Notification */ + $this->assertNotEmpty($savedError = Notification::find()->where(['level' => 'error'])->one()); + $this->assertEquals($body, $savedError->body); + $this->assertEquals($actionUrl, $savedError->data('actionUrl')); + $this->assertNull($savedError->data('invalid')); + } } \ No newline at end of file diff --git a/tests/TestCase.php b/tests/TestCase.php index 464c845..8a8117e 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -4,7 +4,46 @@ */ namespace tuyakhov\notifications\tests; +use yii\console\Application; + class TestCase extends \PHPUnit\Framework\TestCase { + protected function setUp() + { + parent::setUp(); + \Yii::$app = new Application([ + 'id' => 'test-app', + 'basePath' => __DIR__, + 'aliases' => [ + '@tuyakhov/notifications/migrations' => dirname(__DIR__) . '/src/migrations', + ], + 'controllerNamespace' => 'yii\console\controllers', + 'components' => [ + 'db' => [ + 'class' => 'yii\db\Connection', + 'dsn' => 'sqlite::memory:' + ] + ], + 'controllerMap' => [ + 'migrate' => [ + 'class' => 'yii\console\controllers\MigrateController', + 'compact' => true, + 'interactive' => false, + 'migrationNamespaces' => [ + 'tuyakhov\notifications\migrations' + ], + ], + ], + ]); + + \Yii::$app->runAction('migrate/fresh'); + } + + protected function tearDown() + { + parent::tearDown(); + \Yii::$app = null; + } + } \ No newline at end of file diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 5c1a05e..91399de 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -4,6 +4,7 @@ define('YII_ENABLE_ERROR_HANDLER', false); define('YII_DEBUG', true); +define('YII_ENV', 'test'); $_SERVER['SCRIPT_NAME'] = '/' . __DIR__; $_SERVER['SCRIPT_FILENAME'] = __FILE__; From 508318303e0d7054ea38b3a65a37d5fc51129f41 Mon Sep 17 00:00:00 2001 From: Liza Date: Wed, 17 Jul 2019 16:19:19 +0300 Subject: [PATCH 2/5] Add telegram channel and message --- src/channels/TelegramChannel.php | 100 +++++++++++++++++++++++++++++++ src/messages/TelegramMessage.php | 7 +++ 2 files changed, 107 insertions(+) create mode 100755 src/channels/TelegramChannel.php create mode 100755 src/messages/TelegramMessage.php diff --git a/src/channels/TelegramChannel.php b/src/channels/TelegramChannel.php new file mode 100755 index 0000000..d2f01f2 --- /dev/null +++ b/src/channels/TelegramChannel.php @@ -0,0 +1,100 @@ +bot_id) || !isset($this->bot_token)){ + throw new InvalidConfigException("Bot id or bot token is undefined"); + } + + if (!isset($this->httpClient)) { + $this->httpClient = [ + 'class' => Client::className(), + ]; + } + $this->httpClient = Instance::ensure($this->httpClient, Client::className()); + } + + /** + * @param NotifiableInterface $recipient + * @param NotificationInterface $notification + * @return mixed + */ + public function send(NotifiableInterface $recipient, NotificationInterface $notification) + { + /** @var TelegramMessage $message */ + $message = $notification->exportFor('telegram'); + $text = "*{$message->subject}*\n{$message->body}"; + $chatId = $recipient->routeNotificationFor('telegram'); + + $data = [ + "chat_id" => $chatId, + "text" => $text, + ]; + if($this->parse_mode != null){ + $data["parse_mode"] = $this->parse_mode; + } + + $resultUrl = $this->createUrl(); + return $this->httpClient->createRequest() + ->setMethod('post') + ->setUrl($resultUrl) + ->setData($data) + ->send(); + } + + private function createUrl() + { + return $this->api_url . $this->bot_id . ":" . $this->bot_token . "/sendmessage"; + } +} \ No newline at end of file diff --git a/src/messages/TelegramMessage.php b/src/messages/TelegramMessage.php new file mode 100755 index 0000000..71a1a7f --- /dev/null +++ b/src/messages/TelegramMessage.php @@ -0,0 +1,7 @@ + Date: Wed, 20 Nov 2019 15:00:50 +0300 Subject: [PATCH 3/5] Add silent mode feature --- src/channels/TelegramChannel.php | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/channels/TelegramChannel.php b/src/channels/TelegramChannel.php index d2f01f2..35e43b3 100755 --- a/src/channels/TelegramChannel.php +++ b/src/channels/TelegramChannel.php @@ -46,6 +46,16 @@ class TelegramChannel extends Component implements ChannelInterface const PARSE_MODE_MARKDOWN = "Markdown"; + /** + * @var bool + * If you need to change silent_mode, you can use this code before calling telegram channel + * + * \Yii::$container->set('\app\additional\notification\TelegramChannel', [ + * 'silent_mode' => true, + * ]); + */ + public $silent_mode = false; + /** * @throws \yii\base\InvalidConfigException */ @@ -65,21 +75,24 @@ public function init() $this->httpClient = Instance::ensure($this->httpClient, Client::className()); } + /** * @param NotifiableInterface $recipient * @param NotificationInterface $notification * @return mixed + * @throws \Exception */ public function send(NotifiableInterface $recipient, NotificationInterface $notification) { /** @var TelegramMessage $message */ $message = $notification->exportFor('telegram'); $text = "*{$message->subject}*\n{$message->body}"; - $chatId = $recipient->routeNotificationFor('telegram'); + $chat_id = $recipient->routeNotificationFor('telegram'); $data = [ - "chat_id" => $chatId, + "chat_id" => $chat_id, "text" => $text, + 'disable_notification' => $this->silent_mode ]; if($this->parse_mode != null){ $data["parse_mode"] = $this->parse_mode; From fa5dbe41b889eb5ae86fdac88728b7b822ee8dc7 Mon Sep 17 00:00:00 2001 From: Liza Date: Wed, 20 Nov 2019 15:01:54 +0300 Subject: [PATCH 4/5] Fix trying to send messages if recipient doesn't have chat_id --- src/channels/TelegramChannel.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/channels/TelegramChannel.php b/src/channels/TelegramChannel.php index 35e43b3..b1aeeaa 100755 --- a/src/channels/TelegramChannel.php +++ b/src/channels/TelegramChannel.php @@ -88,6 +88,9 @@ public function send(NotifiableInterface $recipient, NotificationInterface $noti $message = $notification->exportFor('telegram'); $text = "*{$message->subject}*\n{$message->body}"; $chat_id = $recipient->routeNotificationFor('telegram'); + if(!$chat_id){ + throw new \Exception("User doesn't have telegram_id"); + } $data = [ "chat_id" => $chat_id, From 3bc1bf262e62e34d3cec75d066a76c2555b9611f Mon Sep 17 00:00:00 2001 From: Liza Date: Thu, 21 Nov 2019 11:13:19 +0300 Subject: [PATCH 5/5] Use camelCase and fix small issues --- src/channels/TelegramChannel.php | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/src/channels/TelegramChannel.php b/src/channels/TelegramChannel.php index b1aeeaa..9dc3e41 100755 --- a/src/channels/TelegramChannel.php +++ b/src/channels/TelegramChannel.php @@ -12,11 +12,6 @@ class TelegramChannel extends Component implements ChannelInterface { - /** - * @var BaseActiveRecord|string - */ - public $model = 'app\additional\notification\Notification'; - /** * @var Client|array|string */ @@ -25,7 +20,7 @@ class TelegramChannel extends Component implements ChannelInterface /** * @var string */ - public $api_url = "https://api.telegram.org/bot"; + public $apiUrl = "https://api.telegram.org/"; /** * @var string @@ -35,12 +30,12 @@ class TelegramChannel extends Component implements ChannelInterface /** * @var string */ - public $bot_token; + public $botToken; /** * @var string */ - public $parse_mode = null; + public $parseMode = null; const PARSE_MODE_HTML = "HTML"; @@ -48,13 +43,13 @@ class TelegramChannel extends Component implements ChannelInterface /** * @var bool - * If you need to change silent_mode, you can use this code before calling telegram channel + * If you need to change silentMode, you can use this code before calling telegram channel * * \Yii::$container->set('\app\additional\notification\TelegramChannel', [ - * 'silent_mode' => true, + * 'silentMode' => true, * ]); */ - public $silent_mode = false; + public $silentMode = false; /** * @throws \yii\base\InvalidConfigException @@ -63,13 +58,14 @@ public function init() { parent::init(); - if(!isset($this->bot_id) || !isset($this->bot_token)){ + if(!isset($this->bot_id) || !isset($this->botToken)){ throw new InvalidConfigException("Bot id or bot token is undefined"); } if (!isset($this->httpClient)) { $this->httpClient = [ 'class' => Client::className(), + 'baseUrl' => $this->apiUrl ]; } $this->httpClient = Instance::ensure($this->httpClient, Client::className()); @@ -95,22 +91,21 @@ public function send(NotifiableInterface $recipient, NotificationInterface $noti $data = [ "chat_id" => $chat_id, "text" => $text, - 'disable_notification' => $this->silent_mode + 'disable_notification' => $this->silentMode ]; - if($this->parse_mode != null){ - $data["parse_mode"] = $this->parse_mode; + if($this->parseMode != null){ + $data["parse_mode"] = $this->parseMode; } - $resultUrl = $this->createUrl(); return $this->httpClient->createRequest() ->setMethod('post') - ->setUrl($resultUrl) + ->setUrl($this->createUrl()) ->setData($data) ->send(); } private function createUrl() { - return $this->api_url . $this->bot_id . ":" . $this->bot_token . "/sendmessage"; + return "bot" . $this->bot_id . ":" . $this->botToken . "/sendmessage"; } } \ No newline at end of file