├── 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 |

label); ?> $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 |

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 |
13 | 14 | 'message-form', 16 | 'enableAjaxValidation' => false, 17 | ]); ?> 18 | errorSummary($model); ?> 19 | 20 |
21 |
22 | 23 | field($model, 'category'); ?> 24 | field($model, 'content')->textArea(['rows' => 6]); ?> 25 | 26 |
27 | 'btn btn-primary']) ?> 28 |
29 |
30 | 31 |
32 | 33 | 34 | 35 |
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 |
17 | 18 | 'subscription-form', 20 | 'enableAjaxValidation' => false, 21 | ]); ?> 22 | errorSummary($model); ?> 23 | 24 |
25 |
26 | 27 | field($model, 'label'); ?> 28 | field($model, 'categories'); ?> 29 | field($model, 'exceptions'); ?> 30 | 31 |
32 | 'btn btn-primary']) ?> 33 |
34 |
35 | 36 |
37 | 38 | 39 | 40 |
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 |

label); ?> $key])?>

22 | 23 |

24 | $key]) ?> / 25 | $key]) ?> 26 |

27 | 28 | 29 |

30 | : 31 |

32 | 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 |
28 | 'delete']); ?> 29 |
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 | 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=''),'
'+img+'
'+n.ntitle+"

"+n.nbody+'

'},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 = << 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 | --------------------------------------------------------------------------------