Mysql
 sql >> Base de Dados >  >> RDS >> Mysql

Sistema de notificação usando php e mysql


Bem, esta pergunta tem 9 meses, então não tenho certeza se o OP ainda precisa de uma resposta, mas devido às muitas visualizações e à recompensa saborosa, gostaria de adicionar também minha mostarda (provérbio alemão ..).

Neste post vou tentar fazer um exemplo simples e explicado de como começar a construir um sistema de notificação.

Editar: Bem, ok isso acabou muito, muito, muito mais do que eu esperava que fosse. Eu cansei muito no final, me desculpe.

WTLDR;


Pergunta 1: ter um sinalizador em cada notificação.

Pergunta 2: Ainda armazene todas as notificações como um único registro dentro de seu banco de dados e agrupe-as quando forem solicitadas.

Estrutura


Presumo que as notificações sejam parecidas com:
+---------------------------------------------+
| ▣ James has uploaded new Homework: Math 1+1 |
+---------------------------------------------+
| ▣ Jane and John liked your comment: Im s... | 
+---------------------------------------------+
| ▢ The School is closed on independence day. |
+---------------------------------------------+

Atrás das cortinas, isso pode ser algo assim:
+--------+-----------+--------+-----------------+-------------------------------------------+
| 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. |
+--------+-----------+--------+-----------------+-------------------------------------------+

Observação: Não recomendo agrupar as notificações dentro do banco de dados, faça isso em tempo de execução isso deixa as coisas bem mais flexíveis.
  • Não lido
    Toda notificação deve ter um sinalizador para indicar se o destinatário já abriu a notificação.
  • Destinatário
    Define quem recebe a notificação.
  • Remetente
    Define quem acionou a notificação.
  • Tipo
    Em vez de ter todas as mensagens em texto simples dentro de seu banco de dados, crie tipos. Dessa forma, você pode criar manipuladores especiais para diferentes tipos de notificação dentro de seu back-end. Reduzirá a quantidade de dados armazenados em seu banco de dados e lhe dará ainda mais flexibilidade, permitindo tradução fácil de notificação, alterações de mensagens anteriores etc.
  • Referência
    A maioria das notificações terá uma referência a um registro em seu banco de dados ou em seu aplicativo.

Todos os sistemas em que trabalhei tinham um simples 1 para 1 relação de referência em uma notificação, você pode ter um 1 para n tenha em mente que continuarei meu exemplo com 1:1. Isso também significa que não preciso de um campo definindo que tipo de objeto é referenciado porque isso é definido pelo tipo de notificação.

Tabela SQL


Agora, ao definir uma estrutura de tabela real para SQL, chegamos a algumas decisões em termos de design do banco de dados. Eu irei com a solução mais simples que será algo assim:
+--------------+--------+---------------------------------------------------------+
| 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.            |
+--------------+--------+---------------------------------------------------------+

Ou para os preguiçosos o comando SQL create table para este exemplo:
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;

Serviço PHP


Essa implementação depende completamente das necessidades do seu aplicativo, Observação: Este é um exemplo que não é o padrão de ouro de como construir um sistema de notificação em PHP.

Modelo de notificação


Este é um modelo base de exemplo da própria notificação, nada extravagante apenas as propriedades necessárias e os métodos abstratos messageForNotification e messageForNotifications esperávamos ser implementados nos diferentes tipos de notificação.
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);
    }
}

Você terá que adicionar um construtor , getters , configuradores e esse tipo de coisa por você mesmo em seu próprio estilo, eu não vou fornecer um sistema de notificação pronto para usar.

Tipos de notificação


Agora você pode criar uma nova Notification subclasse para cada tipo. Este exemplo a seguir lidaria com a ação like de um comentário:
  • Ray gostou do seu comentário. (1 notificação)
  • John e Jane gostaram do seu comentário. (2 notificações)
  • Jane, Johnny, James e Jenny gostaram do seu comentário. (4 notificações)
  • Jonny, James e outras 12 pessoas curtiram seu comentário. (14 notificações)

