web-dev-qa-db-ja.com

PHPとmysqlを使用した通知システム

私たちの学校に通知システムを実装したかったのですが、これは一般公開されていないphp/mysql webappなので、あまりトラフィックを受け取りません。 「毎日500〜1000人の訪問者」。

1。私の最初のアプローチはMYSQLトリガーを使用することでした:

Mysql AFTER INSERT triggerを使用して、notificationsという名前のテーブルにレコードを追加しました。何かのようなもの。

'CREATE TRIGGER `notify_new_homwork` AFTER INSERT ON `homeworks`
 FOR EACH ROW INSERT INTO `notifications` 
    ( `from_id`, `note`, `class_id`) 
 VALUES 
    (new.user_id, 
        concat('A New homework Titled: "',left(new.title,'50'),
        '".. was added' )
    ,new.subject_id , 11);'

この種の黒魔術は非常にうまく機能しましたが、この通知が「ユーザーに新しい通知の数を表示する」ために新しいものかどうかを追跡できませんでした。そのため、notificationsという名前のページを追加しました。

通知は次のようなもので取得されます

SELECT n.* from notifications n 
JOIN user_class on user_class.class_id = n.class_id where user_class.user_id = X;

注:テーブルuser_classは、ユーザーをクラス「user_id、class_id、subject_id」にリンクします。ユーザーが教師でない限り、subjectはnullです。

今、私の次の課題は。

  1. ユーザーごとに新しい通知と古い通知を追跡する方法は?
  2. ユーザーに似た通知を1行に集約するにはどうすればよいですか?

たとえば、2人のユーザーが何かにコメントした場合、新しい行を挿入せず、「userxと1人がhw​​にコメントした」などのように古い行を更新するだけです。

どうもありがとう

編集

以下の回答に従って、行に読み取り/未読フラグを設定するには、クラス全体の行だけでなく、各学生の行が必要になります。つまり、トリガーを次のように編集することを意味します。

insert into notifications (from_id,note,student_id,isread)
select new.user_id,new.note,user_id,'0' from user_class where user_class.class_id = new.class_id group by user_class.user_id
39
Zalaboza

まあ、この質問は9ヶ月ですので、OPがまだ答えを必要としているかどうかはわかりませんが、多くの意見とおいしい賞金のために、マスタードも追加したいと思います(ドイツ語)。

この投稿では、通知システムの構築を開始する方法について簡単に説明した例を作成しようとします。

編集:まあまあ、これは予想以上に長い道のりでした。本当に疲れました。ごめんなさい。

WTLDR;

質問1:すべての通知にフラグがあります。

質問2:引き続き、すべての通知をデータベース内の単一のレコードとして保存し、要求時にグループ化します。


構造

通知は次のようになります。

+---------------------------------------------+
| ▣ James has uploaded new Homework: Math 1+1 |
+---------------------------------------------+
| ▣ Jane and John liked your comment: Im s... | 
+---------------------------------------------+
| ▢ The School is closed on independence day. |
+---------------------------------------------+

カーテンの後ろでは、これは次のようになります。

+--------+-----------+--------+-----------------+-------------------------------------------+
| unread | recipient | sender | type            | reference                                 |
+--------+-----------+--------+-----------------+-------------------------------------------+
| true   | me        | James  | homework.create | Math 1 + 1                                |
+--------+-----------+--------+-----------------+-------------------------------------------+
| true   | me        | Jane   | comment.like    | Im sick of school                         |
+--------+-----------+--------+-----------------+-------------------------------------------+
| true   | me        | John   | comment.like    | Im sick of school                         |
+--------+-----------+--------+-----------------+-------------------------------------------+
| false  | me        | system | message         | The School is closed on independence day. |
+--------+-----------+--------+-----------------+-------------------------------------------+

注:データベース内で通知をグループ化することはお勧めしません。実行時にこれを行うと、より柔軟になります。

  • 未読
    すべての通知には、受信者が既に通知を開いているかどうかを示すフラグが必要です。
  • 受信者
    通知の受信者を定義します。
  • 送信者
    通知をトリガーしたユーザーを定義します。
  • タイプ
    データベース内のプレーンテキストのすべてのメッセージでタイプを作成する代わりに。このようにして、バックエンド内でさまざまな通知タイプ用の特別なハンドラーを作成できます。データベース内に保存されるデータ量を削減し、柔軟性を高め、通知の簡単な翻訳、過去のメッセージの変更などを可能にします。
  • 参照
    ほとんどの通知には、データベースまたはアプリケーションのレコードへの参照が含まれます。

