├── assets
├── img
│ └── sprite.png
├── css
│ └── webnotification.min.css
└── js
│ ├── jquery.webnotification.min.js
│ ├── main.js
│ └── jquery.webnotification.js
├── widgets
├── NfyAsset.php
├── WebNotifications.php
└── Messages.php
├── views
└── queue
│ ├── _queue_messages.php
│ ├── _message_item.php
│ ├── index.php
│ ├── _message_form.php
│ ├── messages.php
│ ├── subscription.php
│ ├── _queue_subscriptions.php
│ └── message.php
├── migrations
├── m140220_104548_nfy_set_unique_categories.php
└── m130713_201034_notifications_install.php
├── messages
├── config.php
└── pl
│ ├── auth.php
│ └── app.php
├── composer.json
├── models
├── MessageForm.php
├── SubscriptionForm.php
├── DbSubscriptionCategory.php
├── DbSubscriptionQuery.php
├── DbSubscription.php
├── DbMessage.php
└── DbMessageQuery.php
├── components
├── SubscribedRule.php
├── QueueEvent.php
├── Queue.php
├── Message.php
├── Subscription.php
├── MailQueue.php
├── QueueInterface.php
├── SysVQueue.php
├── RedisQueue.php
└── DbQueue.php
├── LICENSE
├── Module.php
├── README.md
├── commands
└── NfyController.php
└── controllers
└── QueueController.php
/assets/img/sprite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nineinchnick/yii2-nfy/HEAD/assets/img/sprite.png
--------------------------------------------------------------------------------
/widgets/NfyAsset.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class NfyAsset extends AssetBundle
11 | {
12 | public $sourcePath = '@vendor/nineinchnick/yii2-nfy/assets';
13 | public $baseUrl = '@web';
14 | public $css = [
15 | 'css/webnotification.min.css',
16 | ];
17 | public $js = [
18 | 'js/jquery.webnotification.js',
19 | 'js/main.js',
20 | ];
21 | public $depends = [
22 | ];
23 | }
24 |
--------------------------------------------------------------------------------
/views/queue/_queue_messages.php:
--------------------------------------------------------------------------------
1 |
12 |
13 |
= Html::encode($model->label); ?> = Html::a(Yii::t('app', 'View messages'), ['messages', 'queue_name' => $key, 'subscriber_id' => Yii::$app->user->getId()])?>
14 |
15 |
16 |
--------------------------------------------------------------------------------
/migrations/m140220_104548_nfy_set_unique_categories.php:
--------------------------------------------------------------------------------
1 | db->tablePrefix;
10 | $this->createIndex($prefix.'nfy_subscription_categories_unique_idx', '{{%nfy_subscription_categories}}', 'subscription_id, category, is_exception', true);
11 | }
12 |
13 | public function safeDown()
14 | {
15 | $prefix = $this->db->tablePrefix;
16 | $this->dropIndex($prefix.'nfy_subscription_categories_unique_idx', '{{%nfy_subscription_categories}}');
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/messages/config.php:
--------------------------------------------------------------------------------
1 | dirname(__FILE__).DIRECTORY_SEPARATOR.'..',
8 | 'messagePath' => dirname(__FILE__),
9 | 'languages' => ['pl'],
10 | 'fileTypes' => ['php'],
11 | 'overwrite' => true,
12 | 'exclude' => [
13 | '.svn',
14 | '.git',
15 | '.gitignore',
16 | 'yiilite.php',
17 | 'yiit.php',
18 | 'yiic.php',
19 | '/messages',
20 | '/tests',
21 | '/migrations',
22 | '/extensions',
23 | ],
24 | ];
25 |
--------------------------------------------------------------------------------
/views/queue/_message_item.php:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
">
14 | created_on; ?>
15 | id), $this->context->createMessageUrl($queue_name, $model)); ?>
16 | body; ?>
17 |
18 |
19 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nineinchnick/yii2-nfy",
3 | "description": "A generic message queue interface with various implementations for the Yii 2.0 framework. Basic interface included.",
4 | "license": "MIT",
5 | "type": "yii2-module",
6 | "authors": [
7 | {
8 | "name": "Jan Waś",
9 | "email": "janek.jan@gmail.com",
10 | "homepage": "http://niix.pl/"
11 | }
12 | ],
13 | "require": {
14 | "yiisoft/yii2": "*",
15 | "yiisoft/yii2-bootstrap": "*"
16 | },
17 | "require-dev": {
18 | "phpunit/phpunit": "3.8.*"
19 | },
20 | "autoload": {
21 | "psr-4": {
22 | "nineinchnick\\nfy\\": ""
23 | }
24 | },
25 | "suggest": {
26 | "yiisoft/yii2-redis": "*"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/views/queue/index.php:
--------------------------------------------------------------------------------
1 | title = Yii::t('app', 'Queues');
10 | $this->params['breadcrumbs'][] = $this->title;
11 | ?>
12 |
13 | = Html::encode($this->title) ?>
14 |
15 |
16 | new yii\data\ArrayDataProvider([
18 | 'allModels' => $queues,
19 | 'key' => function ($q) {return $q->id;},
20 | 'pagination' => false,
21 | 'sort' => ['attributes' => ['label']],
22 | ]),
23 | 'itemView' => $subscribedOnly ? '_queue_messages' : '_queue_subscriptions',
24 | 'itemOptions' => ['class' => 'item'],
25 | ]); ?>
26 |
27 |
--------------------------------------------------------------------------------
/models/MessageForm.php:
--------------------------------------------------------------------------------
1 | 'trim'],
20 | [['content', 'category'], 'default'],
21 | ['content', 'required'],
22 | ];
23 | }
24 |
25 | public function attributeLabels()
26 | {
27 | return [
28 | 'content' => Yii::t('app', 'Message content'),
29 | 'category' => Yii::t('app', 'Message category'),
30 | ];
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/views/queue/_message_form.php:
--------------------------------------------------------------------------------
1 |
12 |
36 |
--------------------------------------------------------------------------------
/components/SubscribedRule.php:
--------------------------------------------------------------------------------
1 | isSubscribed(Yii::$app->user->id);
29 | }
30 | }
--------------------------------------------------------------------------------
/components/QueueEvent.php:
--------------------------------------------------------------------------------
1 | title = Yii::t('app', 'Queues');
11 | $this->params['breadcrumbs'][] = ['label' => $this->title, 'url' => ['index']];
12 | $this->params['breadcrumbs'][] = $queue->label;
13 | ?>
14 | label; ?>
15 |
16 |
17 |
18 | $dataProvider,
20 | 'itemView' => '_message_item',
21 | 'viewParams' => ['queue_name' => $queue_name],
22 | 'layout' => "{summary}\n{pager}\n{items}",
23 | 'pager' => [
24 | 'class' => 'yii\widgets\LinkPager',
25 | 'prevPageLabel' => Yii::t('app', 'Newer'),
26 | 'nextPageLabel' => Yii::t('app', 'Older'),
27 | ],
28 | ]); ?>
29 |
30 |
31 |
32 |
33 | render('_message_form', ['model' => $model]); ?>
34 |
35 |
--------------------------------------------------------------------------------
/views/queue/subscription.php:
--------------------------------------------------------------------------------
1 | params['breadcrumbs'][] = ['label' => Yii::t('app', 'Queues'), 'url' => ['index']];
13 | $this->params['breadcrumbs'][] = $queue->label;
14 | ?>
15 | label; ?>
16 |
41 |
--------------------------------------------------------------------------------
/models/SubscriptionForm.php:
--------------------------------------------------------------------------------
1 | 'trim'],
21 | [['label', 'categories', 'exceptions'], 'default'],
22 | [['categories', 'exceptions'], 'prepare'],
23 | ];
24 | }
25 |
26 | public function prepare($attribute, $params)
27 | {
28 | if ($this->$attribute === null) {
29 | return true;
30 | }
31 |
32 | $values = array_map(function ($v) {return trim($v);}, explode(',', $this->$attribute));
33 | if (!empty($values)) {
34 | $this->$attribute = $values;
35 | }
36 |
37 | return true;
38 | }
39 |
40 | public function attributeLabels()
41 | {
42 | return [
43 | 'label' => Yii::t('app', 'Label'),
44 | 'categories' => Yii::t('app', 'Categories'),
45 | 'exceptions' => Yii::t('app', 'Exceptions'),
46 | ];
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/messages/pl/auth.php:
--------------------------------------------------------------------------------
1 | 'Wypisz się z kolejki',
21 | 'Read any queue' => 'Czytaj dowolną kolejkę',
22 | 'Read messages from any queue' => 'Czytaj wiadomości z dowolnej kolejki',
23 | 'Read messages from subscribed queue' => 'Czytaj wiadomości z subskrybowanych kolejek',
24 | 'Read subscribed queue' => 'Czytaj subskrybowaną kolejkę',
25 | 'Send messages to any queue' => 'Wysyłaj wiadomości do dowolnej kolejki',
26 | 'Send messages to subscribed queue' => 'Wysyłaj wiadomości do subskrybowanych kolejek',
27 | 'Subscribe to any queue' => 'Zapisz się do dowolnej kolejki',
28 | ];
29 |
--------------------------------------------------------------------------------
/Module.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class Module extends \yii\base\Module
11 | {
12 | public $defaultRoute = 'queue';
13 | /**
14 | * @var string Name of user model class.
15 | */
16 | public $userClass = 'app\models\User';
17 | /**
18 | * @var string if not null a sound will be played along with displaying a notification
19 | */
20 | public $soundUrl;
21 | /**
22 | * @var integer how many milliseconds to wait for new messages on the server side;
23 | * zero or null disables long polling
24 | */
25 | public $longPolling = 1000;
26 | /**
27 | * @var integer how many times can messages be polled in a single action call
28 | */
29 | public $maxPollCount = 30;
30 | /**
31 | * @var array list of queue application components that will be displayed in the index action of the default controller.
32 | */
33 | public $queues = [];
34 |
35 | public function init()
36 | {
37 | parent::init();
38 | Yii::setAlias('@nfy', dirname(__FILE__));
39 | Yii::$app->i18n->translations['nfy'] = \Yii::$app->i18n->translations['auth'] = [
40 | 'class' => 'yii\i18n\PhpMessageSource',
41 | 'sourceLanguage' => 'en-US',
42 | 'basePath' => '@nfy/messages',
43 | ];
44 | if (Yii::$app instanceof \yii\console\Application) {
45 | $this->controllerMap['nfy'] = 'nineinchnick\nfy\commands\NfyController';
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/views/queue/_queue_subscriptions.php:
--------------------------------------------------------------------------------
1 | getSubscriptions();
16 | } catch (CException $e) {
17 | $supportSubscriptions = false;
18 | }
19 | ?>
20 |
21 | = Html::encode($model->label); ?> = Html::a(Yii::t('app', 'View all messages'), ['messages', 'queue_name' => $key])?>
22 |
23 |
24 | = Html::a(Yii::t('app', 'Subscribe'), ['subscribe', 'queue_name' => $key]) ?> /
25 | = Html::a(Yii::t('app', 'Unsubscribe'), ['unsubscribe', 'queue_name' => $key]) ?>
26 |
27 |
28 |
29 |
30 | :
31 |
32 |
33 |
34 |
35 | = Html::a(
36 | Html::encode($subscription->label),
37 | ['messages', 'queue_name' => $key, 'subscriber_id' => $subscription->subscriber_id],
38 | ['title' => implode("\n", $subscription->categories)]
39 | ) ?>
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/models/DbSubscriptionCategory.php:
--------------------------------------------------------------------------------
1 | 'search'],
35 | ['subscription_id', 'number', 'integerOnly' => true],
36 | ['is_exception', 'boolean'],
37 | ];
38 | }
39 |
40 | public function getSubscription()
41 | {
42 | return $this->hasOne(DbSubscription::className(), ['subscription_id' => 'id']);
43 | }
44 |
45 | /**
46 | * @return array customized attribute labels (name=>label)
47 | */
48 | public function attributeLabels()
49 | {
50 | return [
51 | 'id' => Yii::t('models', 'ID'),
52 | 'subscription_id' => Yii::t('models', 'Subscription ID'),
53 | 'category' => Yii::t('models', 'Category'),
54 | 'is_exception' => Yii::t('models', 'Is Exception'),
55 | ];
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/assets/css/webnotification.min.css:
--------------------------------------------------------------------------------
1 | .wn-container{font-family:arial;position:fixed;z-index:999999}.wn-container .clear{clear:both}.wn-box{background:#FFF;border:1px solid #999;border-radius:6px;box-shadow:0px 2px 3px 0px #666;cursor:default;margin:0 0 10px;opacity:0;overflow:hidden;padding:5px;width:270px}.wn-container.top{top:10px}.wn-container.right{right:2px}.wn-container.bottom{bottom:10px}.wn-container.left{left:2px}.wn-box .wn-head{border-bottom:1px solid #999;color:#999;font-size:12px;margin-left:-10px;padding:0 10px 2px;width:270px}.wn-box .wn-close{background:url("../img/sprite.png") no-repeat top left;background-position:3px 3px;cursor:pointer;display:inline-block;height:14px;padding:0;text-align:center;width:14px}.wn-head .wn-close{border-radius:25px;float:right;opacity:.9;margin:-2px 0 0}.wn-head .wn-close:hover{border:1px solid red;background-color:#F00;background-position:-10px 3px;opacity:.6}.wn-box table .wn-close{background-position:3px 3px;height:14px;width:14px}.wn-box table .wn-close:hover{background-position:-10px 3px}.wn-box .wn-body{clear:both;margin:10px 0 0}.wn-box table,.wn-box .wn-body.hidden{opacity:0}.wn-box .wn-body img{height:32px;width:32px}.wn-box .wn-body img+.wn-message.rtl{float:right;width:230px}.wn-box .wn-body img+.wn-message.ltr{float:left;width:230px}.wn-box .wn-message h5{font-size:14px;margin:0}.wn-box .wn-message p{font-size:14px;margin:5px 0 0}.wn-box .wn-message a{color:#3287D0}.wn-box table.wn-sep{margin-left:-10px;text-align:center;width:300px}.wn-box table hr{border:0;border-bottom:1px solid #999}.wn-box .circle{width:10px}.wn-box .circle span{background-color:transparent;border:1px solid #999;border-radius:40px;display:inline-block;position:relative;top:1px}.wn-box .circle span:hover{border:1px solid red;background-color:#F00;opacity:.6}
2 |
--------------------------------------------------------------------------------
/components/Queue.php:
--------------------------------------------------------------------------------
1 | $message]);
35 | $this->trigger(self::EVENT_BEFORE_SEND, $event);
36 |
37 | return $event->isValid;
38 | }
39 | /**
40 | * @inheritdoc
41 | */
42 | public function afterSend($message)
43 | {
44 | $this->trigger(self::EVENT_AFTER_SEND, new QueueEvent(['message' => $message]));
45 | }
46 | /**
47 | * @inheritdoc
48 | */
49 | public function beforeSendSubscription($message, $subscriber_id)
50 | {
51 | $event = new QueueEvent(['message' => $message, 'subscriber_id' => $subscriber_id]);
52 | $this->trigger(self::EVENT_BEFORE_SEND_SUBSCRIPTION, $event);
53 |
54 | return $event->isValid;
55 | }
56 | /**
57 | * @inheritdoc
58 | */
59 | public function afterSendSubscription($message, $subscriber_id)
60 | {
61 | $this->trigger(self::EVENT_AFTER_SEND_SUBSCRIPTION, new QueueEvent(['message' => $message, 'subscriber_id' => $subscriber_id]));
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/widgets/WebNotifications.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | class WebNotifications extends Widget
16 | {
17 | const METHOD_POLL = 'poll';
18 | const METHOD_PUSH = 'push';
19 |
20 | /**
21 | * @var string url of an ajax action or a websocket
22 | */
23 | public $url;
24 | /**
25 | * @var string poll for ajax polling, push for websockets
26 | */
27 | public $method = self::METHOD_POLL;
28 | /**
29 | * @var integer interval in miliseconds how often a new request is fired in poll mode
30 | */
31 | public $pollInterval = 3000;
32 | /**
33 | * @var array holds websocket JS callbacks as strings prefixed with js:, possible keys are:
34 | * onopen, onclose, onmessage, onerror. Callbacks shoould be a function returning a function, like:
35 | * 'js:function (socket) {return function (e) {console.log(e);};}'
36 | */
37 | public $websocket = [];
38 |
39 | /**
40 | * Registers required JS libraries and CSS files.
41 | * @param \yii\web\View $view
42 | * @param string $method use either METHOD_POLL or METHOD_PULL constants
43 | * @return string base URL for assets
44 | */
45 | public static function initClientScript($view, $method = self::METHOD_POLL)
46 | {
47 | $asset = NfyAsset::register($view);
48 | return $asset->baseUrl;
49 | }
50 |
51 | public function run()
52 | {
53 | $baseUrl = self::initClientScript($this->view, $this->method);
54 | $options = [
55 | 'url' => $this->url,
56 | 'baseUrl' => $baseUrl,
57 | 'method' => $this->method,
58 | 'pollInterval' => $this->pollInterval,
59 | 'websocket' => $this->websocket,
60 | ];
61 | $options = \yii\helpers\Json::encode($options);
62 | $script = "notificationsPoller.init({$options});";
63 | $this->view->registerJs($script, View::POS_END);
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/views/queue/message.php:
--------------------------------------------------------------------------------
1 | params['breadcrumbs'][] = ['label' => Yii::t('app', 'Queues'), 'url' => ['index']];
14 | $this->params['breadcrumbs'][] = ['label' => $queue->label, 'url' => ['messages', 'queue_name' => $queue_name, 'subscriber_id' => $message->subscriber_id]];
15 | $this->params['breadcrumbs'][] = $message->id;
16 |
17 | ?>
18 | $message->id]); ?> created_on; ?>
19 |
20 |
21 |
22 | body === null ? ''.Yii::t('app', 'No message body').' ' : $message->body; ?>
23 |
24 |
25 |
26 | status === Message::AVAILABLE): ?>
27 |
30 |
31 | $queue_name, 'subscriber_id' => $message->subscriber_id]); ?>
32 |
33 |
34 |
35 | user->checkAccess('nfy.message.read.subscribed', [], true, false) && ($otherMessages = $dbMessage->getSubscriptionMessages()->joinWith('subscription.subscriber')->orderBy($dbMessage->getDb()->getSchema()->quoteSimpleTableName('nfy_messages').'.deleted_on, '.$dbMessage->getDb()->getSchema()->quoteSimpleTableName('subscriber').'.username')->all()) != []): ?>
36 | :
37 |
38 |
39 | deleted_on.' '.$otherMessage->subscription->subscriber; ?>
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/models/DbSubscriptionQuery.php:
--------------------------------------------------------------------------------
1 | modelClass;
15 | $this->andWhere($modelClass::tableName().'.is_deleted = false');
16 |
17 | return $this;
18 | }
19 |
20 | /**
21 | * @param string $queue_id
22 | * @return DbSubscriptionQuery $this
23 | */
24 | public function withQueue($queue_id)
25 | {
26 | $modelClass = $this->modelClass;
27 | $this->andWhere($modelClass::tableName().'.queue_id=:queue_id', [':queue_id' => $queue_id]);
28 |
29 | return $this;
30 | }
31 |
32 | /**
33 | * @param string $subscriber_id
34 | * @return DbSubscriptionQuery $this
35 | */
36 | public function withSubscriber($subscriber_id)
37 | {
38 | $modelClass = $this->modelClass;
39 | $this->andWhere($modelClass::tableName().'.subscriber_id=:subscriber_id', [':subscriber_id' => $subscriber_id]);
40 |
41 | return $this;
42 | }
43 |
44 | /**
45 | * @param array|string $categories
46 | * @return DbSubscriptionQuery $this
47 | */
48 | public function matchingCategory($categories)
49 | {
50 | if ($categories === null) {
51 | return $this;
52 | }
53 | $modelClass = $this->modelClass;
54 | $t = $modelClass::tableName();
55 | $r = DbSubscriptionCategory::tableName();
56 |
57 | if (!is_array($categories)) {
58 | $categories = [$categories];
59 | }
60 | if (empty($categories)) {
61 | return $this;
62 | }
63 |
64 | $this->innerJoinWith('categories');
65 |
66 | $i = 0;
67 | $conditions = ['AND'];
68 | $params = [];
69 | foreach ($categories as $category) {
70 | $conditions[] = [
71 | 'OR',
72 | "($r.is_exception = false AND :category$i LIKE $r.category)",
73 | "($r.is_exception = true AND :category$i NOT LIKE $r.category)",
74 | ];
75 | $params[':category'.$i++] = $category;
76 | }
77 |
78 | $this->andWhere($conditions, $params);
79 |
80 | return $this;
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/components/Message.php:
--------------------------------------------------------------------------------
1 | status == self::RESERVED) {
59 | $attributes[] = 'reserved_on';
60 | }
61 | if ($this->status == self::DELETED) {
62 | $attributes[] = 'deleted_on';
63 | }
64 |
65 | return $attributes;
66 | }
67 |
68 | /**
69 | * Sets the properties values in a massive way.
70 | * @param array $values properties values (name=>value) to be set.
71 | */
72 | public function setAttributes($values)
73 | {
74 | if (!is_array($values)) {
75 | return;
76 | }
77 | foreach ($values as $name => $value) {
78 | $this->$name = $value;
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/messages/pl/app.php:
--------------------------------------------------------------------------------
1 | 'Kategorie',
21 | 'Exceptions' => 'Wyjątki',
22 | 'Failed to save category {category} for subscription {subscription_id}' => 'Nie udało się zapisać kategorii {category} dla subskrypcji {subscription_id}',
23 | 'Failed to save message \'{msg}\' in queue {queue_name}.' => 'Nie udało się zapisać wiadomości \'{msg}\' w kolejce {queue_name}.',
24 | 'Failed to save message \'{msg}\' in queue {queue_name} for the subscription {subscription_id}.' => 'Nie udało się zapisać wiadomości \'{msg}\' w kolejce {queue_name} dla subskrypcji {subscription_id}.',
25 | 'Failed to subscribe {subscriber_id} to {queue_name}' => 'Nie udało się zapisać {subscriber_id} do kolejki {queue_name}',
26 | 'Label' => 'Etykieta',
27 | 'Message {id}' => 'Wiadomość {id}',
28 | 'Not sending message \'{msg}\' to queue {queue_name}.' => 'Nie wysyłam wiadomości \'{msg}\' do kolejki {queue_name}.',
29 | 'Queue {queue_name} is full.' => 'Kolejka {queue_name} jest pełna.',
30 | 'Queue id must be exactly a one character.' => 'Id kolejki to musi być dokładnie jeden znak.',
31 | 'Queue with given ID was not found.' => 'Kolejka o podanym ID nie została odnaleziona.',
32 | 'Message with given ID was not found.' => 'Wiadomość o podanym ID nie została odnaleziona.',
33 | 'Sent message \'{msg}\' to queue {queue_name}.' => 'Wysłano wiadomość \'{msg}\' do kolejki {queue_name}.',
34 | 'Subscribe' => 'Zapisz się',
35 | 'Unsubscribe' => 'Wypisz się',
36 | 'Back to messages list' => 'Wróć do listy wiadomości',
37 | 'List' => 'Lista',
38 | 'Mark all as read' => 'Oznacz wszystkie jako przeczytane',
39 | 'Mark as read' => 'Oznacz jako przeczytaną',
40 | 'Message category' => 'Kategoria wiadomości',
41 | 'Message content' => 'Treść wiadomości',
42 | 'Newer' => 'Nowsze',
43 | 'No message body' => 'Brak treści wiadomości',
44 | 'Older' => 'Starsze',
45 | 'Other recipients' => 'Pozostali odbiorcy',
46 | 'Queues' => 'Kolejki',
47 | 'Submit' => 'Wyślij',
48 | 'Subscriptions' => 'Subskrypcje',
49 | 'View all messages' => 'Zobacz wszystkie wiadomości',
50 | 'View messages' => 'Zobacz wiadomości',
51 | ];
52 |
--------------------------------------------------------------------------------
/assets/js/jquery.webnotification.min.js:
--------------------------------------------------------------------------------
1 | !function($){$.wnf=function(options){var F="function",S="string",getNotification=function(){var id="",n=_self.settings.notification;return n.tag&&typeof n.tag===S&&(id="wn-"+n.tag),''+window.location.host+'
'+getBody()+"
"},getBody=function(extra){var img="",n=_self.settings.notification;return n.icon&&typeof n.icon===S&&(img=' '),''},bindOnClickFn=function($b,fn){typeof fn===F&&$b.find(".wn-body").last().click(function(){fn()})},removeNotification=function($notification,$sep){"undefined"!=typeof $sep&&$sep.animate({opacity:0},750,function(){$sep.remove()}),$notification.animate({opacity:0},750,function(){$notification.remove(),typeof _self.settings.onCloseFn===F&&_self.settings.onCloseFn()})},_self=this,defaults={position:"bottom right",autoclose:!1,expire:0,notification:{ntitle:"",nbody:"",icon:"",tag:"",dir:"rtl"},onShowFn:$.noop,onClickFn:$.noop,onCloseFn:$.noop};_self.settings={},function(){_self.settings=$.extend({},defaults,options);var newNotification,$notContainer,$notBox,s=_self.settings,n=s.notification,p=s.position;if(!n.ntitle||!n.nbody)throw"Title, and message of the notification are required parameters.";$notContainer=$("#wn-"+p.replace(/ /g,"")),$notBox=$("#wn-"+n.tag),0===$notContainer.length&&($notContainer=$('
').appendTo("body")),_self.settings.notification.tag&&typeof _self.settings.notification.tag===S&&0!==$notBox.length?(newNotification=''+getBody("hidden"),$notBox.append(newNotification),$notBox.find("table.wn-sep").last().animate({opacity:1},750),$notBox.find(".wn-body").last().animate({opacity:1},750,function(){var $remover=$notBox.find(".wn-close").last();$remover.click(function(){var $sep=$(this).parents("table.wn-sep");removeNotification($sep.next(".wn-body"),$sep)}),bindOnClickFn($notBox,s.onClickFn),typeof s.onShowFn===F&&s.onShowFn(),s.autoclose&&setTimeout(function(){removeNotification($remover.parents("table.wn-sep").next(".wn-body"),$remover.parents("table.wn-sep"))},s.expire)})):(newNotification=getNotification(),$notContainer.append(newNotification),$notContainer.find(".wn-box").last().animate({opacity:1},750,function(){var $box=$(this);$box.find(".wn-head .wn-close").click(function(){removeNotification($box)}),bindOnClickFn($box,s.onClickFn),typeof s.onShowFn===F&&s.onShowFn(),s.autoclose&&setTimeout(function(){if(0==$box.find("table.wn-sep").length)removeNotification($box);else{var $firstNotification=$box.find(".wn-body").first();removeNotification($firstNotification,$firstNotification.next("table.wn-sep"))}},s.expire)}))}()}}(jQuery);
--------------------------------------------------------------------------------
/components/Subscription.php:
--------------------------------------------------------------------------------
1 | value) to be set.
34 | */
35 | public function setAttributes($values)
36 | {
37 | if (!is_array($values)) {
38 | return;
39 | }
40 | foreach ($values as $name => $value) {
41 | $this->$name = $value;
42 | }
43 | }
44 |
45 | /**
46 | * Tests if specified category matches any category and doesn't match any exception of this subscription.
47 | *
48 | * @param string $category
49 | * @return boolean
50 | */
51 | public function matchCategory($category)
52 | {
53 | $result = empty($this->categories);
54 | foreach ($this->categories as $allowedCategory) {
55 | if ($this->categoryContains($allowedCategory, $category)) {
56 | $result = true;
57 | }
58 | }
59 | foreach ($this->exceptions as $deniedCategory) {
60 | if ($this->categoryContains($deniedCategory, $category)) {
61 | $result = false;
62 | }
63 | }
64 |
65 | return $result;
66 | }
67 |
68 | /**
69 | * Checkes if $container contains $category, supporting wildcards.
70 | * Examples:
71 | * - category* contains category, categoryX, category.1
72 | * - category.* contains only category.1, not category itself
73 | * - category.1* contains category.12, category.13
74 | * @param string $container category name that can contain wildcards
75 | * @param string $category category name without wildcards
76 | */
77 | private function categoryContains($container, $category)
78 | {
79 | if (($c = rtrim($container, '*')) !== $container) {
80 | if (strpos($category, $c) === 0) {
81 | return true;
82 | }
83 | } elseif ($container == $category) {
84 | return true;
85 | }
86 |
87 | return false;
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/migrations/m130713_201034_notifications_install.php:
--------------------------------------------------------------------------------
1 | getModule('nfy');
10 | $userClass = $nfy->userClass;
11 | $userTable = $userClass::tableName();
12 | $userPk = $userClass::primaryKey();
13 | $userPkType = $userClass::getTableSchema()->getColumn($userPk[0])->dbType;
14 | $schema = $userClass::getDb()->schema;
15 | $driver = $userClass::getDb()->driverName;
16 |
17 | $this->createTable('{{%nfy_subscriptions}}', [
18 | 'id' => 'pk',
19 | 'queue_id' => 'string NOT NULL',
20 | 'label' => 'string',
21 | 'subscriber_id' => $userPkType.' NOT NULL REFERENCES '.$schema->quoteTableName($userTable).' ('.$userPk[0].') ON DELETE CASCADE ON UPDATE CASCADE',
22 | 'created_on' => 'timestamp',
23 | 'is_deleted' => 'boolean NOT NULL DEFAULT '.($driver === 'sqlite' ? '0' : 'false'),
24 | ]);
25 | $this->createTable('{{%nfy_subscription_categories}}', [
26 | 'id' => 'pk',
27 | 'subscription_id' => 'integer NOT NULL REFERENCES '.$schema->quoteTableName('{{nfy_subscriptions}}').' (id) ON DELETE CASCADE ON UPDATE CASCADE',
28 | 'category' => 'string NOT NULL',
29 | 'is_exception' => 'boolean NOT NULL DEFAULT '.($driver === 'sqlite' ? '0' : 'false'),
30 | ]);
31 | $this->createTable('{{%nfy_messages}}', [
32 | 'id' => 'pk',
33 | 'queue_id' => 'string NOT NULL',
34 | 'created_on' => 'timestamp NOT NULL',
35 | 'sender_id' => $userPkType.' REFERENCES '.$schema->quoteTableName($userTable).' ('.$userPk[0].') ON DELETE CASCADE ON UPDATE CASCADE',
36 | 'message_id' => 'integer',
37 | 'subscription_id' => 'integer REFERENCES '.$schema->quoteTableName('{{nfy_subscriptions}}').' (id) ON DELETE CASCADE ON UPDATE CASCADE',
38 | 'status' => 'integer NOT NULL',
39 | 'timeout' => 'integer',
40 | 'reserved_on' => 'timestamp',
41 | 'deleted_on' => 'timestamp',
42 | 'mimetype' => 'string NOT NULL DEFAULT \'text/plain\'',
43 | 'body' => 'text',
44 | ]);
45 |
46 | $prefix = $this->db->tablePrefix;
47 | $this->createIndex($prefix.'nfy_messages_queue_id_idx', '{{%nfy_messages}}', 'queue_id');
48 | $this->createIndex($prefix.'nfy_messages_sender_id_idx', '{{%nfy_messages}}', 'sender_id');
49 | $this->createIndex($prefix.'nfy_messages_message_id_idx', '{{%nfy_messages}}', 'message_id');
50 | $this->createIndex($prefix.'nfy_messages_status_idx', '{{%nfy_messages}}', 'status');
51 | $this->createIndex($prefix.'nfy_messages_reserved_on_idx', '{{%nfy_messages}}', 'reserved_on');
52 | $this->createIndex($prefix.'nfy_messages_subscription_id_idx', '{{%nfy_messages}}', 'subscription_id');
53 |
54 | $this->createIndex($prefix.'nfy_subscriptions_queue_id_idx', '{{%nfy_subscriptions}}', 'queue_id');
55 | $this->createIndex($prefix.'nfy_subscriptions_subscriber_id_idx', '{{%nfy_subscriptions}}', 'subscriber_id');
56 | $this->createIndex($prefix.'nfy_subscriptions_queue_id_subscriber_id_idx', '{{%nfy_subscriptions}}', 'queue_id,subscriber_id', true);
57 | $this->createIndex($prefix.'nfy_subscriptions_is_deleted_idx', '{{%nfy_subscriptions}}', 'is_deleted');
58 |
59 | $this->createIndex($prefix.'nfy_subscription_categories_subscription_id_idx', '{{%nfy_subscription_categories}}', 'subscription_id');
60 | $this->createIndex($prefix.'nfy_subscription_categories_subscription_id_category_idx', '{{%nfy_subscription_categories}}', 'subscription_id,category', true);
61 | }
62 |
63 | public function safeDown()
64 | {
65 | $this->dropTable('{{%nfy_messages}}');
66 | $this->dropTable('{{%nfy_subscription_categories}}');
67 | $this->dropTable('{{%nfy_subscriptions}}');
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/models/DbSubscription.php:
--------------------------------------------------------------------------------
1 | 'search'],
45 | ['subscriber_id', 'number', 'integerOnly' => true],
46 | ['is_deleted', 'boolean'],
47 | ['label', 'safe'],
48 | ];
49 | }
50 |
51 | public function getMessages()
52 | {
53 | return $this->hasMany(DbMessage::className(), ['subscription_id' => 'id']);
54 | }
55 |
56 | public function getSubscriber()
57 | {
58 | $userClass = Yii::$app->getModule('nfy')->userClass;
59 |
60 | return $this->hasOne($userClass, ['id' => 'subscriber_id'])->from($userClass::tableName().' subscriber');
61 | }
62 |
63 | public function getCategories()
64 | {
65 | return $this->hasMany(DbSubscriptionCategory::className(), ['subscription_id' => 'id']);
66 | }
67 |
68 | public function getMessagesCount()
69 | {
70 | return $this->getMessages()->count();
71 | }
72 |
73 | /**
74 | * @return array customized attribute labels (name=>label)
75 | */
76 | public function attributeLabels()
77 | {
78 | return [
79 | 'id' => Yii::t('models', 'ID'),
80 | 'queue_id' => Yii::t('models', 'Queue ID'),
81 | 'label' => Yii::t('models', 'Label'),
82 | 'subscriber_id' => Yii::t('models', 'Subscriber ID'),
83 | 'created_on' => Yii::t('models', 'Created On'),
84 | 'is_deleted' => Yii::t('models', 'Is Deleted'),
85 | ];
86 | }
87 |
88 | public function beforeSave($insert)
89 | {
90 | if ($insert && $this->created_on === null) {
91 | $now = new \DateTime('now', new \DateTimezone('UTC'));
92 | $this->created_on = $now->format('Y-m-d H:i:s');
93 | }
94 |
95 | return parent::beforeSave($insert);
96 | }
97 |
98 | /**
99 | * Creates an array of Subscription objects from DbSubscription objects.
100 | * @param DbSubscription|array $dbSubscriptions one or more DbSubscription objects
101 | * @return array of Subscription objects
102 | */
103 | public static function createSubscriptions($dbSubscriptions)
104 | {
105 | if (!is_array($dbSubscriptions)) {
106 | $dbSubscriptions = [$dbSubscriptions];
107 | }
108 | $result = [];
109 | foreach ($dbSubscriptions as $dbSubscription) {
110 | $attributes = $dbSubscription->getAttributes();
111 | unset($attributes['id']);
112 | unset($attributes['queue_id']);
113 | unset($attributes['is_deleted']);
114 | $subscription = new components\Subscription();
115 | $subscription->setAttributes($attributes);
116 | foreach ($dbSubscription->categories as $category) {
117 | if (!$category->is_exception) {
118 | $subscription->categories[] = $category->category;
119 | } else {
120 | $subscription->exceptions[] = $category->category;
121 | }
122 | }
123 | $result[] = $subscription;
124 | }
125 |
126 | return $result;
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Notifications
2 |
3 | This is a module for [Yii 2.0 framework](http://www.yiiframework.com/) that provides:
4 |
5 | * a generic queue component
6 | * a Publish/Subscribe message delivery pattern
7 | * a SQL database queue implementation
8 | * a configurable way to send various notifications, messages and tasks to a queue
9 | * a basic widget to read such items from queue and display them to the user as system notifications
10 | * a basic widget to put in a navbar that displays notifications and/or messages in a popup
11 | * a basic CRUD to manage and/or debug queues or use as a simple messanger
12 |
13 | Messages could be passed directly as strings or created from some objects, like Active Records. This could be used to log all changes to the models, exactly like the [audittrail2](http://www.yiiframework.com/extension/audittrail2) extension.
14 |
15 | When recipients are subscribed to a channel, message delivery can depend on category filtering, much like in logging system provided by the framework.
16 |
17 | A simple SQL queue implementation is provided if a MQ server is not available or not necessary.
18 |
19 | ## Installation
20 |
21 | 1. Install [Yii2](https://github.com/yiisoft/yii2/tree/master/apps/basic) using your preferred method
22 | 2. Install package via [composer](http://getcomposer.org/download/)
23 | * Run `php composer.phar require nineinchnick/yii2-nfy "dev-master"` OR add to composer.json require section `"nineinchnick/yii2-nfy": "dev-master"`
24 | * If Redis queue will be used, also install "yiisoft/yii2-redis"
25 | 3. Update config file *config/web.php* as shown below. Check out the Module for more available options.
26 |
27 |
28 | Enable module in configuration. Do it in both main and console configs, because some settings are used in migrations.
29 |
30 | Copy migrations to your migrations folder and adjust dates in file and class names. Then apply migrations.
31 |
32 | Define some queues as application components and optionally enable the module, see the next section.
33 |
34 | ~~~php
35 | $config = [
36 | // .........
37 | 'aliases' => [
38 | '@nineinchnick/nfy' => '@vendor/nineinchnick/yii2-nfy',
39 | ],
40 | 'modules' => [
41 | 'nfy' => [
42 | 'class' => 'nineinchnick\nfy\Module',
43 | ],
44 | ],
45 | 'components' => [
46 | 'dbmq' => [
47 | 'class' => 'nineinchnick\nfy\components\DbQueue',
48 | 'id' => 'queue',
49 | 'label' => 'Notifications',
50 | 'timeout' => 30,
51 | ],
52 | 'sysvmq' => [
53 | 'class' => 'nineinchnick\nfy\components\SysVQueue',
54 | 'id' => 'a',
55 | 'label' => 'IPC queue',
56 | ],
57 | 'redismq' => [
58 | 'class' => 'nineinchnick\nfy\components\RedisQueue',
59 | 'id' => 'mq',
60 | 'label' => 'Redis queue',
61 | 'redis' => 'redis',
62 | ],
63 | // ..........
64 | ],
65 | ]
66 | ~~~
67 |
68 | Then you can send and receive messages through this component:
69 |
70 | ~~~php
71 | // send one message 'test'
72 | Yii::$app->dbmq->send('test');
73 | // receive all available messages without using subscriptions and immediately delete them from the queue
74 | $messages = $queue->receive();
75 | ~~~
76 |
77 | Or you could subscribe some users to it:
78 |
79 | ~~~php
80 | Yii::$app->queue->subscribe(Yii:$app->user->getId());
81 | // send one message 'test'
82 | Yii::$app->queue->send('test');
83 | // receive all available messages for current user and immediately delete them from the queue
84 | $messages = $queue->receive(Yii:$app->user->getId());
85 | // if there are any other users subscribed, they will receive the message independently
86 | ~~~
87 |
88 | ## Module parameters
89 |
90 | By specifying the users model class name in the _userClass_ property proper table name and primary key column name will be used in migrations.
91 |
92 | ## Display notifications
93 |
94 | Put anywhere in your layout or views or controller.
95 |
96 | ~~~php
97 | $this->widget('nfy.extensions.webNotifications.WebNotifications', array('url'=>$this->createUrl('/nfy/default/poll', array('id'=>'queueComponentId'))));
98 | ~~~
99 |
100 | ## Receiving messages
101 |
102 | By configuring the WebNotifications widget messages could be read by:
103 |
104 | * polling using ajax (repeating requests at fixed interval) an action that checks a queue and returns new items
105 | * connect to a web socket and wait for new items
106 |
107 | ## Changelog
108 |
109 | ### 0.95 - 2014-01-15
110 |
111 | Initial release after porting from Yii 1.
112 |
113 |
114 |
--------------------------------------------------------------------------------
/widgets/Messages.php:
--------------------------------------------------------------------------------
1 | messages as $queueName => $messages) {
20 | $count += count($messages);
21 | }
22 |
23 | return $count;
24 | }
25 |
26 | public function createMenuItem()
27 | {
28 | $count = $this->countMessages();
29 |
30 | return [
31 | 'url' => '/nfy/queue',
32 | 'label' => ' '.($count > 0 ? (''.$count.' ') : ''),
33 | //'visible' => !Yii::$app->user->isGuest,
34 | 'options' => ['id' => $this->getId()],
35 | ];
36 | }
37 |
38 | public function run()
39 | {
40 | $elements = '';
41 |
42 | $cnt = 0;
43 | $extraCss = '';
44 |
45 | if ($this->view->context instanceof nineinchnick\nfy\controllers\QueueController) {
46 | $queueController = $this->view->context;
47 | } else {
48 | $queueController = new \nineinchnick\nfy\controllers\QueueController('queue', Yii::$app->getModule('nfy'));
49 | }
50 |
51 | foreach ($this->messages as $queueName => $messages) {
52 | foreach ($messages as $message) {
53 | $text = addcslashes($message->body, "'\r\n");
54 | $detailsUrl = $queueController->createMessageUrl($queueName, $message);
55 |
56 | $extraCss = (++$cnt % 2) === 0 ? 'even' : 'odd';
57 | $elements .= "{$text}
";
58 | }
59 | }
60 |
61 | $label = Yii::t('app', 'Mark all as read');
62 | //! @todo fix this
63 | $deleteUrl = Url::toRoute('/nfy/message/mark');
64 | $widgetId = $this->getId();
65 |
66 | $js = <<{$label} ';
76 |
77 | return ret;
78 | }
79 | });
80 |
81 | $('body').click(function (e) {
82 | var obj = $('div.popover');
83 | if (obj !== null && obj.length > 0 && (obj.is(':visible') || !obj.is(':hidden'))) {
84 | $('#{$widgetId}').popover('hide');
85 | }
86 | });
87 |
88 | $('#{$widgetId}').hover(function (e) {
89 | var obj = $('div.popover');
90 | if (obj === null || obj.length <= 0 || !obj.is(':visible') || obj.is(':hidden')) {
91 | obj = $('div.nav-collapse li.dropdown.open');
92 | if (obj === null || obj.length <= 0) {
93 | $(this).popover('show');
94 | }
95 | }
96 | });
97 |
98 | JavaScript;
99 |
100 | $css = <<view->registerCss($css);
163 | $this->view->registerJs($js);
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/models/DbMessage.php:
--------------------------------------------------------------------------------
1 | 'search'],
52 | [['sender_id', 'subscription_id', 'timeout'], 'number', 'integerOnly' => true],
53 | [['message_id', 'subscription_id', 'timeout'], 'number', 'integerOnly' => true, 'on' => 'search'],
54 | ['status', 'number', 'integerOnly' => true, 'on' => 'search'],
55 | ['mimetype', 'safe', 'on' => 'search'],
56 | ];
57 | }
58 |
59 | public function getMainMessage()
60 | {
61 | return $this->hasOne(DbMessage::className(), ['id' => 'message_id']);
62 | }
63 |
64 | public function getSender()
65 | {
66 | return $this->hasOne(Yii::$app->getModule('nfy')->userClass, ['id' => 'sender_id']);
67 | }
68 |
69 | public function getSubscription()
70 | {
71 | return $this->hasOne(DbSubscription::className(), ['id' => 'subscription_id']);
72 | }
73 |
74 | public function getSubscriptionMessages()
75 | {
76 | return $this->hasMany(DbMessage::className(), [self::tableName().'.message_id' => 'id']);
77 | }
78 |
79 | /**
80 | * @return array customized attribute labels (name=>label)
81 | */
82 | public function attributeLabels()
83 | {
84 | return [
85 | 'id' => Yii::t('models', 'ID'),
86 | 'queue_id' => Yii::t('models', 'Queue ID'),
87 | 'created_on' => Yii::t('models', 'Created On'),
88 | 'sender_id' => Yii::t('models', 'Sender ID'),
89 | 'message_id' => Yii::t('models', 'Message ID'),
90 | 'subscription_id' => Yii::t('models', 'Subscription ID'),
91 | 'status' => Yii::t('models', 'Status'),
92 | 'timeout' => Yii::t('models', 'Timeout'),
93 | 'reserved_on' => Yii::t('models', 'Reserved On'),
94 | 'deleted_on' => Yii::t('models', 'Deleted On'),
95 | 'mimetype' => Yii::t('models', 'MIME Type'),
96 | 'body' => Yii::t('models', 'Message Body'),
97 | ];
98 | }
99 |
100 | /**
101 | * @inheritdoc
102 | */
103 | public function beforeSave($insert)
104 | {
105 | if ($insert && $this->created_on === null) {
106 | $now = new \DateTime('now', new \DateTimezone('UTC'));
107 | $this->created_on = $now->format('Y-m-d H:i:s');
108 | }
109 |
110 | return parent::beforeSave($insert);
111 | }
112 |
113 | public function __clone()
114 | {
115 | unset($this->id);
116 | unset($this->subscription_id);
117 | $this->isNewRecord = true;
118 | }
119 |
120 | /**
121 | * Creates an array of Message objects from DbMessage objects.
122 | * @param DbMessage|array $dbMessages one or more DbMessage objects
123 | * @return array of Message objects
124 | */
125 | public static function createMessages($dbMessages)
126 | {
127 | if (!is_array($dbMessages)) {
128 | $dbMessages = [$dbMessages];
129 | }
130 | $result = [];
131 | foreach ($dbMessages as $dbMessage) {
132 | $attributes = $dbMessage->getAttributes();
133 | $attributes['subscriber_id'] = $dbMessage->subscription_id === null ? null : $dbMessage->subscription->subscriber_id;
134 | unset($attributes['queue_id']);
135 | unset($attributes['subscription_id']);
136 | unset($attributes['mimetype']);
137 | $message = new components\Message();
138 | $message->setAttributes($attributes);
139 | $result[] = $message;
140 | }
141 |
142 | return $result;
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/models/DbMessageQuery.php:
--------------------------------------------------------------------------------
1 | modelClass;
18 | $this->andWhere($modelClass::tableName().'.status = '.Message::DELETED);
19 |
20 | return $this;
21 | }
22 |
23 | /**
24 | * @param integer $timeout
25 | * @return DbMessageQuery $this
26 | */
27 | public function available($timeout = null)
28 | {
29 | return self::withStatus(Message::AVAILABLE, $timeout);
30 | }
31 |
32 | /**
33 | * @param integer $timeout
34 | * @return DbMessageQuery $this
35 | */
36 | public function reserved($timeout = null)
37 | {
38 | return self::withStatus(Message::RESERVED, $timeout);
39 | }
40 |
41 | /**
42 | * @param integer $timeout
43 | * @return DbMessageQuery $this
44 | */
45 | public function timedout($timeout = null)
46 | {
47 | if ($timeout === null) {
48 | $this->andWhere('1=0');
49 |
50 | return $this;
51 | }
52 | $now = new \DateTime($timeout === null ? '' : "-$timeout seconds", new \DateTimezone('UTC'));
53 | $modelClass = $this->modelClass;
54 | $t = $modelClass::tableName();
55 | $this->andWhere("($t.status=".Message::RESERVED." AND $t.reserved_on <= :timeout)", [':timeout' => $now->format('Y-m-d H:i:s')]);
56 |
57 | return $this;
58 | }
59 |
60 | /**
61 | * @param array|string $statuses
62 | * @param integer $timeout
63 | * @return DbMessageQuery $this
64 | */
65 | public function withStatus($statuses, $timeout = null)
66 | {
67 | if (!is_array($statuses)) {
68 | $statuses = [$statuses];
69 | }
70 | $modelClass = $this->modelClass;
71 | $t = $modelClass::tableName();
72 | $now = new \DateTime($timeout === null ? '' : "-$timeout seconds", new \DateTimezone('UTC'));
73 | $conditions = ['or'];
74 | // test for two special cases
75 | if (array_diff($statuses, [Message::AVAILABLE, Message::RESERVED]) === []) {
76 | // only not deleted
77 | $conditions[] = "$t.status!=".Message::DELETED;
78 | } elseif (array_diff($statuses, [Message::AVAILABLE, Message::RESERVED, Message::DELETED]) === []) {
79 | // pass - don't add no conditions
80 | } else {
81 | // merge all statuses
82 | foreach ($statuses as $status) {
83 | switch ($status) {
84 | case Message::AVAILABLE:
85 | $conditions[] = "$t.status=".$status;
86 | if ($timeout !== null) {
87 | $conditions[] = "($t.status=".Message::RESERVED." AND $t.reserved_on <= :timeout)";
88 | $this->addParams([':timeout' => $now->format('Y-m-d H:i:s')]);
89 | }
90 | break;
91 | case Message::RESERVED:
92 | if ($timeout !== null) {
93 | $conditions[] = "($t.status=$status AND $t.reserved_on > :timeout)";
94 | $this->addParams([':timeout' => $now->format('Y-m-d H:i:s')]);
95 | } else {
96 | $conditions[] = "$t.status=".$status;
97 | }
98 | break;
99 | case Message::DELETED:
100 | $conditions[] = "$t.status=".$status;
101 | break;
102 | }
103 | }
104 | }
105 | if ($conditions !== ['or']) {
106 | $this->andWhere($conditions);
107 | }
108 |
109 | return $this;
110 | }
111 |
112 | /**
113 | * @param string $queue_id
114 | * @return DbMessageQuery $this
115 | */
116 | public function withQueue($queue_id)
117 | {
118 | $modelClass = $this->modelClass;
119 | $t = $modelClass::tableName();
120 | $pk = $modelClass::primaryKey();
121 | $this->andWhere($t.'.queue_id=:queue_id', [':queue_id' => $queue_id]);
122 | $this->orderBy = ["$t.{$pk[0]}" => 'ASC'];
123 |
124 | return $this;
125 | }
126 |
127 | /**
128 | * @param string $subscriber_id
129 | * @return DbMessageQuery $this
130 | */
131 | public function withSubscriber($subscriber_id = null)
132 | {
133 | if ($subscriber_id === null) {
134 | $modelClass = $this->modelClass;
135 | $t = $modelClass::tableName();
136 | $this->andWhere("$t.subscription_id IS NULL");
137 | } else {
138 | $this->innerJoinWith('subscription');
139 | $this->andWhere(DbSubscription::tableName().'.subscriber_id=:subscriber_id', [':subscriber_id' => $subscriber_id]);
140 | }
141 |
142 | return $this;
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/commands/NfyController.php:
--------------------------------------------------------------------------------
1 | 'nfy.queue.read', 'bizRule' => null, 'child' => null],
32 | ['name' => 'nfy.queue.read.subscribed', 'bizRule' => $rule->name, 'child' => 'nfy.queue.read'],
33 | ['name' => 'nfy.queue.subscribe', 'bizRule' => null, 'child' => null],
34 | ['name' => 'nfy.queue.unsubscribe', 'bizRule' => null, 'child' => null],
35 | ['name' => 'nfy.message.read', 'bizRule' => null, 'child' => null],
36 | ['name' => 'nfy.message.create', 'bizRule' => null, 'child' => null],
37 | ['name' => 'nfy.message.read.subscribed', 'bizRule' => $rule->name, 'child' => 'nfy.message.read'],
38 | ['name' => 'nfy.message.create.subscribed', 'bizRule' => $rule->name, 'child' => 'nfy.message.create'],
39 | ];
40 | }
41 |
42 | public function getTemplateAuthItemDescriptions()
43 | {
44 | return [
45 | 'nfy.queue.read' => Yii::t('auth', 'Read any queue'),
46 | 'nfy.queue.read.subscribed' => Yii::t('auth', 'Read subscribed queue'),
47 | 'nfy.queue.subscribe' => Yii::t('auth', 'Subscribe to any queue'),
48 | 'nfy.queue.unsubscribe' => Yii::t('auth', 'Unsubscribe from a queue'),
49 | 'nfy.message.read' => Yii::t('auth', 'Read messages from any queue'),
50 | 'nfy.message.create' => Yii::t('auth', 'Send messages to any queue'),
51 | 'nfy.message.read.subscribed' => Yii::t('auth', 'Read messages from subscribed queue'),
52 | 'nfy.message.create.subscribed' => Yii::t('auth', 'Send messages to subscribed queue'),
53 | ];
54 | }
55 |
56 | public function actionCreateAuthItems()
57 | {
58 | $auth = Yii::$app->authManager;
59 |
60 | $newAuthItems = [];
61 | $descriptions = $this->getTemplateAuthItemDescriptions();
62 | foreach ($this->getTemplateAuthItems() as $template) {
63 | $newAuthItems[$template['name']] = $template;
64 | }
65 | $existingAuthItems = $auth->getItems(rbac\Item::TYPE_OPERATION);
66 | foreach ($existingAuthItems as $name => $existingAuthItem) {
67 | if (isset($newAuthItems[$name])) {
68 | unset($newAuthItems[$name]);
69 | }
70 | }
71 | foreach ($newAuthItems as $template) {
72 | if ($template['bizRule'] !== null) {
73 | if (($rule = $auth->getrule($template['bizRule'])) === null) {
74 | $rule = new \nineinchnick\nfy\components\SubscribedRule;
75 | $auth->add($rule);
76 | }
77 | }
78 | $auth->createItem($template['name'], rbac\Item::TYPE_OPERATION, $descriptions[$template['name']], $template['bizRule']);
79 | if (isset($template['child']) && $template['child'] !== null) {
80 | $auth->addItemChild($template['name'], $template['child']);
81 | }
82 | }
83 | }
84 |
85 | public function actionRemoveAuthItems()
86 | {
87 | $auth = Yii::$app->authManager;
88 |
89 | foreach ($this->getTemplateAuthItems() as $template) {
90 | $auth->removeItem($template['name']);
91 | }
92 | }
93 |
94 | /**
95 | * @param string $queue name of the queue component
96 | * @param string $message
97 | */
98 | public function actionSend($queue, $message)
99 | {
100 | $q = Yii::$app->getComponent($queue);
101 | if ($q === null) {
102 | throw new NotFoundHttpException('Queue not found.');
103 | }
104 | $q->send($message);
105 | }
106 |
107 | /**
108 | * @param string $queue name of the queue component
109 | */
110 | public function actionReceive($queue, $limit = -1)
111 | {
112 | $q = Yii::$app->getComponent($queue);
113 | if ($q === null) {
114 | throw new NotFoundHttpException('Queue not found.');
115 | }
116 | var_dump($q->receive(null, $limit));
117 | }
118 |
119 | /**
120 | * @param string $queue name of the queue component
121 | */
122 | public function actionPeek($queue, $limit = -1)
123 | {
124 | $q = Yii::$app->getComponent($queue);
125 | if ($q === null) {
126 | throw new NotFoundHttpException('Queue not found.');
127 | }
128 | var_dump($q->peek(null, $limit));
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/assets/js/main.js:
--------------------------------------------------------------------------------
1 | (function( notificationsPoller, $, undefined ) {
2 | "use strict";
3 |
4 | var _method_poll = 'poll',
5 | _method_push = 'push';
6 |
7 | var _defaultSettings = {
8 | url: null,
9 | baseUrl: null,
10 | method: _method_poll,
11 | // time in miliseconds, how often to check for new messages
12 | pollInterval: 3000,
13 | // true if requests are cross domain
14 | xDomain: false,
15 | websocket: {}
16 | };
17 | var _settings;
18 | /**
19 | * @var object in polling mode: active ajax request
20 | */
21 | var _jqxhr;
22 | /**
23 | * @var object in polling mode: active timer
24 | */
25 | var _timer;
26 | /**
27 | * @var object in push mode: socket.io
28 | */
29 | var _socket;
30 | /**
31 | * @var array messages stack
32 | */
33 | var _messages = [];
34 | /**
35 | * @var boolean did user agreed to receive notifications
36 | */
37 | var _ready = false;
38 |
39 | notificationsPoller.wrapApi = function() {
40 | // from: https://gist.github.com/jhthorsen/5813059
41 | // added call to $.wnf when webkitNotification is not available
42 | if(window.Notification) {
43 | return;
44 | }
45 |
46 | if(window.webkitNotifications) {
47 | window.Notification = function(title, args) {
48 | var n = window.webkitNotifications.createNotification(args.iconUrl || '', title, args.body || '');
49 | $.each(['onshow', 'onclose'], function(k, i) { if(args[k]) this[k] = args[k]; });
50 | n.ondisplay = function() { if(this.onshow) this.onshow(); };
51 | n.show();
52 | return n;
53 | };
54 | window.Notification.permission = webkitNotifications.checkPermission() ? 'default' : 'granted';
55 | window.Notification.requestPermission = function(cb) {
56 | webkitNotifications.requestPermission(function() {
57 | window.Notification.permission = webkitNotifications.checkPermission() ? 'denied' : 'granted';
58 | cb(window.Notification.permission);
59 | });
60 | };
61 | window.Notification.prototype.close = function() { if(this.onclose) this.onclose(); };
62 |
63 | // since requestPermission won't work when called from init lets display a fallback popup to ask for permission
64 | $.wnf({notification: {
65 | autoclose: true,
66 | ntitle: 'Enable system notifications',
67 | nbody: 'Enable system notifications '
68 | }});
69 | return;
70 | }
71 | window.Notification = function(title, args) {
72 | var config = {
73 | /*position: 'bottom-right',
74 | autoclose: false,
75 | expire: null,*/
76 | notification: { ntitle: title, nbody: args.body, icon: args.iconUrl || '', tag: args.tag || '' }
77 | };
78 | if (args.onshow) config.onShowFn = args.onshow;
79 | if (args.onclose) config.onCloseFn = args.onclose;
80 | $.wnf( config );
81 | return this;
82 | };
83 | window.Notification.permission = 'granted';
84 | window.Notification.requestPermission = function(cb) { cb('granted'); };
85 | window.Notification.prototype.close = function() { if(this.onclose) this.onclose(); };
86 | };
87 |
88 | notificationsPoller.init = function(settings) {
89 | notificationsPoller.wrapApi();
90 | notificationsPoller.ask();
91 |
92 | _settings = $.extend({}, _defaultSettings, settings);
93 |
94 | if (_settings.method === _method_poll || typeof WebSocket === 'undefined') {
95 | if('onopen' in _settings.websocket) {
96 | _settings.websocket['onopen'](null)(null);
97 | }
98 | notificationsPoller.poll();
99 | return;
100 | }
101 |
102 | _socket = new WebSocket(_settings.url);
103 | for(var i in _settings.websocket) {
104 | if (typeof _settings.websocket[i] === 'function') {
105 | _socket[i] = _settings.websocket[i](_socket);
106 | }
107 | }
108 | window.WEB_SOCKET_SWF_LOCATION = _settings.baseUrl + '/js/WebSocketMain'+(_settings.xDomain ? 'Insecure' : '')+'.swf';
109 | };
110 |
111 | notificationsPoller.ask = function() {
112 | if (!window.Notification.permission !== 'granted') {
113 | window.Notification.requestPermission(function(){
114 | _ready = true;
115 | });
116 | } else {
117 | _ready = true;
118 | }
119 | // callable from anchor tag's onclick event
120 | return false;
121 | };
122 |
123 | notificationsPoller.poll = function() {
124 | _jqxhr = $.ajax({
125 | url: _settings.url,
126 | cache: false,
127 | success: notificationsPoller.process,
128 | error: notificationsPoller.error
129 | });
130 | };
131 |
132 | notificationsPoller.process = function(data) {
133 | if (typeof data === 'undefined' || typeof data.messages === 'undefined' || data.messages.length === 0) {
134 | _timer = window.setTimeout(notificationsPoller.poll, _settings.pollInterval);
135 | return false;
136 | }
137 | for (var i = 0; i < data.messages.length; i++) {
138 | notificationsPoller.addMessage(data.messages[i]);
139 | }
140 |
141 | notificationsPoller.display();
142 | _timer = window.setTimeout(notificationsPoller.poll, _settings.pollInterval);
143 | };
144 |
145 | notificationsPoller.addMessage = function(message) {
146 | _messages.push(message);
147 | };
148 |
149 | notificationsPoller.display = function() {
150 | if('onmessage' in _settings.websocket) {
151 | for (var i = 0; i < _messages.length; i++) {
152 | var ret = _settings.websocket['onmessage'](null)(_messages[i]);
153 |
154 | if(typeof ret !== 'undefined' && !ret) {
155 | delete _messages[i];
156 | }
157 | }
158 | }
159 |
160 | if (!_ready)
161 | return false;
162 |
163 | while(_messages.length) {
164 | var msg = _messages.shift();
165 |
166 | if(typeof msg === 'undefined') {
167 | continue;
168 | }
169 |
170 | new window.Notification(msg.title, {body: msg.body});
171 | if (typeof msg.sound !== 'undefined') {
172 | notificationsPoller.sound(msg.sound);
173 | }
174 | }
175 | };
176 |
177 | notificationsPoller.sound = function(url) {
178 | //$("").appendTo('body');
179 | $(" ").attr({ 'src':url, 'autoplay':'autoplay' }).appendTo("body");
180 | };
181 |
182 | notificationsPoller.error = function() {
183 | if('onerror' in _settings.websocket) {
184 | _settings.websocket['onerror'](null)(null);
185 | }
186 | console.log('Failed to check new messages at '+_settings.url);
187 | };
188 | }( window.notificationsPoller = window.notificationsPoller || {}, jQuery ));
189 |
--------------------------------------------------------------------------------
/components/MailQueue.php:
--------------------------------------------------------------------------------
1 | blocking) {
37 | throw new NotSupportedException(Yii::t('app', 'MailQueue does not support blocking.'));
38 | }
39 | $this->subscriptionQueue = Instance::ensure($this->subscriptionQueue, 'nineinchnick\nfy\components\QueueInterface');
40 | $this->mailer = Instance::ensure($this->mailer, 'yii\mail\MailerInterface');
41 | if (!is_callable($this->recipientCallback)) {
42 | throw new InvalidConfigException(Yii::t('app', 'MailQueue requires a valid callback for recipientCallback.'));
43 | }
44 | if ($this->composeCallback === null) {
45 | $this->composeCallback = [$this, 'createMessage'];
46 | }
47 | }
48 |
49 | /**
50 | * Creates an instance of a Message.
51 | * This method may be overriden in extending classes.
52 | * @param string $body message body
53 | * @return MessageInterface
54 | */
55 | protected function createMessage($body)
56 | {
57 | return $this->mailer->compose('nfy/message', ['message' => $body])
58 | ->setSubject(Yii::t('app', 'Notification from {app}', ['app' => Yii::$app->name]));
59 | }
60 |
61 | /**
62 | * @inheritdoc
63 | */
64 | public function send($message, $category = null)
65 | {
66 | $stringMessage = $message;
67 | if (!is_string($message)) {
68 | $stringMessage = print_r($message, true);
69 | }
70 | if ($this->beforeSend($message) !== true) {
71 | Yii::info(Yii::t('app', "Not sending message '{msg}' to queue {queue_label}.", [
72 | 'msg' => $stringMessage,
73 | 'queue_label' => $this->label,
74 | ]), 'nfy');
75 |
76 | return false;
77 | }
78 |
79 | $success = true;
80 |
81 | if ($this->sendToSubscriptionQueue) {
82 | $this->subscriptionQueue->send($message, $category);
83 | }
84 |
85 | $mailMessage = call_user_func($this->composeCallback, $message);
86 |
87 | foreach ($this->getSubscriptions() as $subscription) {
88 | if ($this->beforeSendSubscription($message, $subscription->subscriber_id) !== true) {
89 | continue;
90 | }
91 |
92 | if ($category !== null && !$subscription->matchCategory($category)) {
93 | continue;
94 | }
95 |
96 | if (!$mailMessage->setTo(call_user_func($this->recipientCallback, $subscription->subscriber_id))->send()) {
97 | Yii::error(Yii::t('app', "Failed to save message '{msg}' in queue {queue_label} for the subscription {subscription_id}.", [
98 | 'msg' => $stringMessage,
99 | 'queue_label' => $this->label,
100 | 'subscription_id' => $subscription->subscriber_id,
101 | ]), 'nfy');
102 | $success = false;
103 | }
104 |
105 | $this->afterSendSubscription($message, $subscription->subscriber_id);
106 | }
107 |
108 | $this->afterSend($message);
109 |
110 | Yii::info(Yii::t('app', "Sent message '{msg}' to queue {queue_label}.", [
111 | 'msg' => $stringMessage,
112 | 'queue_label' => $this->label,
113 | ]), 'nfy');
114 |
115 | return $success;
116 | }
117 |
118 | /**
119 | * @inheritdoc
120 | */
121 | public function peek($subscriber_id = null, $limit = -1, $status = Message::AVAILABLE)
122 | {
123 | throw new NotSupportedException(Yii::t('app', 'MailQueue does not support peeking.'));
124 | }
125 |
126 | /**
127 | * @inheritdoc
128 | */
129 | public function reserve($subscriber_id = null, $limit = -1)
130 | {
131 | throw new NotSupportedException(Yii::t('app', 'MailQueue does not support reserving.'));
132 | }
133 |
134 | /**
135 | * @inheritdoc
136 | */
137 | public function receive($subscriber_id = null, $limit = -1)
138 | {
139 | throw new NotSupportedException(Yii::t('app', 'MailQueue does not support receiving.'));
140 | }
141 |
142 | /**
143 | * @inheritdoc
144 | */
145 | public function delete($message_id, $subscriber_id = null)
146 | {
147 | throw new NotSupportedException(Yii::t('app', 'MailQueue does not support deleting.'));
148 | }
149 |
150 | /**
151 | * @inheritdoc
152 | */
153 | public function release($message_id, $subscriber_id = null)
154 | {
155 | throw new NotSupportedException(Yii::t('app', 'MailQueue does not support releasing.'));
156 | }
157 |
158 | /**
159 | * @inheritdoc
160 | */
161 | public function releaseTimedout()
162 | {
163 | throw new NotSupportedException(Yii::t('app', 'MailQueue does not support releasing.'));
164 | }
165 |
166 | /**
167 | * @inheritdoc
168 | */
169 | public function subscribe($subscriber_id, $label = null, $categories = null, $exceptions = null)
170 | {
171 | return $this->subscriptionQueue->subscribe($subscriber_id, $label, $categories, $exceptions);
172 | }
173 |
174 | /**
175 | * @inheritdoc
176 | */
177 | public function unsubscribe($subscriber_id, $categories = null)
178 | {
179 | return $this->subscriptionQueue->unsubscribe($subscriber_id, $categories);
180 | }
181 |
182 | /**
183 | * @inheritdoc
184 | */
185 | public function isSubscribed($subscriber_id, $category = null)
186 | {
187 | return $this->subscriptionQueue->isSubscribed($subscriber_id, $category);
188 | }
189 |
190 | /**
191 | * @inheritdoc
192 | */
193 | public function getSubscriptions($subscriber_id = null)
194 | {
195 | return $this->subscriptionQueue->getSubscriptions($subscriber_id);
196 | }
197 | }
198 |
--------------------------------------------------------------------------------
/components/QueueInterface.php:
--------------------------------------------------------------------------------
1 | id) !== 1) {
36 | throw new InvalidConfigException(Yii::t('app', 'Queue id must be exactly a one character.'));
37 | }
38 | }
39 |
40 | /**
41 | * Return a number representing the current queue.
42 | * @return integer
43 | */
44 | private function getKey()
45 | {
46 | if ($this->_key === null) {
47 | $this->_key = ftok(__FILE__, $this->id);
48 | }
49 |
50 | return $this->_key;
51 | }
52 | private function getQueue()
53 | {
54 | if ($this->_queue === null) {
55 | $this->_queue = msg_get_queue($this->getKey(), $this->permissions);
56 | }
57 |
58 | return $this->_queue;
59 | }
60 | /**
61 | * Creates an instance of Message model. The passed message body may be modified, @see formatMessage().
62 | * This method may be overriden in extending classes.
63 | * @param string $body message body
64 | * @return Message
65 | */
66 | protected function createMessage($body)
67 | {
68 | $now = new \DateTime('now', new \DateTimezone('UTC'));
69 | $message = new Message();
70 | $message->setAttributes([
71 | 'created_on' => $now->format('Y-m-d H:i:s'),
72 | 'sender_id' => Yii::$app->has('user') ? Yii::$app->user->getId() : null,
73 | 'body' => $body,
74 | ]);
75 |
76 | return $this->formatMessage($message);
77 | }
78 |
79 | /**
80 | * Formats the body of a queue message. This method may be overriden in extending classes.
81 | * @param Message $message
82 | * @return Message $message
83 | */
84 | protected function formatMessage($message)
85 | {
86 | return $message;
87 | }
88 |
89 | /**
90 | * @inheritdoc
91 | */
92 | public function send($message, $category = null)
93 | {
94 | $queueMessage = $this->createMessage($message);
95 |
96 | if ($this->beforeSend($queueMessage) !== true) {
97 | Yii::info(Yii::t('app', "Not sending message '{msg}' to queue {queue_label}.", [
98 | 'msg' => $queueMessage->body,
99 | 'queue_label' => $this->label
100 | ]), 'nfy');
101 |
102 | return;
103 | }
104 |
105 | $success = msg_send($this->getQueue(), 1, $queueMessage, true, false, $errorcode);
106 | if (!$success) {
107 | Yii::error(Yii::t('app', "Failed to save message '{msg}' in queue {queue_label}.", [
108 | 'msg' => $queueMessage->body,
109 | 'queue_label' => $this->label,
110 | ]), 'nfy');
111 | if ($errorcode === MSG_EAGAIN) {
112 | Yii::error(Yii::t('app', "Queue {queue_label} is full.", [
113 | 'queue_label' => $this->label,
114 | ]), 'nfy');
115 | }
116 |
117 | return false;
118 | }
119 |
120 | $this->afterSend($queueMessage);
121 |
122 | Yii::info(Yii::t('app', "Sent message '{msg}' to queue {queue_label}.", [
123 | 'msg' => $queueMessage->body,
124 | 'queue_label' => $this->label,
125 | ]), 'nfy');
126 | }
127 |
128 | /**
129 | * @inheritdoc
130 | * @throws NotSupportedException
131 | */
132 | public function peek($subscriber_id = null, $limit = -1, $status = Message::AVAILABLE)
133 | {
134 | throw new NotSupportedException('Not implemented. System V queues does not support peeking. Use the receive() method.');
135 | }
136 |
137 | /**
138 | * @inheritdoc
139 | * @throws NotSupportedException
140 | */
141 | public function reserve($subscriber_id = null, $limit = -1)
142 | {
143 | throw new NotSupportedException('Not implemented. System V queues does not support reserving messages. Use the receive() method.');
144 | }
145 |
146 | /**
147 | * Gets available messages from the queue and removes them from the queue.
148 | * @param mixed $subscriber_id unused, must be null
149 | * @param integer $limit number of available messages that will be fetched from the queue, defaults to -1 which means no limit
150 | * @return array of Message objects
151 | * @throws NotSupportedException
152 | */
153 | public function receive($subscriber_id = null, $limit = -1)
154 | {
155 | if ($subscriber_id !== null) {
156 | throw new NotSupportedException('Not implemented. System V queues does not support subscriptions.');
157 | }
158 | $flags = $this->blocking ? 0 : MSG_IPC_NOWAIT;
159 | $messages = [];
160 | $count = 0;
161 | while (($limit == -1 || $count < $limit) && (msg_receive($this->getQueue(), 0, $msgtype, self::MSG_MAXSIZE, $message, true, $flags, $errorcode))) {
162 | $message->subscriber_id = $subscriber_id;
163 | $message->status = Message::AVAILABLE;
164 | $messages[] = $message;
165 | $count++;
166 | }
167 |
168 | return $messages;
169 | }
170 |
171 | /**
172 | * @inheritdoc
173 | * @throws NotSupportedException
174 | */
175 | public function delete($message_id, $subscriber_id = null)
176 | {
177 | throw new NotSupportedException('Not implemented. System V queues does not support reserving messages.');
178 | }
179 |
180 | /**
181 | * @inheritdoc
182 | * @throws NotSupportedException
183 | */
184 | public function release($message_id, $subscriber_id = null)
185 | {
186 | throw new NotSupportedException('Not implemented. System V queues does not support reserving messages.');
187 | }
188 |
189 | /**
190 | * @inheritdoc
191 | * @throws NotSupportedException
192 | */
193 | public function releaseTimedout()
194 | {
195 | throw new NotSupportedException('Not implemented. System V queues does not support reserving messages.');
196 | }
197 |
198 | /**
199 | * @inheritdoc
200 | * @throws NotSupportedException
201 | */
202 | public function subscribe($subscriber_id, $label = null, $categories = null, $exceptions = null)
203 | {
204 | throw new NotSupportedException('Not implemented. System V queues does not support subscriptions.');
205 | }
206 |
207 | /**
208 | * @inheritdoc
209 | * @throws NotSupportedException
210 | */
211 | public function unsubscribe($subscriber_id, $categories = null)
212 | {
213 | throw new NotSupportedException('Not implemented. System V queues does not support subscriptions.');
214 | }
215 |
216 | /**
217 | * @inheritdoc
218 | * @throws NotSupportedException
219 | */
220 | public function isSubscribed($subscriber_id)
221 | {
222 | throw new NotSupportedException('Not implemented. System V queues does not support subscriptions.');
223 | }
224 |
225 | /**
226 | * @inheritdoc
227 | * @throws NotSupportedException
228 | */
229 | public function getSubscriptions($subscriber_id = null)
230 | {
231 | throw new NotSupportedException('Not implemented. System V queues does not support subscriptions.');
232 | }
233 | }
234 |
--------------------------------------------------------------------------------
/assets/js/jquery.webnotification.js:
--------------------------------------------------------------------------------
1 | /*
2 | * wnf v. 0.1
3 | * Web Notification Fallback
4 | *
5 | */
6 |
7 | (function($) {
8 |
9 | $.wnf = function(options) {
10 |
11 | var F = 'function',
12 | S = "string";
13 |
14 | // returns the complete markup to generate the notification
15 | var getNotification = function() {
16 |
17 | var id = '',
18 | n = _self.settings.notification;
19 |
20 | // check if the notification has a tag
21 | if (!!n.tag && typeof n.tag === S) {
22 |
23 | id = 'wn-' + n.tag;
24 |
25 | }
26 |
27 | return '' + window.location.host + '
' + getBody() + '
';
28 |
29 | }
30 |
31 | // returns the markup for the body of the notification
32 | var getBody = function(extra) {
33 |
34 | var img = '',
35 | n = _self.settings.notification;
36 |
37 | // check if the notification has an icon
38 | if (!!n.icon && typeof n.icon === S) {
39 |
40 | img = ' ';
41 |
42 | }
43 |
44 | return '' + img + '
' + n.ntitle + ' ' + n.nbody + '
';
45 |
46 | }
47 |
48 | // attach event listener: click on the notification
49 | var bindOnClickFn = function($b, fn) {
50 |
51 | if (typeof fn === F) {
52 |
53 | $b.find('.wn-body').last().click(function() {
54 |
55 | fn();
56 |
57 | });
58 |
59 | }
60 |
61 | }
62 |
63 | // remove the notification
64 | var removeNotification = function($notification, $sep) {
65 |
66 | if (typeof $sep != 'undefined') {
67 |
68 | $sep.animate({ opacity: 0 }, 750, function() {
69 |
70 | $sep.remove();
71 |
72 | });
73 |
74 | }
75 |
76 | $notification.animate({ opacity: 0 }, 750, function() {
77 |
78 | $notification.remove();
79 |
80 | // execute onCloseFn callback
81 | if (typeof _self.settings.onCloseFn === F) {
82 |
83 | _self.settings.onCloseFn();
84 |
85 | }
86 |
87 | });
88 |
89 | }
90 |
91 |
92 | var _self = this,
93 | defaults = {
94 | position: 'bottom right',
95 | autoclose: false,
96 | expire: 0,
97 | notification: {
98 | ntitle: '',
99 | nbody: '',
100 | icon: '',
101 | tag: '',
102 | dir: 'rtl'
103 | },
104 | onShowFn: $.noop,
105 | onClickFn: $.noop,
106 | onCloseFn: $.noop
107 | };
108 |
109 | _self.settings = {};
110 |
111 | (function() {
112 |
113 | _self.settings = $.extend({}, defaults, options);
114 |
115 | var s = _self.settings,
116 | n = s.notification,
117 | p = s.position,
118 | newNotification,
119 | $notContainer,
120 | $notBox;
121 |
122 |
123 | if (!n.ntitle || !n.nbody) {
124 |
125 | // throw an exception when required parameters are missing
126 | throw 'Title, and message of the notification are required parameters.';
127 |
128 | }
129 |
130 | // $notContainer is the fixed box that contains all the notification with a specific position
131 | $notContainer = $('#wn-' + p.replace(/ /g, ''));
132 |
133 | // $notBox is the box that contains the notification with a specific tag
134 | $notBox = $('#wn-' + n.tag);
135 |
136 |
137 | if ($notContainer.length === 0) {
138 |
139 | // create container for notification
140 | $notContainer = $('
').appendTo('body');
141 |
142 | }
143 |
144 | /* two cases:
145 | a) the notification does not have any tag, or does not still exist another notification with its same tag.
146 | b) already exists a notification with the same tag. */
147 |
148 | if (!(!!_self.settings.notification.tag && typeof _self.settings.notification.tag === S) || $notBox.length === 0) {
149 | /* (A) */
150 |
151 | // create notification, and append it to the DOM, but with display: none
152 | newNotification = getNotification();
153 | $notContainer.append(newNotification);
154 |
155 | // display the notification
156 | $notContainer.find('.wn-box').last().animate({ opacity: 1 }, 750, function() {
157 |
158 | var $box = $(this);
159 |
160 | // attach event listener: click on close notification
161 | $box.find('.wn-head .wn-close').click(function() {
162 |
163 | removeNotification($box);
164 |
165 | });
166 |
167 | // attach click on the notification
168 | bindOnClickFn($box, s.onClickFn);
169 |
170 | // execute onShowFn callback
171 | if (typeof s.onShowFn === F) {
172 |
173 | s.onShowFn();
174 |
175 | }
176 |
177 | // set autoclose
178 | if (s.autoclose) {
179 |
180 | setTimeout(function() {
181 |
182 | if ($box.find('table.wn-sep').length == 0) {
183 |
184 | removeNotification($box);
185 |
186 | }
187 | else {
188 |
189 | var $firstNotification = $box.find('.wn-body').first();
190 | removeNotification($firstNotification, $firstNotification.next('table.wn-sep'));
191 |
192 | }
193 |
194 | }, s.expire);
195 |
196 | }
197 |
198 | });
199 |
200 | }
201 | else {
202 | /* (B) */
203 |
204 | // create the new notification, and insert it after the notification with its same tag, but with display: none
205 | newNotification = '' + getBody('hidden');
206 | $notBox.append(newNotification);
207 |
208 | // display the notification
209 | $notBox.find('table.wn-sep').last().animate({ opacity: 1 }, 750);
210 | $notBox.find('.wn-body').last().animate({ opacity: 1 }, 750, function() {
211 |
212 | // shortcut selector for the trigger element of closing of the last notification
213 | var $remover = $notBox.find('.wn-close').last();
214 |
215 | // attach event listener: click on close notification
216 | $remover.click(function() {
217 |
218 | var $sep = $(this).parents('table.wn-sep');
219 | removeNotification($sep.next('.wn-body'), $sep);
220 |
221 | });
222 |
223 | // attach click on the notification
224 | bindOnClickFn($notBox, s.onClickFn);
225 |
226 | // execute onShowFn callback
227 | if (typeof s.onShowFn === F) {
228 |
229 | s.onShowFn();
230 |
231 | }
232 |
233 | // set autoclose
234 | if (s.autoclose) {
235 |
236 | setTimeout(function() {
237 |
238 | removeNotification($remover.parents('table.wn-sep').next('.wn-body'), $remover.parents('table.wn-sep'));
239 |
240 | }, s.expire);
241 |
242 | }
243 |
244 | });
245 |
246 | }
247 |
248 | }());
249 |
250 | }
251 |
252 | })(jQuery);
--------------------------------------------------------------------------------
/controllers/QueueController.php:
--------------------------------------------------------------------------------
1 |
19 | */
20 | class QueueController extends \yii\web\Controller
21 | {
22 | public function filters()
23 | {
24 | return [
25 | 'accessControl',
26 | ];
27 | }
28 |
29 | public function accessRules()
30 | {
31 | return [
32 | [
33 | 'allow',
34 | 'actions' => ['index'],
35 | 'users' => ['@'],
36 | 'roles' => ['nfy.queue.read'],
37 | ],
38 | [
39 | 'allow',
40 | 'actions' => ['messages', 'message', 'subscribe', 'unsubscribe', 'poll', 'mark'],
41 | 'users' => ['@'],
42 | ],
43 | [
44 | 'deny',
45 | 'users' => ['*'],
46 | ],
47 | ];
48 | }
49 |
50 | /**
51 | * Displays a list of queues and their subscriptions.
52 | */
53 | public function actionIndex()
54 | {
55 | /** @var User */
56 | $user = Yii::$app->user;
57 | $subscribedOnly = $user->can('nfy.queue.read.subscribed');
58 | $queues = [];
59 | foreach ($this->module->queues as $queueId) {
60 | /** @var Queue */
61 | $queue = Yii::$app->get($queueId);
62 | if (!($queue instanceof components\QueueInterface)
63 | || ($subscribedOnly
64 | && !$queue->isSubscribed($user->getId()))
65 | ) {
66 | continue;
67 | }
68 | $queues[$queueId] = $queue;
69 | }
70 |
71 | return $this->render('index', [
72 | 'queues' => $queues,
73 | 'subscribedOnly' => $subscribedOnly,
74 | ]);
75 | }
76 |
77 | /**
78 | * Subscribe current user to selected queue.
79 | * @param string $queue_name
80 | * @return string|\yii\web\Response
81 | */
82 | public function actionSubscribe($queue_name)
83 | {
84 | /** @var QueueInterface $queue */
85 | list($queue, $authItems) = $this->loadQueue($queue_name, ['nfy.queue.subscribe']);
86 |
87 | $form = new models\SubscriptionForm();
88 | if (isset($_POST['SubscriptionForm'])) {
89 | $form->attributes = $_POST['SubscriptionForm'];
90 | if ($form->validate()) {
91 | $queue->subscribe(Yii::$app->user->getId(), $form->label, $form->categories, $form->exceptions);
92 |
93 | return $this->redirect(['index']);
94 | }
95 | }
96 |
97 | return $this->render('subscription', [
98 | 'queue' => $queue,
99 | 'model' => $form,
100 | ]);
101 | }
102 |
103 | /**
104 | * Unsubscribe current user from selected queue.
105 | * @param string $queue_name
106 | * @return \yii\web\Response
107 | */
108 | public function actionUnsubscribe($queue_name)
109 | {
110 | /** @var QueueInterface $queue */
111 | list($queue, $authItems) = $this->loadQueue($queue_name, ['nfy.queue.unsubscribe']);
112 | $queue->unsubscribe(Yii::$app->user->getId());
113 |
114 | return $this->redirect(['index']);
115 | }
116 |
117 | /**
118 | * Displays and send messages in the specified queue.
119 | * @param string $queue_name
120 | * @param string $subscriber_id
121 | * @return string|\yii\web\Response
122 | */
123 | public function actionMessages($queue_name, $subscriber_id = null)
124 | {
125 | if (($subscriber_id = trim($subscriber_id)) === '') {
126 | $subscriber_id = null;
127 | }
128 | /** @var QueueInterface $queue */
129 | list($queue, $authItems) = $this->loadQueue($queue_name, ['nfy.message.read', 'nfy.message.create']);
130 | $this->verifySubscriber($queue, $subscriber_id);
131 |
132 | $formModel = new models\MessageForm();
133 | if ($authItems['nfy.message.create'] && isset($_POST['MessageForm'])) {
134 | $formModel->attributes = $_POST['MessageForm'];
135 | if ($formModel->validate()) {
136 | $queue->send($formModel->content, $formModel->category);
137 |
138 | return $this->redirect([
139 | 'messages',
140 | 'queue_name' => $queue_name,
141 | 'subscriber_id' => $subscriber_id,
142 | ]);
143 | }
144 | }
145 |
146 | $dataProvider = null;
147 | if ($authItems['nfy.message.read']) {
148 | $dataProvider = new \yii\data\ArrayDataProvider([
149 | 'allModels' => $queue->peek($subscriber_id, 200, [
150 | components\Message::AVAILABLE,
151 | components\Message::RESERVED,
152 | components\Message::DELETED,
153 | ]),
154 | 'sort' => [
155 | 'attributes' => ['id'],
156 | 'defaultOrder' => ['id' => SORT_DESC],
157 | ],
158 | ]);
159 | // reverse display order to simulate a chat window, where latest message is right above the message form
160 | $dataProvider->setModels(array_reverse($dataProvider->getModels()));
161 | }
162 |
163 | return $this->render('messages', [
164 | 'queue' => $queue,
165 | 'queue_name' => $queue_name,
166 | 'dataProvider' => $dataProvider,
167 | 'model' => $formModel,
168 | 'authItems' => $authItems,
169 | ]);
170 | }
171 |
172 | /**
173 | * Marks all messages or specified message as read.
174 | * @param $queue_name
175 | * @param mixed $message_id
176 | * @param mixed $subscriber_id
177 | * @param string $returnUrl
178 | * @return \yii\web\Response
179 | */
180 | public function actionMark($queue_name, $message_id = null, $subscriber_id = null, $returnUrl = null)
181 | {
182 | if (($subscriber_id = trim($subscriber_id)) === '') {
183 | $subscriber_id = null;
184 | }
185 | /** @var QueueInterface $queue */
186 | list($queue, $authItems) = $this->loadQueue($queue_name, ['nfy.message.read']);
187 | $this->verifySubscriber($queue, $subscriber_id);
188 |
189 | if ($authItems['nfy.message.read']) {
190 | if ($message_id === null) {
191 | $queue->receive($subscriber_id);
192 | } else {
193 | $queue->delete($message_id, $subscriber_id);
194 | }
195 | }
196 | return $this->redirect($returnUrl !== null ? $returnUrl : Yii::$app->user->returnUrl);
197 | }
198 |
199 | /**
200 | * Fetches details of a single message, allows to release or delete it or sends a new message.
201 | * @param string $queue_name
202 | * @param string $subscriber_id
203 | * @param string $message_id
204 | * @return string|\yii\web\Response
205 | * @throws ForbiddenHttpException
206 | * @throws NotFoundHttpException
207 | */
208 | public function actionMessage($queue_name, $subscriber_id = null, $message_id = null)
209 | {
210 | if (($subscriber_id = trim($subscriber_id)) === '') {
211 | $subscriber_id = null;
212 | }
213 | /** @var QueueInterface $queue */
214 | list($queue, $authItems) = $this->loadQueue($queue_name, ['nfy.message.read', 'nfy.message.create']);
215 | $this->verifySubscriber($queue, $subscriber_id);
216 |
217 | if ($queue instanceof components\DbQueue) {
218 | $query = models\DbMessage::find()->withQueue($queue->id);
219 | if ($subscriber_id !== null) {
220 | $query->withSubscriber($subscriber_id);
221 | }
222 |
223 | $dbMessage = $query->andWhere([
224 | 'in',
225 | models\DbMessage::tableName().'.'.models\DbMessage::primaryKey()[0],
226 | $message_id,
227 | ])->one();
228 | if ($dbMessage === null) {
229 | throw new NotFoundHttpException(Yii::t("app", 'Message with given ID was not found.'));
230 | }
231 | $messages = models\DbMessage::createMessages($dbMessage);
232 | $message = reset($messages);
233 | } else {
234 | $dbMessage = null;
235 | //! @todo should we even bother to locate a single message by id?
236 | $message = new components\Message();
237 | $message->setAttributes([
238 | 'id' => $message_id,
239 | 'subscriber_id' => $subscriber_id,
240 | 'status' => components\Message::AVAILABLE,
241 | ]);
242 | }
243 |
244 | if (isset($_POST['delete'])) {
245 | $queue->delete($message->id, $message->subscriber_id);
246 |
247 | return $this->redirect([
248 | 'messages',
249 | 'queue_name' => $queue_name,
250 | 'subscriber_id' => $message->subscriber_id,
251 | ]);
252 | }
253 |
254 | return $this->render('message', [
255 | 'queue' => $queue,
256 | 'queue_name' => $queue_name,
257 | 'dbMessage' => $dbMessage,
258 | 'message' => $message,
259 | 'authItems' => $authItems,
260 | ]);
261 | }
262 |
263 | /**
264 | * Loads queue specified by id and checks authorization.
265 | * @param string $name queue component name
266 | * @param array $authItems
267 | * @return array QueueInterface object and array with authItems as keys and boolean values
268 | * @throws ForbiddenHttpException
269 | * @throws NotFoundHttpException
270 | */
271 | protected function loadQueue($name, $authItems = [])
272 | {
273 | /** @var User */
274 | $user = Yii::$app->user;
275 | /** @var Queue */
276 | $queue = Yii::$app->get($name);
277 | if (!($queue instanceof components\QueueInterface)) {
278 | throw new NotFoundHttpException(Yii::t("app", 'Queue with given ID was not found.'));
279 | }
280 | $assignedAuthItems = [];
281 | $allowAccess = empty($authItems);
282 | foreach ($authItems as $authItem) {
283 | $assignedAuthItems[$authItem] = $user->can($authItem, ['queue' => $queue]);
284 | if ($assignedAuthItems[$authItem]) {
285 | $allowAccess = true;
286 | }
287 | }
288 | if (!$allowAccess) {
289 | throw new ForbiddenHttpException(Yii::t('yii', 'You are not authorized to perform this action.'));
290 | }
291 |
292 | return [$queue, $assignedAuthItems];
293 | }
294 |
295 | /**
296 | * Checks if current user can read only messages from subscribed queues and is subscribed.
297 | * @param QueueInterface $queue
298 | * @param integer $subscriber_id
299 | * @throws ForbiddenHttpException
300 | */
301 | protected function verifySubscriber($queue, $subscriber_id)
302 | {
303 | /** @var User */
304 | $user = Yii::$app->user;
305 | $subscribedOnly = $user->can('nfy.message.read.subscribed');
306 | if ($subscribedOnly && (!$queue->isSubscribed($user->getId()) || $subscriber_id != $user->getId())) {
307 | throw new ForbiddenHttpException(Yii::t('yii', 'You are not authorized to perform this action.'));
308 | }
309 | }
310 |
311 | /**
312 | * @param string $id id of the queue component
313 | * @param boolean $subscribed should the queue be checked using current user's subscription
314 | * @return array
315 | * @throws ForbiddenHttpException
316 | */
317 | public function actionPoll($id, $subscribed = true)
318 | {
319 | $userId = Yii::$app->user->getId();
320 | $queue = Yii::$app->get($id);
321 | if (!($queue instanceof components\QueueInterface)) {
322 | return [];
323 | }
324 | if (!Yii::$app->user->can('nfy.message.read', ['queue' => $queue])) {
325 | throw new ForbiddenHttpException(Yii::t('yii', 'You are not authorized to perform this action.'));
326 | }
327 |
328 | Yii::$app->session->close();
329 |
330 | $data = [];
331 | $data['messages'] = $this->getMessages($queue, $subscribed ? $userId : null);
332 |
333 | /** @var Module $module */
334 | $module = Yii::$app->getModule($this->module);
335 | $pollFor = $module->longPolling;
336 | $maxPoll = $module->maxPollCount;
337 | if ($pollFor && $maxPoll && empty($data['messages'])) {
338 | while (empty($data['messages']) && $maxPoll) {
339 | $data['messages'] = $this->getMessages($queue, $subscribed ? $userId : null);
340 | usleep($pollFor * 1000);
341 | $maxPoll--;
342 | }
343 | }
344 |
345 | if (empty($data['messages'])) {
346 | Yii::$app->response->setStatusCode(304);
347 | Yii::$app->end();
348 | return;
349 | }
350 | Yii::$app->response->format = 'application/json';
351 | $this->view->jsFiles = [];
352 | $this->view->cssFiles = [];
353 | echo json_encode($data);
354 | }
355 |
356 | /**
357 | * Fetches messages from a queue and deletes them. Messages are transformed into a json serializable array.
358 | * If a sound is configured in the module, an url is added to each message.
359 | *
360 | * Only first 20 messages are returned but all available messages are deleted from the queue.
361 | *
362 | * @param QueueInterface $queue
363 | * @param string $userId
364 | * @return array
365 | */
366 | protected function getMessages($queue, $userId)
367 | {
368 | $messages = $queue->receive($userId);
369 |
370 | if (empty($messages)) {
371 | return [];
372 | }
373 |
374 | $messages = array_slice($messages, 0, 20);
375 | /** @var Module $module */
376 | $module = Yii::$app->getModule($this->module);
377 | $soundUrl = $module->soundUrl !== null ? Url::to($module->soundUrl) : null;
378 |
379 | $results = [];
380 | foreach ($messages as $message) {
381 | $result = [
382 | 'title' => $queue->label,
383 | 'body' => $message->body,
384 | ];
385 | if ($soundUrl !== null) {
386 | $result['sound'] = $soundUrl;
387 | }
388 | $results[] = $result;
389 | }
390 |
391 | return $results;
392 | }
393 |
394 | public function createMessageUrl($queue_name, components\Message $message)
395 | {
396 | return Url::toRoute('message', [
397 | 'queue_name' => $queue_name,
398 | 'subscriber_id' => $message->subscriber_id,
399 | 'message_id' => $message->id,
400 | ]);
401 | }
402 | }
403 |
--------------------------------------------------------------------------------
/components/RedisQueue.php:
--------------------------------------------------------------------------------
1 | redis)) {
39 | $this->redis = Yii::$app->get($this->redis);
40 | } elseif (is_array($this->redis)) {
41 | }
42 | if (!($this->redis instanceof yii\redis\Connection)) {
43 | throw new InvalidConfigException('The redis property must contain a name or a valid yii\redis\Connection component.');
44 | }
45 | }
46 |
47 | /**
48 | * Creates an instance of Message model. The passed message body may be modified, @see formatMessage().
49 | * This method may be overriden in extending classes.
50 | * @param string $body message body
51 | * @return Message
52 | */
53 | protected function createMessage($body)
54 | {
55 | $now = new \DateTime('now', new \DateTimezone('UTC'));
56 | $message = new Message();
57 | $message->setAttributes([
58 | 'id' => $this->redis->incr($this->id.self::MESSAGE_ID),
59 | 'status' => Message::AVAILABLE,
60 | 'created_on' => $now->format('Y-m-d H:i:s'),
61 | 'sender_id' => Yii::$app->has('user') ? Yii::$app->user->getId() : null,
62 | 'body' => $body,
63 | ]);
64 |
65 | return $this->formatMessage($message);
66 | }
67 |
68 | /**
69 | * Formats the body of a queue message. This method may be overriden in extending classes.
70 | * @param Message $message
71 | * @return Message $message
72 | */
73 | protected function formatMessage($message)
74 | {
75 | return $message;
76 | }
77 |
78 | /**
79 | * @inheritdoc
80 | */
81 | public function send($message, $category = null)
82 | {
83 | $queueMessage = $this->createMessage($message);
84 |
85 | if ($this->beforeSend($queueMessage) !== true) {
86 | Yii::info(Yii::t('app', "Not sending message '{msg}' to queue {queue_label}.", [
87 | 'msg' => $queueMessage->body,
88 | 'queue_label' => $this->label,
89 | ]), 'nfy');
90 |
91 | return;
92 | }
93 |
94 | if ($this->blocking) {
95 | $this->redis->publish($category, serialize($queueMessage));
96 | } else {
97 | $this->sendToList($queueMessage, $category);
98 | }
99 |
100 | $this->afterSend($queueMessage);
101 |
102 | Yii::info(Yii::t('app', "Sent message '{msg}' to queue {queue_label}.", [
103 | 'msg' => $queueMessage->body,
104 | 'queue_label' => $this->label,
105 | ]), 'nfy');
106 | }
107 |
108 | private function sendToList($queueMessage, $category)
109 | {
110 | $subscriptions = $this->redis->hvals($this->id.self::SUBSCRIPTIONS_HASH);
111 |
112 | $this->redis->multi();
113 |
114 | $this->redis->lpush($this->id, serialize($queueMessage));
115 |
116 | foreach ($subscriptions as $rawSubscription) {
117 | $subscription = unserialize($rawSubscription);
118 | if ($category !== null && !$subscription->matchCategory($category)) {
119 | continue;
120 | }
121 | $subscriptionMessage = clone $queueMessage;
122 | if ($this->beforeSendSubscription($subscriptionMessage, $subscription->subscriber_id) !== true) {
123 | continue;
124 | }
125 |
126 | $this->redis->lpush($this->id.self::SUBSCRIPTION_LIST_PREFIX.$subscription->subscriber_id, serialize($subscriptionMessage));
127 |
128 | $this->afterSendSubscription($subscriptionMessage, $subscription->subscriber_id);
129 | }
130 |
131 | $this->redis->exec();
132 | }
133 |
134 | /**
135 | * @inheritdoc
136 | * @throws InvalidConfigException
137 | */
138 | public function peek($subscriber_id = null, $limit = -1, $status = Message::AVAILABLE)
139 | {
140 | if ($this->blocking) {
141 | throw new NotSupportedException('When in blocking mode peeking is not available. Use the receive() method.');
142 | }
143 | //! @todo implement peeking at other lists, joining, sorting results by date and limiting,
144 | // remember about settings status after unserialize
145 | $list_id = $this->id.($subscriber_id === null ? '' : self::SUBSCRIPTION_LIST_PREFIX.$subscriber_id);
146 | $messages = [];
147 | foreach ($this->redis->lrange($list_id, 0, $limit) as $rawMessage) {
148 | $message = unserialize($rawMessage);
149 | $message->subscriber_id = $subscriber_id;
150 | $message->status = Message::AVAILABLE;
151 | $messages[] = $message;
152 | }
153 |
154 | return $messages;
155 | }
156 |
157 | /**
158 | * @inheritdoc
159 | * @throws InvalidConfigException
160 | */
161 | public function reserve($subscriber_id = null, $limit = -1)
162 | {
163 | if ($this->blocking) {
164 | throw new NotSupportedException('When in blocking mode reserving is not available. Use the receive() method.');
165 | }
166 |
167 | $messages = [];
168 | $count = 0;
169 | $list_id = $this->id.($subscriber_id === null ? '' : self::SUBSCRIPTION_LIST_PREFIX.$subscriber_id);
170 | $reserved_list_id = $list_id.self::RESERVED_LIST;
171 | $now = new \DateTime();
172 | $this->redis->multi();
173 | while (($limit == -1 || $count < $limit)) {
174 | if (($rawMessage = $this->redis->rpop($list_id)) === null) {
175 | break;
176 | }
177 | $message = unserialize($rawMessage);
178 | $message->setAttributes([
179 | 'status' => Message::RESERVED,
180 | 'reserved_on' => $now->format('Y-m-d H:i:s'),
181 | 'subscriber_id' => $subscriber_id,
182 | ]);
183 | $this->redis->lpush($reserved_list_id, serialize($message));
184 | $messages[] = $message;
185 | $count++;
186 | }
187 | $this->redis->exec();
188 |
189 | return $messages;
190 | }
191 |
192 | /**
193 | * @inheritdoc
194 | * The result does not include reserved but timed-out messages. @see releaseTimedout().
195 | */
196 | public function receive($subscriber_id = null, $limit = -1)
197 | {
198 | $messages = [];
199 | $count = 0;
200 | if ($this->blocking) {
201 | $response = $this->redis->parseResponse('', true);
202 | if (is_array($response)) {
203 | $type = array_shift($reponse);
204 | if ($type == 'message') {
205 | $channel = array_shift($response);
206 | $message = array_shift($response);
207 | } elseif ($type == 'pmessage') {
208 | $pattern = array_shift($response);
209 | $channel = array_shift($response);
210 | $message = array_shift($response);
211 | }
212 | if (isset($message)) {
213 | $messages[] = $message;
214 | }
215 | }
216 |
217 | return $messages;
218 | }
219 | $list_id = $this->id.($subscriber_id === null ? '' : self::SUBSCRIPTION_LIST_PREFIX.$subscriber_id);
220 | while (($limit == -1 || $count < $limit) && ($message = $this->redis->rpop($list_id)) !== null) {
221 | $message = unserialize($rawMessage);
222 | $message->subscriber_id = $subscriber_id;
223 | $message->status = Message::AVAILABLE;
224 | $messages[] = $message;
225 | $count++;
226 | //! @todo implement moving messages to :deleted queue (optionally, if configured)
227 | }
228 |
229 | return $messages;
230 | }
231 |
232 | /**
233 | * @inheritdoc
234 | * @throws NotSupportedException
235 | */
236 | public function delete($message_id, $subscriber_id = null)
237 | {
238 | if ($this->blocking) {
239 | throw new NotSupportedException('When in blocking mode reserving is not available. Use the receive() method.');
240 | }
241 | $this->releaseInternal($message_id, true);
242 | }
243 |
244 | /**
245 | * @inheritdoc
246 | * @throws NotSupportedException
247 | */
248 | public function release($message_id, $subscriber_id = null)
249 | {
250 | if ($this->blocking) {
251 | throw new NotSupportedException('When in blocking mode reserving is not available. Use the receive() method.');
252 | }
253 | $this->releaseInternal($message_id, false);
254 | }
255 |
256 | private function releaseInternal($message_id, $delete = false)
257 | {
258 | if (!is_array($message_id)) {
259 | $message_id = [$message_id];
260 | }
261 | $message_id = array_flip($message_id);
262 | $this->redis->multi();
263 | $list_id = $this->id.($subscriber_id === null ? '' : self::SUBSCRIPTION_LIST_PREFIX.$subscriber_id);
264 | $reserved_list_id = $list_id.self::RESERVED_LIST;
265 | $messages = array_reverse($this->redis->lrange($reserved_list_id, 0, -1));
266 | foreach ($messages as $rawMessage) {
267 | $message = unserialize($rawMessage);
268 | if (isset($message_id[$message->id])) {
269 | $this->redis->lrem($reserved_list_id, $rawMessage, -1);
270 | if (!$delete) {
271 | $this->redis->lpush($list_id, $rawMessage);
272 | } else {
273 | //! @todo implement moving messages to :deleted queue (optionally, if configured)
274 | }
275 | }
276 | }
277 | $this->redis->exec();
278 | }
279 |
280 | /**
281 | * @inheritdoc
282 | */
283 | public function releaseTimedout()
284 | {
285 | $keys = array_merge(
286 | $this->redis->keys($this->id.self::RESERVED_LIST),
287 | $this->redis->keys($this->id.self::SUBSCRIPTION_LIST_PREFIX.'*'.self::RESERVED_LIST)
288 | );
289 | $message_ids = [];
290 |
291 | $this->redis->multi();
292 | foreach ($keys as $reserved_list_id) {
293 | $list_id = substr($reserved_list_id, 0, -strlen(self::RESERVED_LIST));
294 | $messages = array_reverse($this->redis->lrange($reserved_list_id, 0, -1));
295 | $now = new \DateTime();
296 | foreach ($messages as $rawMessage) {
297 | $message = unserialize($rawMessage);
298 | $reserved_on = new \DateTime($message->reserved_on);
299 | if ($reserved_on->add(new DateInterval('PT'.$message->timeout.'S')) <= $now) {
300 | $this->redis->lrem($reserved_list_id, $rawMessage, -1);
301 | $this->redis->lpush($list_id, $rawMessage);
302 | $message_ids[] = $message->id;
303 | }
304 | }
305 | }
306 | $this->redis->exec();
307 |
308 | return $message_ids;
309 | }
310 |
311 | /**
312 | * @inheritdoc
313 | * @throws NotSupportedException
314 | */
315 | public function subscribe($subscriber_id, $label = null, $categories = null, $exceptions = null)
316 | {
317 | if ($this->blocking) {
318 | if ($exceptions !== null) {
319 | throw new NotSupportedException('Redis queues does not support pattern exceptions in blocking (pubsub) mode.');
320 | }
321 | foreach ($categories as $category) {
322 | if (($c = rtrim($category, '*')) !== $category) {
323 | $this->redis->psubscribe($category);
324 | } else {
325 | $this->redis->subscribe($category);
326 | }
327 | }
328 |
329 | return;
330 | }
331 | $now = new \DateTime('now', new \DateTimezone('UTC'));
332 | $subscription = new Subscription();
333 | $subscription->setAttributes([
334 | 'subscriber_id' => $subscriber_id,
335 | 'label' => $label,
336 | 'categories' => $categories,
337 | 'exceptions' => $exceptions !== null ? $exceptions : [],
338 | 'created_on' => $now->format('Y-m-d H:i:s'),
339 | ]);
340 | $this->redis->hset($this->id.self::SUBSCRIPTIONS_HASH, $subscriber_id, serialize($subscription));
341 | }
342 |
343 | /**
344 | * @inheritdoc
345 | */
346 | public function unsubscribe($subscriber_id, $categories = null)
347 | {
348 | if ($this->blocking) {
349 | if ($categories === null) {
350 | $this->redis->punsubscribe();
351 | $this->redis->unsubscribe();
352 | } else {
353 | foreach ($categories as $category) {
354 | if (($c = rtrim($category, '*')) !== $category) {
355 | $this->redis->punsubscribe($category);
356 | } else {
357 | $this->redis->unsubscribe($category);
358 | }
359 | }
360 | }
361 |
362 | return;
363 | }
364 | if ($categories === null) {
365 | $this->redis->hdel($this->id.self::SUBSCRIPTIONS_HASH, $subscriber_id);
366 | } else {
367 | $subscription = unserialize($this->redis->hget($this->id.self::SUBSCRIPTIONS_HASH, $subscriber_id));
368 | $subscription->categories = array_diff($subscription->categories, $categories);
369 | $this->redis->hset($this->id.self::SUBSCRIPTIONS_HASH, $subscriber_id, serialize($subscription));
370 | }
371 | }
372 |
373 | /**
374 | * @inheritdoc
375 | * @throws NotSupportedException
376 | */
377 | public function isSubscribed($subscriber_id)
378 | {
379 | if ($this->blocking) {
380 | throw new NotSupportedException('In blocking mode it is not possible to track subscribers.');
381 |
382 | return;
383 | }
384 |
385 | return $this->redis->hexists($this->id.self::SUBSCRIPTIONS_HASH, $subscriber_id);
386 | }
387 |
388 | /**
389 | * @inheritdoc
390 | */
391 | public function getSubscriptions($subscriber_id = null)
392 | {
393 | $subscriptions = [];
394 | foreach ($this->redis->hvals($this->id.self::SUBSCRIPTIONS_HASH) as $rawSubscription) {
395 | $subscriptions[] = unserialize($rawSubscription);
396 | }
397 |
398 | return $subscriptions;
399 | }
400 | }
401 |
--------------------------------------------------------------------------------
/components/DbQueue.php:
--------------------------------------------------------------------------------
1 | blocking) {
23 | throw new NotSupportedException(Yii::t('app', 'DbQueue does not support blocking.'));
24 | }
25 | }
26 |
27 | /**
28 | * Creates an instance of DbMessage model. The passed message body may be modified, @see formatMessage().
29 | * This method may be overriden in extending classes.
30 | * @param string $body message body
31 | * @return models\DbMessage
32 | */
33 | protected function createMessage($body)
34 | {
35 | $message = new models\DbMessage();
36 | $message->setAttributes([
37 | 'queue_id' => $this->id,
38 | 'timeout' => $this->timeout,
39 | 'sender_id' => Yii::$app->has('user') ? Yii::$app->user->getId() : null,
40 | 'status' => Message::AVAILABLE,
41 | 'body' => $body,
42 | ], false);
43 |
44 | return $this->formatMessage($message);
45 | }
46 |
47 | /**
48 | * Formats the body of a queue message. This method may be overriden in extending classes.
49 | * @param models\DbMessage $message
50 | * @return models\DbMessage $message
51 | */
52 | protected function formatMessage($message)
53 | {
54 | return $message;
55 | }
56 |
57 | /**
58 | * @param Subscription[] $subscriptions
59 | * @param models\DbMessage $queueMessage
60 | * @return bool
61 | */
62 | private function sendToSubscriptions($subscriptions, $queueMessage)
63 | {
64 | $success = true;
65 | foreach ($subscriptions as $subscription) {
66 | $subscriptionMessage = clone $queueMessage;
67 | $subscriptionMessage->subscription_id = $subscription->id;
68 | $subscriptionMessage->message_id = $queueMessage->id;
69 | if ($this->beforeSendSubscription($subscriptionMessage, $subscription->subscriber_id) !== true) {
70 | continue;
71 | }
72 |
73 | if (!$subscriptionMessage->save()) {
74 | Yii::error(Yii::t('app', "Failed to save message '{msg}' in queue {queue_label} for the subscription {subscription_id}.", [
75 | 'msg' => $queueMessage->body,
76 | 'queue_label' => $this->label,
77 | 'subscription_id' => $subscription->id,
78 | ]) . ' ' . print_r($subscriptionMessage->getErrors(), true), 'nfy');
79 | $success = false;
80 | }
81 |
82 | $this->afterSendSubscription($subscriptionMessage, $subscription->subscriber_id);
83 | }
84 | return $success;
85 | }
86 |
87 | /**
88 | * @inheritdoc
89 | */
90 | public function send($message, $category = null)
91 | {
92 | $queueMessage = $this->createMessage($message);
93 |
94 | if ($this->beforeSend($queueMessage) !== true) {
95 | Yii::info(Yii::t('app', "Not sending message '{msg}' to queue {queue_label}.", [
96 | 'msg' => $queueMessage->body,
97 | 'queue_label' => $this->label,
98 | ]), 'nfy');
99 |
100 | return;
101 | }
102 |
103 | $success = true;
104 |
105 | $subscriptions = models\DbSubscription::find()->current()->withQueue($this->id)->matchingCategory($category)->all();
106 |
107 | $trx = $queueMessage->getDb()->transaction !== null ? null : $queueMessage->getDb()->beginTransaction();
108 |
109 | // empty($subscriptions) &&
110 | if (!$queueMessage->save()) {
111 | Yii::error(Yii::t('app', "Failed to save message '{msg}' in queue {queue_label}.", [
112 | 'msg' => $queueMessage->body,
113 | 'queue_label' => $this->label,
114 | ]) . ' ' . print_r($queueMessage->getErrors(), true), 'nfy');
115 |
116 | return false;
117 | }
118 |
119 | if (!$this->sendToSubscriptions($subscriptions, $queueMessage)) {
120 | $success = false;
121 | }
122 |
123 | $this->afterSend($queueMessage);
124 |
125 | if ($trx !== null) {
126 | $trx->commit();
127 | }
128 |
129 | Yii::info(Yii::t('app', "Sent message '{msg}' to queue {queue_label}.", [
130 | 'msg' => $queueMessage->body,
131 | 'queue_label' => $this->label,
132 | ]), 'nfy');
133 |
134 | return $success;
135 | }
136 |
137 | /**
138 | * @inheritdoc
139 | */
140 | public function peek($subscriber_id = null, $limit = -1, $status = Message::AVAILABLE)
141 | {
142 | $primaryKey = models\DbMessage::primaryKey();
143 | $messages = models\DbMessage::find()
144 | ->withQueue($this->id)
145 | ->withSubscriber($subscriber_id)
146 | ->withStatus($status, $this->timeout)
147 | ->limit($limit)
148 | ->indexBy($primaryKey[0])
149 | ->all();
150 |
151 | return models\DbMessage::createMessages($messages);
152 | }
153 |
154 | /**
155 | * @inheritdoc
156 | */
157 | public function reserve($subscriber_id = null, $limit = -1)
158 | {
159 | return $this->receiveInternal($subscriber_id, $limit, self::GET_RESERVE);
160 | }
161 |
162 | /**
163 | * @inheritdoc
164 | */
165 | public function receive($subscriber_id = null, $limit = -1)
166 | {
167 | return $this->receiveInternal($subscriber_id, $limit, self::GET_DELETE);
168 | }
169 |
170 | /**
171 | * Perform message extraction.
172 | * @param mixed $subscriber_id
173 | * @param int $limit
174 | * @param int $mode one of: self::GET_DELETE, self::GET_RESERVE or self::GET_PEEK
175 | * @return models\DbMessage[]
176 | * @throws \yii\db\Exception
177 | */
178 | protected function receiveInternal($subscriber_id = null, $limit = -1, $mode = self::GET_RESERVE)
179 | {
180 | $primaryKey = models\DbMessage::primaryKey();
181 | $trx = models\DbMessage::getDb()->transaction !== null ? null : models\DbMessage::getDb()->beginTransaction();
182 | $messages = models\DbMessage::find()
183 | ->withQueue($this->id)
184 | ->withSubscriber($subscriber_id)
185 | ->available($this->timeout)
186 | ->limit($limit)
187 | ->indexBy($primaryKey[0])
188 | ->all();
189 | if (!empty($messages)) {
190 | $now = new \DateTime('now', new \DateTimezone('UTC'));
191 | if ($mode === self::GET_DELETE) {
192 | $attributes = ['status' => Message::DELETED, 'deleted_on' => $now->format('Y-m-d H:i:s')];
193 | } elseif ($mode === self::GET_RESERVE) {
194 | $attributes = ['status' => Message::RESERVED, 'reserved_on' => $now->format('Y-m-d H:i:s')];
195 | }
196 | if (isset($attributes)) {
197 | models\DbMessage::updateAll($attributes, ['in', models\DbMessage::primaryKey(), array_keys($messages)]);
198 | }
199 | }
200 | if ($trx !== null) {
201 | $trx->commit();
202 | }
203 |
204 | return models\DbMessage::createMessages($messages);
205 | }
206 |
207 | /**
208 | * @inheritdoc
209 | */
210 | public function delete($message_id, $subscriber_id = null)
211 | {
212 | $trx = models\DbMessage::getDb()->transaction !== null ? null : models\DbMessage::getDb()->beginTransaction();
213 | $primaryKey = models\DbMessage::primaryKey();
214 | $tableName = models\DbMessage::tableName();
215 | $message_ids = array_map(function ($row) use ($primaryKey) {
216 | return array_intersect_key($row, array_flip($primaryKey));
217 | }, models\DbMessage::find()
218 | ->withQueue($this->id)
219 | ->withSubscriber($subscriber_id)
220 | ->select(array_map(function ($pk) use ($tableName) { return $tableName.'.'.$pk; }, array_merge($primaryKey, ['subscription_id'])))
221 | ->andWhere(['in', $tableName.'.id', $message_id])
222 | ->asArray()
223 | ->all()
224 | );
225 | $now = new \DateTime('now', new \DateTimezone('UTC'));
226 | models\DbMessage::updateAll([
227 | 'status' => Message::DELETED,
228 | 'deleted_on' => $now->format('Y-m-d H:i:s'),
229 | ], ['in', $primaryKey, $message_ids]);
230 | if ($trx !== null) {
231 | $trx->commit();
232 | }
233 |
234 | return $message_ids;
235 | }
236 |
237 | /**
238 | * @inheritdoc
239 | */
240 | public function release($message_id, $subscriber_id = null)
241 | {
242 | $trx = models\DbMessage::getDb()->transaction !== null ? null : models\DbMessage::getDb()->beginTransaction();
243 | $primaryKey = models\DbMessage::primaryKey();
244 | $tableName = models\DbMessage::tableName();
245 | $message_ids = array_map(function ($row) use ($primaryKey) {
246 | return array_intersect_key($row, array_flip($primaryKey));
247 | }, models\DbMessage::find()
248 | ->withQueue($this->id)
249 | ->withSubscriber($subscriber_id)
250 | ->reserved($this->timeout)
251 | ->select(array_map(function ($pk) use ($tableName) { return $tableName.'.'.$pk; }, array_merge($primaryKey, ['subscription_id'])))
252 | ->andWhere(['in', $tableName.'.id', $message_id])
253 | ->asArray()
254 | ->all()
255 | );
256 | models\DbMessage::updateAll(['status' => Message::AVAILABLE], ['in', $primaryKey, $message_ids]);
257 | if ($trx !== null) {
258 | $trx->commit();
259 | }
260 |
261 | return $message_ids;
262 | }
263 |
264 | /**
265 | * Releases timed-out messages.
266 | * @return array of released message ids
267 | */
268 | public function releaseTimedout()
269 | {
270 | $trx = models\DbMessage::getDb()->transaction !== null ? null : models\DbMessage::getDb()->beginTransaction();
271 | $primaryKey = models\DbMessage::primaryKey();
272 | $message_ids = models\DbMessage::find()
273 | ->withQueue($this->id)
274 | ->timedout($this->timeout)
275 | ->select($primaryKey)
276 | ->asArray()
277 | ->all();
278 | models\DbMessage::updateAll(['status' => Message::AVAILABLE], ['in', $primaryKey, $message_ids]);
279 | if ($trx !== null) {
280 | $trx->commit();
281 | }
282 |
283 | return $message_ids;
284 | }
285 |
286 | /**
287 | * @inheritdoc
288 | */
289 | public function subscribe($subscriber_id, $label = null, $categories = null, $exceptions = null)
290 | {
291 | $trx = models\DbSubscription::getDb()->transaction !== null ? null : models\DbSubscription::getDb()->beginTransaction();
292 | $subscription = models\DbSubscription::find()->withQueue($this->id)->withSubscriber($subscriber_id)->one();
293 | if ($subscription === null) {
294 | $subscription = new models\DbSubscription();
295 | $subscription->setAttributes([
296 | 'queue_id' => $this->id,
297 | 'subscriber_id' => $subscriber_id,
298 | 'label' => $label,
299 | ]);
300 | } else {
301 | $subscription->is_deleted = 0;
302 | }
303 | if (!$subscription->save()) {
304 | throw new Exception(Yii::t('app', 'Failed to subscribe {subscriber_id} to {queue_label}', [
305 | 'subscriber_id' => $subscriber_id,
306 | 'queue_label' => $this->label,
307 | ]));
308 | }
309 | $this->saveSubscriptionCategories($categories, $subscription->primaryKey, false);
310 | $this->saveSubscriptionCategories($exceptions, $subscription->primaryKey, true);
311 | if ($trx !== null) {
312 | $trx->commit();
313 | }
314 |
315 | return true;
316 | }
317 |
318 | protected function saveSubscriptionCategories($categories, $subscription_id, $are_exceptions = false)
319 | {
320 | if ($categories === null) {
321 | return true;
322 | }
323 | if (!is_array($categories)) {
324 | $categories = [$categories];
325 | }
326 | foreach ($categories as $category) {
327 | $subscriptionCategory = new models\DbSubscriptionCategory();
328 | $subscriptionCategory->setAttributes([
329 | 'subscription_id' => $subscription_id,
330 | 'category' => str_replace('*', '%', $category),
331 | 'is_exception' => $are_exceptions ? 1 : 0,
332 | ]);
333 | if (!$subscriptionCategory->save()) {
334 | throw new Exception(Yii::t('app', 'Failed to save category {category} for subscription {subscription_id}', [
335 | 'category' => $category,
336 | 'subscription_id' => $subscription_id,
337 | ]));
338 | }
339 | }
340 |
341 | return true;
342 | }
343 |
344 | /**
345 | * @inheritdoc
346 | * @param boolean $permanent if false, the subscription will only be marked as removed
347 | * and the messages will remain in the storage;
348 | * if true, everything is removed permanently
349 | */
350 | public function unsubscribe($subscriber_id, $categories = null, $permanent = true)
351 | {
352 | $trx = models\DbSubscription::getDb()->transaction !== null ? null : models\DbSubscription::getDb()->beginTransaction();
353 | $subscription = models\DbSubscription::find()
354 | ->withQueue($this->id)
355 | ->withSubscriber($subscriber_id)
356 | ->matchingCategory($categories)
357 | ->one();
358 | if ($subscription !== null) {
359 | $canDelete = true;
360 | if ($categories !== null) {
361 | // it may be a case when some (but not all) categories are about to be unsubscribed
362 | // if that happens and this subscription ends up with some other categories, only given categories
363 | // should be deleted, not the whole subscription
364 | $primaryKey = models\DbSubscriptionCategory::primaryKey();
365 | models\DbSubscriptionCategory::deleteAll([
366 | reset($primaryKey) => array_map(function ($c) { return $c->id; }, $subscription->categories)
367 | ]);
368 | $canDelete = models\DbSubscriptionCategory::find()->where([
369 | 'subscription_id' => $subscription->id,
370 | ])->count() <= 0;
371 | }
372 |
373 | if ($canDelete) {
374 | if ($permanent) {
375 | $subscription->delete();
376 | } else {
377 | $subscription->is_deleted = 1;
378 | $subscription->update(true, ['is_deleted']);
379 | }
380 | }
381 | }
382 | if ($trx !== null) {
383 | $trx->commit();
384 | }
385 | }
386 |
387 | /**
388 | * @inheritdoc
389 | */
390 | public function isSubscribed($subscriber_id, $category = null)
391 | {
392 | $subscription = models\DbSubscription::find()
393 | ->current()
394 | ->withQueue($this->id)
395 | ->withSubscriber($subscriber_id)
396 | ->matchingCategory($category)
397 | ->one();
398 |
399 | return $subscription !== null;
400 | }
401 |
402 | /**
403 | * @param mixed $subscriber_id
404 | * @return array|models\DbSubscription
405 | */
406 | public function getSubscriptions($subscriber_id = null)
407 | {
408 | /** @var $query \yii\db\ActiveQuery */
409 | $query = models\DbSubscription::find()
410 | ->current()
411 | ->withQueue($this->id)
412 | ->with(['categories']);
413 | if ($subscriber_id !== null) {
414 | $dbSubscriptions = $query->andWhere('subscriber_id=:subscriber_id', [':subscriber_id' => $subscriber_id]);
415 | }
416 | $dbSubscriptions = $query->all();
417 |
418 | return models\DbSubscription::createSubscriptions($dbSubscriptions);
419 | }
420 |
421 | /**
422 | * Removes deleted messages from the storage.
423 | * @return array of removed message ids
424 | */
425 | public function removeDeleted()
426 | {
427 | $trx = models\DbMessage::getDb()->transaction !== null ? null : models\DbMessage::getDb()->beginTransaction();
428 | $primaryKey = models\DbMessage::primaryKey();
429 | $message_ids = models\DbMessage::find()
430 | ->withQueue($this->id)
431 | ->deleted()
432 | ->select($primaryKey)
433 | ->asArray()
434 | ->all();
435 | models\DbMessage::deleteAll(['in', $primaryKey, $message_ids]);
436 | if ($trx !== null) {
437 | $trx->commit();
438 | }
439 |
440 | return $message_ids;
441 | }
442 | }
443 |
--------------------------------------------------------------------------------