Cóż, to pytanie ma 9 miesięcy, więc nie jestem pewien, czy OP nadal potrzebuje odpowiedzi, ale ze względu na wiele wyświetleń i smaczną nagrodę chciałbym dodać również moją musztardę (niemieckie powiedzenie...).
W tym poście postaram się zrobić prosty wyjaśniony przykład jak zacząć budować system powiadomień.
Edytuj: Cóż, okazało się, że tak, o wiele dłużej, niż się spodziewałem. W końcu naprawdę się zmęczyłem, przepraszam.
WTLDR;
Pytanie 1: mieć flagę na każdym powiadomieniu.
Pytanie 2: Nadal przechowuj każde powiadomienie jako pojedynczy rekord w swojej bazie danych i grupuj je, gdy są wymagane.
Struktura
Zakładam, że powiadomienia będą wyglądać mniej więcej tak:
+---------------------------------------------+
| ▣ James has uploaded new Homework: Math 1+1 |
+---------------------------------------------+
| ▣ Jane and John liked your comment: Im s... |
+---------------------------------------------+
| ▢ The School is closed on independence day. |
+---------------------------------------------+
Za zasłonami może to wyglądać mniej więcej tak:
+--------+-----------+--------+-----------------+-------------------------------------------+
| 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. |
+--------+-----------+--------+-----------------+-------------------------------------------+
Uwaga: Nie polecam grupowania powiadomień w bazie danych, rób to w czasie wykonywania, dzięki czemu wszystko jest o wiele bardziej elastyczne.
- Nieprzeczytane
Każde powiadomienie powinno mieć flagę wskazującą, czy odbiorca już otworzył powiadomienie. - Odbiorca
Określa, kto otrzyma powiadomienie. - Nadawca
Określa, kto wywołał powiadomienie. - Typ
Zamiast umieszczania każdej wiadomości w postaci zwykłego tekstu w bazie danych, twórz typy. W ten sposób możesz tworzyć specjalne programy obsługi dla różnych typów powiadomień w swoim zapleczu. Zmniejszy ilość danych przechowywanych w Twojej bazie danych i zapewni jeszcze większą elastyczność, umożliwi łatwe tłumaczenie powiadomień, zmiany przeszłych wiadomości itp. - Odniesienie
Większość powiadomień będzie miała odniesienie do wpisu w Twojej bazie danych lub aplikacji.
Każdy system, nad którym pracowałem, miał prosty 1 do 1 związek odniesienia w powiadomieniu, możesz mieć od 1 do n pamiętaj, że będę kontynuował swój przykład z 1:1. Oznacza to również, że nie potrzebuję pola określającego, do jakiego typu obiektu się odwołujemy, ponieważ jest to określone przez typ powiadomienia.
Tabela SQL
Teraz, kiedy definiujemy rzeczywistą strukturę tabeli dla SQL, dochodzimy do kilku decyzji w zakresie projektu bazy danych. Wybiorę najprostsze rozwiązanie, które będzie wyglądać mniej więcej tak:
+--------------+--------+---------------------------------------------------------+
| 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. |
+--------------+--------+---------------------------------------------------------+
Lub dla leniwych polecenie tworzenia tabeli SQL dla tego przykładu:
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;
Usługa PHP
Ta implementacja zależy całkowicie od potrzeb Twojej aplikacji, Uwaga: To jest przykład, a nie złoty standard budowania systemu powiadomień w PHP.
Model powiadomienia
To jest przykładowy model bazowy samego powiadomienia, nic nadzwyczajnego tylko potrzebne właściwości i abstrakcyjne metody messageForNotification
i messageForNotifications
spodziewaliśmy się, że zostaną wdrożone w różnych typach powiadomień.
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);
}
}
Będziesz musiał dodać konstruktora , gettery , setery i tego rodzaju rzeczy samodzielnie w swoim własnym stylu, nie zamierzam dostarczać gotowego do użycia systemu powiadomień.
Typy powiadomień
Teraz możesz utworzyć nowe Notification
podklasa dla każdego typu. Poniższy przykład poradzi sobie z działaniem podobnym komentarza:
- Ray polubił Twój komentarz. (1 powiadomienie)
- John i Jane polubili twój komentarz. (2 powiadomienia)
- Jane, Johnny, James i Jenny polubili twój komentarz. (4 powiadomienia)
- Jonny, James i 12 innych osób polubiło Twój komentarz. (14 powiadomień)
Przykładowa implementacja:
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
}
}
Menedżer powiadomień
Aby pracować z powiadomieniami w aplikacji, utwórz coś w rodzaju menedżera powiadomień:
class NotificationManager
{
protected $notificationAdapter;
public function add(Notification $notification);
public function markRead(array $notifications);
public function get(User $user, $limit = 20, $offset = 0) : array;
}
notificationAdapter
właściwość powinna zawierać logikę w bezpośredniej komunikacji z backendem danych w przypadku tego przykładu mysql.
Tworzenie powiadomień
Korzystanie z mysql
wyzwalacze nie są złe, ponieważ nie ma złego rozwiązania. Co działa, działa... Ale zdecydowanie odradzam, aby baza danych nie zajmowała się logiką aplikacji.
Więc w menedżerze powiadomień możesz chcieć zrobić coś takiego:
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(),
]);
}
}
Za add
metoda notificationAdapter
może być surowym poleceniem wstawiania mysql. Korzystanie z tej abstrakcji adaptera umożliwia łatwe przełączanie się z mysql na bazę danych opartą na dokumentach, taką jak mongodb co miałoby sens w przypadku systemu powiadomień.
isDoublicate
metoda na notificationAdapter
powinien po prostu sprawdzić, czy istnieje już powiadomienie z tym samym recipient
, sender
, type
i reference
.
Nie potrafię wystarczająco wskazać, że to tylko przykład. (Naprawdę muszę skrócić kolejne kroki, które ten post robi się absurdalnie długi -.-)
Zakładając więc, że masz jakiś kontroler z akcją, gdy nauczyciel przesyła pracę domową:
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);
}
}
Stworzy powiadomienie dla każdego ucznia nauczyciela, gdy prześle nową pracę domową.
Czytanie powiadomień
Teraz nadchodzi najtrudniejsza część. Problem z grupowaniem po stronie PHP polega na tym, że będziesz musiał załadować wszystkie powiadomienia bieżącego użytkownika, aby je poprawnie pogrupować. To byłoby złe, cóż, jeśli masz tylko kilku użytkowników, prawdopodobnie nadal nie stanowiłoby to problemu, ale to nie czyni tego dobrze.
Prostym rozwiązaniem jest po prostu ograniczenie liczby żądanych powiadomień i tylko ich pogrupowanie. Będzie to działać dobrze, gdy nie ma wielu podobnych powiadomień (np. 3-4 na 20). Ale powiedzmy, że post użytkownika / studenta dostaje około stu polubień, a ty wybierasz tylko 20 ostatnich powiadomień. Użytkownik zobaczy wtedy tylko, że 20 osób polubiło jego post, co byłoby jego jedynym powiadomieniem.
„Właściwym” rozwiązaniem byłoby pogrupowanie powiadomień znajdujących się już w bazie danych i wybranie tylko kilku próbek na grupę powiadomień. Wtedy wystarczyłoby wstrzyknąć rzeczywistą liczbę do swoich powiadomień.
Prawdopodobnie nie przeczytałeś poniższego tekstu, więc pozwól mi kontynuować z fragmentem:
select *, count(*) as count from notifications
where recipient_id = 1
group by `type`, `reference_id`
order by created_at desc, unread desc
limit 20
Teraz wiesz, jakie powiadomienia powinny być dostępne dla danego użytkownika i ile powiadomień zawiera grupa.
A teraz gówniana część. Nadal nie mogłem znaleźć lepszego sposobu na wybranie ograniczonej liczby powiadomień dla każdej grupy bez wykonywania zapytania dla każdej grupy. Wszystkie sugestie tutaj są mile widziane.
Więc robię coś takiego:
$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;
}
Teraz będę kontynuował założenie, że notificationAdapter
s get
metoda implementuje to grupowanie i zwraca tablicę w ten sposób:
[
{
count: 12,
items: [Note1, Note2, Note3, Note4, Note5]
},
{
count: 1,
items: [Note1]
},
{
count: 3,
items: [Note1, Note2, Note3]
}
]
Ponieważ zawsze mamy co najmniej jedno powiadomienie w naszej grupie, a nasze zamówienia preferują Nieprzeczytane i Nowe powiadomienia możemy po prostu użyć pierwszego powiadomienia jako próbki do renderowania.
Aby móc pracować z tymi grupowymi powiadomieniami, potrzebujemy nowego obiektu:
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);
}
}
I wreszcie możemy poskładać większość rzeczy do kupy. Tak działa funkcja get w NotificationManager
może wyglądać tak:
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;
}
I wreszcie wewnątrz możliwej akcji kontrolera:
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);
}