Push em Tempo Real com o Symfony!

Andréia Bohner
6 min readMar 11, 2019

--

Poder transmitir dados em tempo real de servidores para clientes é um requisito para muitas aplicações web e móveis modernas.

Como os SAPIs PHP mais populares não são capazes de manter conexões persistentes, o Symfony não estava fornecendo nenhum método integrado para enviar dados aos clientes até agora. Era obrigatório contar com provedores de serviços externos ou com outras linguagens de programação para implementar esse recurso.

Esse tempo acabou! O Symfony conta agora com o Componente Mercure e o MercureBundle!

Como seus nomes indicam, eles implementam Mercure, um protocolo aberto projetado desde o início para publicar atualizações do servidor para os clientes. É uma alternativa moderna e eficiente à timer-based polling e WebSocket.

Esses dois novatos da família Symfony são projetados especificamente para casos de uso que exigem recursos em tempo real, como:

  • Criar uma interface do usuário reagindo ao vivo a alterações feitas por outros usuários (por exemplo, um usuário altera os dados atualmente navegados por vários outros usuários, todas as interfaces de usuário são atualizadas instantaneamente)
  • notificar o usuário quando um job assíncrono foi concluído
  • criar aplicações de bate-papo

Por ser construído com os principais Server-Sent Events (SSE), o Mercure é suportado na maioria dos navegadores modernos (Edge e IE exigem um polyfill) e tem implementações de alto nível em muitas linguagens de programação.

O Mercure vem com um mecanismo de autorização, reconexão automática em caso de problemas de rede com recuperação de atualizações perdidas, push “sem conexão” para smartphones e auto-descoberta (um cliente suportado pode descobrir e assinar atualizações de um determinado recurso automaticamente graças a um cabeçalho HTTP específico).

Todos esses recursos são suportados na integração do Symfony!

Ao contrário do WebSocket, que é compatível apenas com o HTTP 1.x, o Mercure utiliza os recursos de multiplexação fornecidos pelo HTTP/2 e HTTP/3 (mas também suporta versões mais antigas do HTTP).

No vídeo abaixo, você pode ver como uma API web Symfony utiliza o Mercure e a API Platform para atualizar em tempo real uma aplicação React e um aplicação móvel (React Native) gerada usando o gerador de cliente do API Platform:

Instalar

O primeiro passo para poder realizar “push” com o Symfony é instalar a receita oficial do Symfony Flex para a integração com o Mercure:

$ composer require mercure

Para gerenciar conexões persistentes, o Mercure conta com um Hub: um servidor dedicado que lida com conexões SSE persistentes com os clientes. A aplicação Symfony publica as atualizações para o hub, que as transmitirá para os clientes.

Uma implementação oficial e de código aberto (AGPL) de um Hub pode ser baixada como um binário estático em Mercure.rocks.

Execute o seguinte comando para iniciá-lo:

$ JWT_KEY='aVerySecretKey' ADDR='localhost:3000' ALLOW_ANONYMOUS=1 ./mercure

Como alternativa ao binário, uma imagem do Docker, um gráfico Helm para o Kubernetes, e um Hub de Alta Disponibilidade gerenciado, também são fornecidos pelo Mercure.rocks.

A distribuição da API Platform vem com uma configuração do Docker Compose, bem como um gráfico Helm para o Kubernetes, que são 100% compatíveis com o Symfony e contêm um hub Mercure. Você pode copiá-los em seu projeto, mesmo que não use a API Platform.