私が取り組んでいるすべてのシステムには、シンプルな1対1通知の参照関係がありました。1対nを覚えておいてください1:1で例を続けます。これは、通知タイプによって定義されるため、参照されるオブジェクトのタイプを定義するフィールドが不要であることも意味します。

SQLテーブル

SQLの実際のテーブル構造を定義するとき、データベース設計の観点からいくつかの決定を下します。私はこのように見える最も簡単な解決策に行きます:

+--------------+--------+---------------------------------------------------------+
| column       | type   | description                                             |
+--------------+--------+---------------------------------------------------------+
| id           | int    | Primary key                                             |
+--------------+--------+---------------------------------------------------------+
| recipient_id | int    | The receivers user id.                                  |
+--------------+--------+---------------------------------------------------------+
| sender_id    | int    | The sender's user id.                                   |
+--------------+--------+---------------------------------------------------------+
| unread       | bool   | Flag if the recipient has already read the notification |
+--------------+--------+---------------------------------------------------------+
| type         | string | The notification type.                                  |
+--------------+--------+---------------------------------------------------------+
| parameters   | array  | Additional data to render different notification types. |
+--------------+--------+---------------------------------------------------------+
| reference_id | int    | The primary key of the referencing object.              |
+--------------+--------+---------------------------------------------------------+
| created_at   | int    | Timestamp of the notification creation date.            |
+--------------+--------+---------------------------------------------------------+

または、怠け者の場合、この例ではSQL create tableコマンド

