Mmmm... I think I couldn't write this article in the same manner because the feeling of writing it is different from now. But I won't give up, at the end it is all my fault anyway. I must fix this bug. I wonder what causes it to be delete that way tho.
So this is the first article I write in english but not the very first. I meant this is the first technical thing I wanted to share but written in english. I couldn't help because I think in this language when I codes or when I randomly internet ( most of time ).
It all started with this blog. Formerly codenamed cranberry, which was powered by a typical software bundle LAMP ( Linux, Apache, MySQL, PHP ) and now-dead. Reborn with the codename "Rosemary" and powered by LAMN ( Linux, Apache, Node.js, MongoDB ) which Apache only served as a proxy and a lame file server.
There were much things to share even in the cranberry era which is written in 2 years ago. Some are good, some are bad, mostly just bad. But still, even it is bad I could still share, right? It may serve as an experience for people to (hardly) learn something from it. I wonder!
This time I am going to share the notification model in cranberry. I still can remember I was proud of it when it get finished. It was such a sophisticated and elegant thing I'd ever built.
Let's dig in:mysql> SELECT * FROM `notifications`;
+----+---------+-----------+------------+------------+
| id | user_id | notify_id | carrier_id | timestamp |
+----+---------+-----------+------------+------------+
| 1 | 2 | 1037 | 113 | 1390372930 |
+----+---------+-----------+------------+------------+
mysql> SELECT * FROM `notification_followers` LIMIT 5;
+-----------+---------+---------+---------------+
| notify_id | user_id | type_id | constraint_id |
+-----------+---------+---------+---------------+
| 1 | 1 | 0 | 0 |
| 2 | 1 | 1 | 0 |
| 3 | 1 | 2 | 0 |
.
.
.
| 1037 | 2 | 1 | 1790 |
.
.
.
+-----------+---------+---------+---------------+
From the look of it. It seems each row in `notifications` represents one notification. And notification_followers, by guessing the name, it is the notification that a user had subscribed.
I have no idea what `carrier_id` nor `constraint_id` is.
Let's look at the class structure:AstroArch: ~/cranberry/includes/astroblog/notifications
$ ls -la
total 44
drwxr-xr-x 2 penguin penguin 4096 Aug 20 21:38 .
drwxr-xr-x 6 penguin penguin 4096 Jun 4 11:32 ..
-rw-r--r-- 1 penguin penguin 1261 Jan 15 2014 ArticlePublishNotice.php
-rw-r--r-- 1 penguin penguin 385 Jan 8 2014 ArticleUpdateNotice.php
-rw-r--r-- 1 penguin penguin 1380 Jan 15 2014 CommentReplyNotice.php
-rw-r--r-- 1 penguin penguin 5834 Jan 15 2014 NotificationCenter.php
-rw-r--r-- 1 penguin penguin 541 Jan 15 2014 NotificationModel.php
-rw-r--r-- 1 penguin penguin 5174 Jan 15 2014 Notification.php
-rw-r--r-- 1 penguin penguin 1288 Jan 15 2014 UserCommentNotice.php
So there is 4 notification types I guess they are what the `type_id` referred to. And I guess they probably extends `NotificationModel`?
... close but instead extends they implements the NotificationModel and extends Notification:namespace astroblog\notifications;
class UserCommentNotice extends Notification implements NotificationModel {
const ID = 1;
const NAME = "User Comment";
...
Now that one thing is cleared: The row in `notifications` says that there is 1 notification for user with id 2. And the notification type is a `UserCommentNotice`.
Still what exactly is `carrier_id` and `constraint_id` ? Well I guess it's time to look inside the actual codes.
I wonder how it's invoked:
<?php
...
use astroblog\notifications\NotificationCenter;
use astroblog\notifications\UserCommentNotice;
use astroblog\notifications\CommentReplyNotice;
...
if ($count == 1) {
$success = true;
$cm_id = (int)$dbh->lastInsertId();
$ntc = new NotificationCenter($dbh);
if($parent_id) {
// Prepare registration note for CommentReply:listener_id = parent_id
$nt = new CommentReplyNotice((int)$parent_id, $cm_id);
// Follow first, will be able to turn off later
$ntc->userFollow($session->user_id, $nt);
// Push notifications to all followers
$ntc->pushNotification($session->user_id, $nt);
}
// Push notification to all followers
$nt = new UserCommentNotice((int)$data['article_id'], $cm_id);
$ntc->pushNotification($session->user_id, $nt);
The NotificationCenter is construct with the database handler:$ntc = new NotificationCenter($dbh);
To push a notification, I do:
$ntc->pushNotification( <USER>, <NotificationModel> );
This is simple and clear, it's good even to now-me. *Applaud myself*
But how's the notification getting retrieved?
<?php // ajax-notification.php
...
use \astroblog\notifications\NotificationCenter;
$ntc = new NotificationCenter($dbh);
try {
switch($_POST['action']) {
case "get":
$data = $ntc->getNotifications($session->user_id);
break;
case "read":
$ntc->notificationRead($_POST['id']);
break;
case "enable":
$ntc->enableNotification($session->user_id, $_POST['tid'], $_POST['cid']);
break;
case "disable":
$ntc->disableNotification($session->user_id, $_POST['tid'], $_POST['cid']);
break;
case "getSettings":
$data = $ntc->getTypeStats($session->user_id);
break;
default:
$status = false;
$message = "No such action";
}
} catch ( Exception $ex ) {
$status = false;
$message = $ex->getMessage();
}
echo json_encode([
'status' => isset($status) ? $status : true,
'message' => isset($message) ? $message : 'OK',
'data' => isset($data) ? $data : NULL
]);
( There was an entire file dedicated to handle the notifications. )
I am guessing these actions:
- get - get the list of notification for current user
- read - since the get does what it does. I think this is where the user read a notification
- enable - enable a notification ( subscribe? )
- disable - disable a notification ( unsubscribe? )
- getSettings - list all notification settings by type ( list all subscribed notifications? )
I still didn't get what the two id were. This is a minus.
I have to dig deeper:
<?php // NotificationCenter.php
...
public function getNotifications($user_id) {
$nmessages = [];
foreach( $this->dbh->query(implode(" UNION ", array_map($this->staticNotice('getMessage'), $this->allNotifications())), [":user_id" => $user_id]) as $row ) {
$nmessages[] = $this->printMessage($row);
}
return $nmessages;
}
...
// Closure, anonymous functions
public function staticNotice($fn) {
// Call notice func
return function ($c) use ($fn) { return call_user_func([__NAMESPACE__ . '\\' . $c, $fn]); };
}
...
protected function allNotifications() {
static $nfs;
if(empty($nfs)) {
$nfs = [
'UserCommentNotice'
, 'CommentReplyNotice'
, 'ArticlePublishNotice'
];
}
return $nfs;
}
The getNotifications gets all `Notifaction` from Notifications defined in allNotifications which are UserCommentNotice, CommentReplyNotice and ArticlePublishNotice. And called for each of the `getMessage` method the `UNION` them together.
I guest the getMessage function returns a query?
Yes! it is:
<?php // UserCommentNotice.php
...
class UserCommentNotice extends Notification implements NotificationModel {
...
const ID = 1;
...
const KEY_JOINT = " LEFT JOIN articles a ON nf.constraint_id=a.article_id";
...
public static function getMessage() {
$j = self::KEY_JOINT . " LEFT JOIN comments c ON n.carrier_id=c.comment_id";
return self::selectMessages(self::ID, $j, 'a.article_id', 'c.comment_id', 'a.title', 'NULL', 'c.commenter', 'NULL');
}
}
<?php // Notification.php
// Notification class is a query store
// it is like an instruction
// it stores all required queries and run by NotificationCenter
...
class Notification {
...
// $type: type of the Notification, provided bu Notification::ID
// $JOINT: the joinning query, relationship between listener and dispatcher
// $l_anchor: listener anchor, the anchor link for the listener
// $d_anchor: dispatcher anchora, the anchor link for the dispatcher
// $l_caption: listener caption, the caption to show on the message
// $l_detail: listener detail, the detail to show on the message
// $d_caption: dispatcher caption
// $d_detail: dispatcher detail
protected static function selectMessages($type, $JOINT, $l_anchor, $d_anchor, $l_caption, $l_detail = 'NULL', $d_caption = 'NULL', $d_detail = 'NULL') {
return <<<___SQL___
SELECT n.id, n.notify_id, nf.type_id type, $l_anchor LAnchor, $d_anchor DAnchor, $l_caption LCaption, $l_detail LDetail, $d_caption DCaption, $d_detail DDetail, n.timestamp
FROM notifications n
LEFT JOIN notification_followers nf USING(notify_id)
$JOINT
WHERE n.user_id=:user_id AND nf.type_id=$type
___SQL___;
}
I am not going to comment about the handling of the SQL queries. Handling SQL queries in Php is always a pain for web devs for years. That's why we use ORM and was one of the reason I dropped php and MySQL. Even the ORM library itself is just consist of a lot of not-very-readable query joining magic when you look inside. However in cranberry, the primary goal was try, failed and learn. Things where written entirely from scratch with few exceptions.
And this is the query output when extracted only with UserCommentNotice:mysql> SELECT n.id, n.notify_id, nf.type_id type, a.article_id LAnchor, c.comment_id DAnchor, a.title LCaption, Null LDetail, c.commenter DCaption, Null DDetail, n.timestamp
-> FROM notifications n
-> LEFT JOIN notification_followers nf USING(notify_id)
-> LEFT JOIN articles a ON nf.constraint_id=a.article_id
-> LEFT JOIN comments c ON n.carrier_id=c.comment_id
-> WHERE n.user_id=2 AND nf.type_id=1;
+----+-----------+------+---------+---------+-----------------------------+---------+---------------+---------+------------+
| id | notify_id | type | LAnchor | DAnchor | LCaption | LDetail | DCaption | DDetail | timestamp |
+----+-----------+------+---------+---------+-----------------------------+---------+---------------+---------+------------+
| 1 | 1037 | 1 | 1790 | 113 | 呀! 甚麼都寫不出來! | NULL | 斟酌 鵬兄 | NULL | 1390372930 |
+----+-----------+------+---------+---------+-----------------------------+---------+---------------+---------+------------+
1 row in set (0.03 sec)
This is a fine-looking query. Upvote for myself!
After that it called print message, which translate the above data to a readable information:<?php // NotificationCenter.php
...
protected function printMessage($n) {
$nLine = [
'id' => $n['id']
, 'type' => $n['type']
, 'date' => $n['timestamp']
];
switch($n['type']) {
case UserCommentNotice::ID:
$nLine['message'] = sprintf(UserCommentNotice::MESSAGE_MODEL, $n['LCaption'], $n['DCaption']);
$nLine['link'] = sprintf(UserCommentNotice::ANCHOR, $n['LAnchor'], $n['DAnchor']);
break;
case CommentReplyNotice::ID:
$nLine['message'] = sprintf(CommentReplyNotice::MESSAGE_MODEL, $n['LCaption'], $n['DCaption']);
$nLine['link'] = sprintf(CommentReplyNotice::ANCHOR, $n['LAnchor'], $n['DAnchor']);
break;
case ArticlePublishNotice::ID:
$nLine['message'] = sprintf(ArticlePublishNotice::MESSAGE_MODEL, $n['LCaption'], $n['DCaption']);
$nLine['link'] = sprintf(ArticlePublishNotice::ANCHOR, $n['LAnchor']);
break;
default:
throw new \InvalidArgumentException($n);
}
return $nLine;
}
<?php // UserCommentNotice.php
...
class UserCommentNotice extends Notification implements NotificationModel {
...
const MESSAGE_MODEL = '%2$s has commented on article(%1$s)';
const ANCHOR = 'article/view/%1$s/#comment_%2$s';
...
The printMessage looks repetitive and can be perhaps further optimize. The output will be something like this:[message] 斟酌 鵬兄 has commented on article(呀! 甚麼都寫不出來!)
[link] article/view/1790/#comment_113
Summary:
This constraint_id and carrier_id is dynamic to the type of the notifications.
i.e. For UserCommentNotice, I must get both article_id & comment_id to compile the notification message.
For ArticleUpdateNotice, I'll just need to article_id to do so. ( So the carrier_id is not needed )
This is brilliant! I still think this is the most elegant class I've ever made. Proud of myself!
Continue to "The Notification Model" Part II >> 斟酌 鵬兄
Thu Aug 20 2015 16:55:33 GMT+0000 (Coordinated Universal Time)
Last modified: Fri Mar 11 2016 14:32:11 GMT+0000 (Coordinated Universal Time)