Agora, defina a URL do seu hub como o valor da variável env MERCURE_PUBLISH_URL. O arquivo .env do seu projeto foi atualizado pela receita do Flex para fornecer valores de exemplo. Configure-o para a URL do Hub Mercure (http://localhost:3000/hub por padrão).

Além disso, a aplicação Symfony deve carregar um JSON Web Token (JWT) no Hub Mercure para ser autorizada a publicar atualizações. Esse JWT deve ser armazenado na variável de ambiente MERCURE_JWT_SECRET.

O JWT deve ser assinado com a mesma chave secreta que a usada pelo Hub para verificar o JWT (aVerySecretKey em nosso exemplo). Seu payload deve conter pelo menos a estrutura a seguir para permitir a publicação:

{
"mercure": {
"publish": []
}
}

Como o array está vazio, a aplicação Symfony só estará autorizada a publicar atualizações públicas.

O site jwt.io é uma maneira conveniente de criar e assinar JWTs. Confira este exemplo JWT, que concede direitos de publicação para todos os destinos (observe a estrela no array). Não se esqueça de definir sua chave secreta corretamente na parte inferior do painel direito do formulário!

CUIDADO!
Não coloque a chave secreta em MERCURE_JWT_SECRET, isso não funcionará! Essa variável de ambiente deve conter um JWT, assinado com a chave secreta.

Além disso, mantenha a chave secreta e os JWTs… secretos!

Publicar

O Componente Mercure fornece um objeto valor Update que representa a atualização a ser publicada. Ele também fornece um serviço Publisher para despachar atualizações para o hub.

O serviço Publisherpode ser injetado em quaisquer outros serviços, incluindo controladores, usando o recurso de autowiring do serviço:

// src/Controller/PublishController.php
namespace App\Controller;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mercure\Publisher;
use Symfony\Component\Mercure\Update;

class PublishController
{
public function __invoke(Publisher $publisher): Response
{
$update = new Update(
'http://example.com/books/1',
json_encode(['status' => 'OutOfStock'])
);

// The Publisher service is an invokable object
$publisher($update);

return new Response('published!');
}
}

O primeiro parâmetro a passar para o construtor Update é o tópico que está sendo atualizado. Este tópico deve ser um IRI (Identificador de Recurso Internacionalizado, RFC 3987): um identificador exclusivo do recurso que está sendo despachado.

Normalmente, esse parâmetro contém a URL original do recurso transmitido ao cliente, mas ele pode ser qualquer IRI válido, não precisa ser uma URL que exista (de maneira semelhante aos namespaces XML).

O segundo parâmetro do construtor é o conteúdo da atualização. Pode ser qualquer coisa, armazenada em qualquer formato. No entanto, é recomendado serializar o recurso em um formato hipermídia, como JSON-LD, Atom, HTML ou XML.

Inscrever

A inscrição em atualizações no JavaScript é simples:

const es = new EventSource('http://localhost:3000/hub?topic=' + encodeURIComponent('http://example.com/books/1'));
es.onmessage = e => {
// Will be called every time an update is published by the server
console.log(JSON.parse(e.data));
}

Como você pode ver, não precisa de nenhuma biblioteca JS ou SDK. Apenas funciona!

O Mercure também permite inscrever em diversos tópicos e usar Templates URI como padrões:

// URL is a built-in JavaScript class to manipulate URLs
const u = new URL('http://localhost:3000/hub');
u.searchParams.append('topic', 'http://example.com/books/1');
// Subscribe to updates of several Book resources
u.searchParams.append('topic', 'http://example.com/books/2');
// All Review resources will match this pattern
u.searchParams.append('topic', 'http://example.com/reviews/{id}');

const es = new EventSource(u);
es.onmessage = e => {
console.log(JSON.parse(e.data));
}

Dica: O Google Chrome DevTools integra de forma nativa uma interface de usuário prática exibindo ao vivo os eventos recebidos.

Para usá-lo:

  • abra o DevTools
  • selecione a guia “Network
  • clique na requisição para o hub do Mercure
  • clique na subguia “EventStream”.

Dica: Teste se um Template URI corresponde a uma URL usando o depurador on-line.

Envio Assíncrono

Em vez de chamar diretamente o serviço Publisher, você também pode permitir que o Symfony despache as atualizações de forma assíncrona graças à integração fornecida com o componente Messenger.

Primeiro, instale o componente Messenger:

$ composer require messenger

Você também deve configurar um transporte (se você não fizer isso, o handler será chamado de forma síncrona).

Agora, você pode simplesmente enviar o Mercure Update para o Message Bus do Messenger, ele será tratado automaticamente:

// src/Controller/PublishController.php
namespace App\Controller;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Mercure\Update;

class PublishController
{
public function __invoke(MessageBusInterface $bus): Response
{
$update = new Update(
'http://example.com/books/1',
json_encode(['status' => 'OutOfStock'])
);

// Sync, or async (RabbitMQ, Kafka...)
$bus->dispatch($update);

return new Response('published!');
}
}

APIs Web

Ao criar uma API web, é conveniente poder realizar push instantaneamente de novas versões dos recursos para todos os dispositivos conectados e atualizar suas visualizações.

A API Platform pode usar o Componente Mercure para despachar atualizações automaticamente, toda vez que um recurso da API é criado, modificado ou excluído.

Comece instalando a biblioteca usando sua receita oficial:

$ composer require api

Em seguida, criar a entidade a seguir é suficiente para obter uma API de hipermídia completa e transmissão automática de atualização por meio do hub do Mercure:

// src/Entity/Book.php
namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;

/**
* @ApiResource(mercure=true)
* @ORM\Entity
*/
class Book
{
/**
* @ORM\Id
* @ORM\Column
*/
public $name;

/**
* @ORM\Column
*/
public $status;
}

Como mostrado no vídeo anterior, o Gerador de Cliente da API Platform também permite gerar aplicações React e React Native completas a partir desta API. Essas aplicações renderizarão automaticamente o conteúdo das atualizações do Mercure em tempo real.

Consulte a documentação dedicada da API Platform para saber mais sobre o suporte do Mercure.

Indo além

Há mais! O Symfony aproveita todos os recursos do protocolo Mercure, incluindo autorização, descoberta através de Web Linking e configuração avançada de SSE. Confira os novos artigos no Symfony Docs sobre o Mercure para saber todos os detalhes!

Tradução de: Symfony Gets Real-time Push Capabilities!

--

--