Exemplo de implementação:
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
    }
}

Gerenciador de notificações


Para trabalhar com suas notificações dentro do seu aplicativo, crie algo como um gerenciador de notificações:
class NotificationManager
{
    protected $notificationAdapter;

    public function add(Notification $notification);

    public function markRead(array $notifications);

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

O notificationAdapter A propriedade deve conter a lógica em comunicação direta com seu backend de dados no caso deste exemplo mysql.

Criando notificações


Usando mysql gatilhos não está errado, porque não há solução errada. O que funciona, funciona.. Mas eu recomendo fortemente não deixar o banco de dados manipular a lógica do aplicativo.

Então, dentro do gerenciador de notificações, você pode querer fazer algo assim:
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(),
        ]);
    }
}

Atrás do add método do notificationAdapter pode ser um comando de inserção mysql bruto. O uso desta abstração do adaptador permite que você alterne facilmente do mysql para um banco de dados baseado em documento como mongodb o que faria sentido para um sistema de Notificação.

O isDoublicate método no notificationAdapter deve simplesmente verificar se já existe uma notificação com o mesmo recipient , sender , type e reference .

Não posso apontar o suficiente que este é apenas um exemplo. (Também eu realmente tenho que encurtar os próximos passos, este post está ficando ridiculamente longo -.-)

Então, supondo que você tenha algum tipo de controlador com uma ação quando um professor faz o upload do dever de casa:
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);
    }
}

Criará uma notificação para cada aluno do professor quando ele enviar uma nova lição de casa.

Lendo as notificações


Agora vem a parte difícil. O problema com o agrupamento no lado do PHP é que você terá que carregar todos notificações do usuário atual para agrupá-los corretamente. Isso seria ruim, bem, se você tiver apenas alguns usuários, provavelmente ainda não seria um problema, mas isso não o torna bom.

A solução fácil é simplesmente limitar o número de notificações solicitadas e apenas agrupá-las. Isso funcionará bem quando não houver muitas notificações semelhantes (como 3-4 por 20). Mas digamos que a postagem de um usuário/aluno receba cerca de cem curtidas e você selecione apenas as últimas 20 notificações. O usuário verá apenas que 20 pessoas gostaram de sua postagem e essa seria sua única notificação.

Uma solução "correta" seria agrupar as notificações já existentes no banco de dados e selecionar apenas algumas amostras por grupo de notificação. Do que você teria que injetar a contagem real em suas mensagens de notificação.

Você provavelmente não leu o texto abaixo, então deixe-me continuar com um trecho:
select *, count(*) as count from notifications
where recipient_id = 1
group by `type`, `reference_id`
order by created_at desc, unread desc
limit 20

Agora você sabe quais notificações devem estar disponíveis para um determinado usuário e quantas notificações o grupo contém.

E agora a parte chata. Ainda não consegui descobrir uma maneira melhor de selecionar um número limitado de notificações para cada grupo sem fazer uma consulta para cada grupo. Todas as sugestões aqui são muito bem-vindas.

Então eu faço algo como:
$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;
}

Agora continuarei assumindo que o notificationAdapter s get O método implementa esse agrupamento e retorna um array como este:
[
    {
        count: 12,
        items: [Note1, Note2, Note3, Note4, Note5] 
    },
    {
        count: 1,
        items: [Note1] 
    },
    {
        count: 3,
        items: [Note1, Note2, Note3] 
    }
]

Porque sempre temos pelo menos uma notificação em nosso grupo e nosso pedido prefere Não lido e Novo notificações, podemos usar apenas a primeira notificação como amostra para renderização.

Então, para poder trabalhar com essas notificações agrupadas, precisamos de um novo objeto:
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);
    }
}

E, finalmente, podemos realmente juntar a maioria das coisas. É assim que a função get no NotificationManager pode parecer:
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;
}

E finalmente dentro de uma possível ação do controlador:
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);
}