1 year ago

#232698

test-img

Francisco

How to configure Mercure bundle to send notifications to web UI on a Symfony application

I'm starting with Mercure hub, but I have some doubts that are stopping me. The idea is add to a Symfony 5.4 web application (its a management web application based on sf 5.4) the classic notification bell for users after a certain operation< in this specific case, the notification must appear once a new patient transfer request is registered. The notification bell will be activated for users who belong to the medical institution in which the patient is currently registered.

Note: locally it was installed a WAMP server with a https virtual host over Windows 10.

First, I installed the mercure bundle: composer require mercure

I have downloaded the mercure executable for Windows OS.

The necessary environment variables: I understand that MERCURE_URL its a address for communication between the web server and the mercure hub, and MERCURE_PUBLIC_URL its a address for the clients subscription to the hub.

Doubt: Based on what data the MERCURE_JWT_SECRET should be generated. This must be a static jwt token stored in an environment variable that doesn't change?

Mercure recipe on mercure.yaml:

mercure:
    hubs:
        default:
            url: '%env(MERCURE_URL)%'
            public_url: '%env(MERCURE_PUBLIC_URL)%'
            jwt:
                secret: '%env(MERCURE_JWT_SECRET)%'
                publish: '*'

Doubt: I don't understand the composition of JWT on the context of mercure bundle, how should be the publish section of the configuration?

This is the action that store the new patient transfer request and starts the notification process by sending a message to the configured message bus:

/**
     * 
     * @param Request $request
     * @param ManagerRegistry $manager
     * @param UserInterface $user
     * @param UuidEncoder $uuidEncoder
     * @param LoggerInterface $logger
     * @param \Symfony\Component\Messenger\MessageBusInterface $messageBus
     * @return Response
     * @throws Exception
     * @throws type
     */
    public function solicitarTrasladoAction(Request $request, ManagerRegistry $manager, UserInterface $user, UuidEncoder $uuidEncoder, LoggerInterface $logger, \Symfony\Component\Messenger\MessageBusInterface $messageBus): Response
    {
        if ($request->isXmlHttpRequest()) {
            try {
                if (!$request->isMethod(Request::METHOD_POST)) {
                    return new Response("Operación no soportada!!!", 500);
                }

                $id = $uuidEncoder->decode($request->request->get('embarazadaId', null));
                $cmfDestinoId = $uuidEncoder->decode($request->get('cmfDestino', null));

                $em = $manager->getManager();

                $conn = $em->getConnection();
                $conn->beginTransaction();
                try {

                    $cmfDestino = $manager->getRepository(EstructuraOrganizativa::class)->findOneJoinTipoEstructuraOrganizativa($cmfDestinoId);
                    if (\is_null($cmfDestino)) {
                        throw new \Exception("No se encontró la unidad de destino.", 404);
                    } else if ($cmfDestino->getTipoEstructuraOrganizativa()->getId() !== 6) {
                        throw new \Exception("No es posible ubicar una embarazada fuera de un CMF.", 406);
                    }

                    $embarazada = $em->getRepository(Embarazada::class)->findOneJoinEstructuraOrganizativa($id);
                    if (\is_null($embarazada)) {
                        throw new \Exception("No se encontró la embarazada solicitada", 404);
                    }

                    if ($embarazada->getEstructuraOrganizativa()->getId() === $cmfDestino->getId()) {
                        throw new \Exception("No es posible reubicar la embarazada en el CMF al que pertenece actualmente.", 406);
                    }

                    $posibleSolicitud = $em->getRepository(\App\Entity\SolicitudTrasladoEmbarazada::class)->findOneBy(['embarazada' => $embarazada, 'estado' => 'solicitado']);
                    if (!\is_null($posibleSolicitud)) {
                        throw new \Exception("Ya existe una solicitud de traslado para esta paciente.", 406);
                    }

                    $nuevaSolicitudTraslado = new \App\Entity\SolicitudTrasladoEmbarazada();
                    $nuevaSolicitudTraslado->setEmbarazada($embarazada);
                    $nuevaSolicitudTraslado->setCmfDestino($cmfDestino);
                    $nuevaSolicitudTraslado->setEstado("solicitado");
                    $nuevaSolicitudTraslado->setAsunto("Solicitud de traslado");
                    $nuevaSolicitudTraslado->setMensaje(\sprintf("Solicito reubicar a '%s' hacia provincia '%s', municipio '%s', CMF: %s.", $embarazada->getNombre(), $cmfDestino->getParent()->getParent()->getParent()->getParent()->getTitle(), $cmfDestino->getParent()->getParent()->getParent()->getTitle(), $cmfDestino->getTitle()));

                    $em->persist($nuevaSolicitudTraslado);
                    $em->flush();
                    $conn->commit();
                } catch (\Exception $exc) {
                    $conn->rollback();
                    $conn->close();

                    if (in_array($exc->getCode(), array(404, 406))) {
                        return new Response($exc->getMessage(), 500);
                    }

                    $logger->error(sprintf("[%s:%s]: %s", __CLASS__, __FUNCTION__, $exc->getMessage()));
                    return new Response("Ocurrió un error inesperado al ejecutar la operación", 500);
                }

                $messageBus->dispatch(new \App\Message\NotificacionSolicitudTrasladoEmbarazadaMessage($uuidEncoder->encode($nuevaSolicitudTraslado->getIdPublico())));

                return new Response("La solicitud de traslado fue enviada satisfactoriamente");
            } catch (\Exception $exc) {
                $logger->error(sprintf("[%s:%s]: %s", self::class, __FUNCTION__, $exc->getMessage()));
                return new Response("Ocurrió un error inesperado al ejecutar la operación", 500);
            }
        } else {
            throw $this->createNotFoundException("Recurso no encontrado");
        }
    }

