sql >> Base de Datos >  >> RDS >> Mysql

Sistema de notificaciones usando php y mysql

Bueno, esta pregunta tiene 9 meses, así que no estoy seguro de si OP todavía necesita una respuesta, pero debido a las muchas opiniones y la sabrosa generosidad, me gustaría agregar también mi mostaza (dicho alemán ...).

En esta publicación, intentaré hacer un ejemplo simple explicado sobre cómo comenzar a construir un sistema de notificación.

Editar: Bueno, está bien, esto resultó mucho, mucho, mucho más largo de lo que esperaba. Me cansé mucho al final, lo siento.

WTLDR;

Pregunta 1: tener una bandera en cada notificación.

Pregunta 2: Todavía almacene cada notificación como un registro único dentro de su base de datos y agrúpelas cuando se soliciten.

Estructura

Supongo que las notificaciones se parecerán a:

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

Detrás de las cortinas, esto podría verse así:

+--------+-----------+--------+-----------------+-------------------------------------------+
| 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. |
+--------+-----------+--------+-----------------+-------------------------------------------+

Nota: No recomiendo agrupar las notificaciones dentro de la base de datos, hazlo en tiempo de ejecución, lo que mantiene las cosas mucho más flexibles.

  • No leído
    Cada notificación debe tener una bandera para indicar si el destinatario ya abrió la notificación.
  • Destinatario
    Define quién recibe la notificación.
  • Remitente
    Define quién activó la notificación.
  • Tipo
    En lugar de tener cada mensaje en texto sin formato dentro de su base de datos, cree tipos. De esta manera, puede crear controladores especiales para diferentes tipos de notificaciones dentro de su backend. Reducirá la cantidad de datos almacenados dentro de su base de datos y le dará aún más flexibilidad, habilitará una fácil traducción de notificaciones, cambios de mensajes anteriores, etc.
  • Referencia
    La mayoría de las notificaciones tendrán una Referencia a un registro en su base de datos o su aplicación.

Cada sistema en el que he estado trabajando tenía un simple 1 a 1 relación de referencia en una notificación, es posible que tenga un 1 a n tenga en cuenta que continuaré mi ejemplo con 1:1. Esto también significa que no necesito un campo que defina a qué tipo de objeto se hace referencia porque está definido por el tipo de notificación.

Tabla SQL

Ahora, al definir una estructura de tabla real para SQL, tomamos algunas decisiones en términos del diseño de la base de datos. Iré con la solución más simple que se verá así:

+--------------+--------+---------------------------------------------------------+
| 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.            |
+--------------+--------+---------------------------------------------------------+

O para los perezosos, el comando SQL create table para este ejemplo:

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;

Servicio PHP

Esta implementación depende completamente de las necesidades de su aplicación, Nota: Este es un ejemplo, no el estándar de oro sobre cómo construir un sistema de notificación en PHP.

Modelo de notificación

Este es un modelo base de ejemplo de la notificación en sí, nada especial, solo las propiedades necesarias y los métodos abstractos messageForNotification y messageForNotifications esperábamos que se implementara en los diferentes tipos de notificación.

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);
    }
}

Deberá agregar un constructor , captadores , establecedores y ese tipo de cosas por ti mismo en tu propio estilo, no voy a proporcionar un sistema de notificación listo para usar.

Tipos de notificación

Ahora puedes crear una nueva Notification subclase para cada tipo. El siguiente ejemplo manejaría la acción me gusta de un comentario:

  • A Ray le ha gustado tu comentario. (1 notificación)
  • A John y Jane les gustó tu comentario. (2 notificaciones)
  • A Jane, Johnny, James y Jenny les gustó tu comentario. (4 notificaciones)
  • A Jonny, James y 12 personas más les gustó tu comentario. (14 notificaciones)

Ejemplo de implementación:

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
    }
}

Administrador de notificaciones

Para trabajar con sus notificaciones dentro de su aplicación, cree algo como un administrador de notificaciones:

class NotificationManager
{
    protected $notificationAdapter;

    public function add(Notification $notification);

    public function markRead(array $notifications);

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

El notificationAdapter La propiedad debe contener la lógica en comunicación directa con su backend de datos en el caso de mysql de este ejemplo.

Crear notificaciones

Usando mysql triggers no está mal, porque no hay una solución incorrecta. Lo que funciona, funciona... Pero recomiendo enfáticamente no dejar que la base de datos maneje la lógica de la aplicación.

Entonces, dentro del administrador de notificaciones, es posible que desee hacer algo como esto:

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(),
        ]);
    }
}

Detrás del add método del notificationAdapter puede ser un comando de inserción mysql sin procesar. El uso de esta abstracción de adaptador le permite cambiar fácilmente de mysql a una base de datos basada en documentos como mongodb lo que tendría sentido para un sistema de notificación.

El isDoublicate método en el notificationAdapter simplemente debe comprobar si ya existe una notificación con el mismo recipient , sender , type y reference .

No puedo señalar lo suficiente que esto es solo un ejemplo. (También tengo que acortar los próximos pasos, esta publicación se está volviendo ridículamente larga -.-)

Entonces, suponiendo que tiene algún tipo de controlador con una acción cuando un maestro sube la tarea:

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);
    }
}

Creará una notificación para cada estudiante del maestro cuando cargue una nueva tarea.

Leyendo las notificaciones

Ahora viene la parte difícil. El problema con la agrupación en el lado de PHP es que tendrá que cargar todos notificaciones del usuario actual para agruparlas correctamente. Esto sería malo, bueno, si solo tiene unos pocos usuarios, probablemente no sería un problema, pero eso no lo hace bueno.

La solución fácil es simplemente limitar la cantidad de notificaciones solicitadas y solo agruparlas. Esto funcionará bien cuando no haya muchas notificaciones similares (como 3 o 4 de cada 20). Pero digamos que la publicación de un usuario/estudiante obtiene alrededor de cien Me gusta y solo selecciona las últimas 20 notificaciones. El usuario solo verá que a 20 personas les gustó su publicación y esa sería su única notificación.

Una solución "correcta" sería agrupar las notificaciones que ya están en la base de datos y seleccionar solo algunas muestras por grupo de notificaciones. Entonces solo tendría que inyectar el conteo real en sus mensajes de notificación.

Probablemente no hayas leído el texto a continuación, así que déjame continuar con un fragmento:

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

Ahora sabe qué notificaciones deberían existir para el usuario dado y cuántas notificaciones contiene el grupo.

Y ahora la parte de mierda. Todavía no pude encontrar una mejor manera de seleccionar un número limitado de notificaciones para cada grupo sin hacer una consulta para cada grupo. Todas las sugerencias aquí son muy bienvenidas.

Así que hago 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;
}

Ahora continuaré asumiendo que el notificationAdapter s get implementa esta agrupación y devuelve una matriz como esta:

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

Porque siempre tenemos al menos una notificación en nuestro grupo y nuestro pedido prefiere No leído y Nuevo notificaciones podemos usar la primera notificación como muestra para renderizar.

Entonces, para poder trabajar con estas notificaciones agrupadas, necesitamos un nuevo 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);
    }
}

Y finalmente podemos juntar la mayoría de las cosas. Así funciona la función get en el NotificationManager podría verse como:

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;
}

Y realmente finalmente dentro de una posible acción del 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);
}