CREATE TABLE `notifications` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `recipient_id` int(11) NOT NULL,
  `sender_id` int(11) NOT NULL,
  `unread` tinyint(1) NOT NULL DEFAULT '1',
  `type` varchar(255) NOT NULL DEFAULT '',
  `parameters` text NOT NULL,
  `reference_id` int(11) NOT NULL,
  `created_at` int(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

PHPサービス

この実装は、アプリケーションのニーズに完全に依存します。注:これは、PHPで通知システムを構築する方法に関する黄金の標準ではありません。

通知モデル

これは、通知自体の基本モデルの例であり、必要なプロパティと抽象メソッドmessageForNotificationおよびmessageForNotificationsだけを空想したものではありません。

abstract class Notification
{
    protected $recipient;
    protected $sender;
    protected $unread;
    protected $type;
    protected $parameters;
    protected $referenceId;
    protected $createdAt;

    /**
     * Message generators that have to be defined in subclasses
     */
    public function messageForNotification(Notification $notification) : string;
    public function messageForNotifications(array $notifications) : string;

    /**
     * Generate message of the current notification.
     */ 
    public function message() : string
    {
        return $this->messageForNotification($this);
    }
}

constructorgetterssettersおよびその種類を追加する必要がありますあなた自身のスタイルで自分のものを、私はすぐに使用できる通知システムを提供するつもりはありません。

通知タイプ

これで、タイプごとに新しいNotificationサブクラスを作成できます。次の例は、コメントのlikeアクションを処理します。

  • レイはあなたのコメントを気に入りました。 (1通知)
  • ジョンとジェーンはあなたのコメントを気に入りました。 (2つの通知)
  • ジェーン、ジョニー、ジェームズ、ジェニーはあなたのコメントを気に入りました。 (4つの通知)
  • Jonny、James、その他12人があなたのコメントを気に入りました。 (14件の通知)

実装例:

namespace Notification\Comment;

class CommentLikedNotification extends \Notification
{
    /**
     * Generate a message for a single notification
     * 
     * @param Notification              $notification
     * @return string 
     */
    public function messageForNotification(Notification $notification) : string 
    {
        return $this->sender->getName() . 'has liked your comment: ' . substr($this->reference->text, 0, 10) . '...'; 
    }

    /**
     * Generate a message for a multiple notifications
     * 
     * @param array              $notifications
     * @return string 
     */
    public function messageForNotifications(array $notifications, int $realCount = 0) : string 
    {
        if ($realCount === 0) {
            $realCount = count($notifications);
        }

        // when there are two 
        if ($realCount === 2) {
            $names = $this->messageForTwoNotifications($notifications);
        }
        // less than five
        elseif ($realCount < 5) {
            $names = $this->messageForManyNotifications($notifications);
        }
        // to many
        else {
            $names = $this->messageForManyManyNotifications($notifications, $realCount);
        }

        return $names . ' liked your comment: ' . substr($this->reference->text, 0, 10) . '...'; 
    }

    /**
     * Generate a message for two notifications
     *
     *      John and Jane has liked your comment.
     * 
     * @param array              $notifications
     * @return string 
     */
    protected function messageForTwoNotifications(array $notifications) : string 
    {
        list($first, $second) = $notifications;
        return $first->getName() . ' and ' . $second->getName(); // John and Jane
    }

    /**
     * Generate a message many notifications
     *
     *      Jane, Johnny, James and Jenny has liked your comment.
     * 
     * @param array              $notifications
     * @return string 
     */
    protected function messageForManyNotifications(array $notifications) : string 
    {
        $last = array_pop($notifications);

        foreach($notifications as $notification) {
            $names .= $notification->getName() . ', ';
        }

        return substr($names, 0, -2) . ' and ' . $last->getName(); // Jane, Johnny, James and Jenny
    }

    /**
     * Generate a message for many many notifications
     *
     *      Jonny, James and 12 other have liked your comment.
     * 
     * @param array              $notifications
     * @return string 
     */
    protected function messageForManyManyNotifications(array $notifications, int $realCount) : string 
    {
        list($first, $second) = array_slice($notifications, 0, 2);

        return $first->getName() . ', ' . $second->getName() . ' and ' .  $realCount . ' others'; // Jonny, James and 12 other
    }
}

通知マネージャー

アプリケーション内で通知を処理するには、通知マネージャーのようなものを作成します。

class NotificationManager
{
    protected $notificationAdapter;

    public function add(Notification $notification);

    public function markRead(array $notifications);

    public function get(User $user, $limit = 20, $offset = 0) : array;
}

このmysqlの例の場合、notificationAdapterプロパティには、データバックエンドと直接通信するロジックが含まれている必要があります。

通知を作成する

間違った解決策がないため、mysqlトリガーの使用は間違っていません。 機能する、機能する..しかし、データベースにアプリケーションロジックを処理させないことを強くお勧めします。

そのため、通知マネージャー内で次のような操作を実行できます。

public function add(Notification $notification)
{
    // only save the notification if no possible duplicate is found.
    if (!$this->notificationAdapter->isDoublicate($notification))
    {
        $this->notificationAdapter->add([
            'recipient_id' => $notification->recipient->getId(),
            'sender_id' => $notification->sender->getId()
            'unread' => 1,
            'type' => $notification->type,
            'parameters' => $notification->parameters,
            'reference_id' => $notification->reference->getId(),
            'created_at' => time(),
        ]);
    }
}

addnotificationAdapterメソッドの背後には、未加工のmysql挿入コマンドがあります。このアダプターの抽象化を使用すると、mysqlからmongodbのようなドキュメントベースのデータベースに簡単に切り替えることができ、これは通知システムにとって意味があります。

isDoublicatenotificationAdapterメソッドは、同じrecipientsendertype、およびreferenceの通知が既に存在するかどうかを単純に確認する必要があります。


これが単なる例にすぎないことを十分に指摘することはできません。(また、この投稿が途方もなく長くなる次のステップを短くしなければなりません-.-)


したがって、教師が宿題をアップロードするときにアクションを実行する何らかのコントローラーがあると仮定します。

function uploadHomeworkAction(Request $request)
{
    // handle the homework and have it stored in the var $homework.

    // how you handle your services is up to you...
    $notificationManager = new NotificationManager;

    foreach($homework->teacher->students as $student)
    {
        $notification = new Notification\Homework\HomeworkUploadedNotification;
        $notification->sender = $homework->teacher;
        $notification->recipient = $student;
        $notification->reference = $homework;

        // send the notification
        $notificationManager->add($notification);
    }
}

すべての教師の生徒が新しい宿題をアップロードしたときに通知を作成します。

通知を読む

ここで難しい部分があります。 PHP側のグループ化の問題は、現在のユーザーの通知を正しくグループ化するためにallをロードする必要があることです。これは悪いことです。もし少数のユーザーしかいなければ、おそらく問題はないでしょうが、それは良いことではありません。

簡単な解決策は、要求された通知の数を単純に制限し、これらの通知のみをグループ化することです。これは、同様の通知があまり多くない場合(20ごとに3-4など)に正常に機能します。しかし、ユーザー/学生の投稿には約100のいいね!があり、最後の20件の通知のみを選択するとしましょう。ユーザーは、20人が自分の投稿を気に入っていることだけを確認し、それが彼の唯一の通知にもなります。

「正しい」解決策は、すでにデータベースにある通知をグループ化し、通知グループごとにいくつかのサンプルのみを選択することです。それよりも、通知メッセージに実際のカウントを挿入する必要があります。

おそらく以下のテキストを読んでいないので、スニペットを続けましょう。

select *, count(*) as count from notifications
where recipient_id = 1
group by `type`, `reference_id`
order by created_at desc, unread desc
limit 20

これで、特定のユーザーに通知する必要がある通知と、グループに含まれる通知の数がわかりました。

そして今、くだらない部分。グループごとにクエリを実行しないと、グループごとに限られた数の通知を選択するより良い方法を見つけることができませんでした。 ここでの提案はすべて大歓迎です。

だから私は次のようなことをします:

$notifcationGroups = [];

foreach($results as $notification)
{
    $notifcationGroup = ['count' => $notification['count']];

    // when the group only contains one item we don't 
    // have to select it's children
    if ($notification['count'] == 1)
    {
        $notifcationGroup['items'] = [$notification];
    }
    else
    {
        // example with query builder
        $notifcationGroup['items'] = $this->select('notifications')
            ->where('recipient_id', $recipient_id)
            ->andWehere('type', $notification['type'])
            ->andWhere('reference_id', $notification['reference_id'])
            ->limit(5);
    }

    $notifcationGroups[] = $notifcationGroup;
}

notificationAdapters getメソッドがこのグループ化を実装し、次のような配列を返すと仮定します。

[
    {
        count: 12,
        items: [Note1, Note2, Note3, Note4, Note5] 
    },
    {
        count: 1,
        items: [Note1] 
    },
    {
        count: 3,
        items: [Note1, Note2, Note3] 
    }
]

グループには常に少なくとも1つの通知があり、順序はUnreadおよびNew通知を優先するため、最初の通知のみを使用できますレンダリングのサンプルとして。

したがって、これらのグループ化された通知を使用するには、新しいオブジェクトが必要です。

class NotificationGroup
{
    protected $notifications;

    protected $realCount;

    public function __construct(array $notifications, int $count)
    {
        $this->notifications = $notifications;
        $this->realCount = $count;
    }

    public function message()
    {
        return $this->notifications[0]->messageForNotifications($this->notifications, $this->realCount);
    }

    // forward all other calls to the first notification
    public function __call($method, $arguments)
    {
        return call_user_func_array([$this->notifications[0], $method], $arguments);
    }
}

そして最後に、ほとんどのものを実際にまとめることができます。 NotificationManagerのget関数は次のようになります。

public function get(User $user, $limit = 20, $offset = 0) : array
{
    $groups = [];

    foreach($this->notificationAdapter->get($user->getId(), $limit, $offset) as $group)
    {
        $groups[] = new NotificationGroup($group['notifications'], $group['count']);
    }

    return $gorups;
}

そして、実際に可能なコントローラーアクションの内部:

public function viewNotificationsAction(Request $request)
{
    $notificationManager = new NotificationManager;

    foreach($notifications = $notificationManager->get($this->getUser()) as $group)
    {
        echo $group->unread . ' | ' . $group->message() . ' - ' . $group->createdAt() . "\n"; 
    }

    // mark them as read 
    $notificationManager->markRead($notifications);
}
125
Mario

回答:

  1. 通知に読み取り/未読変数を導入します。その後、SQLで... WHERE status = 'UNREAD'を実行することで、未読の通知のみをプルできます。

  2. あなたは本当にできない...あなたはその通知をプッシュしたくなるでしょう。できることは、GROUP BYを使用してそれらを集約することです。新しい宿題のようなユニークなものでグループ化したいと思うかもしれません。それは... GROUP BY homework.id

2
geggleto

ユーザーのIDとユーザーが既読としてマークしたい通知のIDを含むNotificationsReadテーブルを作成することで問題を解決できます。 (この方法により、各生徒と教師を分離したままにすることができます。)その後、そのテーブルを通知のテーブルに参加させることができます。

geggletoの答えは2番目の部分について正しかったので、SELECT *, COUNT(*) AS counter WHERE ... GROUP BY 'type'で通知を取得できます。その後、同じタイプがいくつあるかがわかり、「userxと1つのその他のhwのコメント」を表示できます.

また、表示したいテキスト全体を保存せず、代わりに必要な情報を保存することをお勧めします:from_id、class_id、type、nameなど-この方法では、必要に応じて後で簡単にメカニズムを変更できます。より少なく保管する必要があります。

1
Razor_alpha