Before send the response to the user for the generation of the new patient transfer request, and to reach an asynchronously behavior, I have added a new message to a Symfony message bus (specifically its use a doctrine transport), passing as part of a content of message the public id (idPublico) of the new registry that has been created.

// content of messenger.yaml recipe:

framework:
    messenger:
        # Uncomment this (and the failed transport below) to send failed messages to this transport for later handling.
        failure_transport: failed
        reset_on_message: true
        transports:
            # https://symfony.com/doc/current/messenger.html#transport-configuration
            async: 
                dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
                options:
                    auto_setup: false
            failed: 
                dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
                options:
                    queue_name: 'failed'
            sync: 'sync://'

        routing:
            # Route your messages to the transports
             'App\Message\NotificacionSolicitudTrasladoEmbarazadaMessage': async

The Message class:

namespace App\Message;

class NotificacionSolicitudTrasladoEmbarazadaMessage
{

    private $content;

    public function __construct(string $content)
    {
        $this->content = $content;
    }

    public function getContent(): string
    {
        return $this->content;
    }
}

The Message handler class, here its encapsulated the mercure hub logic on invoke function:

use App\Message\NotificacionSolicitudTrasladoEmbarazadaMessage;
use App\Repository\SolicitudTrasladoEmbarazadaRepository;
use App\Services\UuidEncoder;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Mercure\Update;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;

/**
 * Envia al Mercure bus la nueva solicitud de traslado para ser notificada a los usuarios
 */
class NotificacionSolicitudTrasladoEmbarazadaMessageHandler implements MessageHandlerInterface
{

    private $mercureHub;
    private $repositorySolicitudTrasladoEmbarazada;
    private $uuidEncoder;

    public function __construct(HubInterface $mercureHub, SolicitudTrasladoEmbarazadaRepository $repositorySolicitudTrasladoEmbarazada, UuidEncoder $uuidEncoder)
    {
        $this->mercureHub = $mercureHub;
        $this->repositorySolicitudTrasladoEmbarazada = $repositorySolicitudTrasladoEmbarazada;
        $this->uuidEncoder = $uuidEncoder;
    }

    public function __invoke(NotificacionSolicitudTrasladoEmbarazadaMessage $message)
    {
        // get the real content of the message
        $idPublico = $this->uuidEncoder->decode($message->getContent());

        // get the fresh object from the database
        $solicitud = $this->repositorySolicitudTrasladoEmbarazada->findOneBy(['idPublico' => $idPublico]);
        if(\is_null()){
            return;
        }
        
        /** Count all unatended request of movements **/
        $totalNoAtendidas = $this->repositorySolicitudTrasladoEmbarazada->contarNoAtendidas($solicitud->getCmfDestino());

        // make a update to the hub
        $actualizacion = new Update(
                'https://the-uri-of-resource', // This URI must be generated with symfony routing services, or its a simple formality?                
                \json_encode(['ultimaSolictud' => $solicitud->getAsunto(), 'totalNoAtendidas' => $totalNoAtendidas]),
                true // privado necesita jwt auth set that need JWT auth
        );

        $this->mercureHub->publish($actualizacion);

        return new Response("Publicado");
    }
}

Doubt: The URI parameter of Update object must be a generated URL with Symfony routing or its a simple "formality"?

At this point the messages are constantly passing to a failed queue, retrying to be sent to a mercure hub, because I have not been able to configure and run the mercure.exe

enter image description here

For the client side, the subscription case, I still don't understand how to set the JWT token, but I'll leave that for another question

symfony

wampserver

mercure

0 Answers

Your Answer

Accepted video resources