Mercure Hub Publisher¶
The Mercure Hub Publisher implements the Socket\Publisher port using the Symfony Mercure component. It publishes real-time messages to a Mercure hub, enabling server-sent events for connected clients.
Table of Contents¶
- Overview
- Installing the Mercure Component
- Wiring Up the Publisher
- Publishing Messages
- Error Handling
- Complete Example
Overview¶
The system is built from three cooperating pieces:
Application code
└─► $publisher->push($topic, $message)
└─► MercureHubPublisher::push()
└─► HubInterface::publish(new Update($topic, $data))
└─► Mercure Hub
└─► SSE pushed to subscribed clients
Publisher interface — the application-layer port at Fight\Common\Application\Socket\Publisher. Defines a single method:
MercureHubPublisher — the adapter at Fight\Common\Adapter\Socket\MercureHubPublisher. Takes a Symfony HubInterface and translates push() calls into $hub->publish(new Update(...)).
HubInterface — the Symfony Mercure component's current API (v0.5+). The older Publisher/PublisherInterface from the Mercure component is deprecated; this adapter uses the new HubInterface API.
Installing the Mercure Component¶
This library declares symfony/mercure as a suggested dev dependency. Your project must add it to require:
If you are running PHP 8.5 with Symfony 8.0, version ^0.7 is compatible.
Wiring Up the Publisher¶
Option 1: Using MercureBundle (recommended)¶
If you have symfony/mercure-bundle installed with autoconfigure enabled, the default Hub service is already available as mercure.hub.default. Register the adapter as an alias:
# config/services.yaml
services:
Fight\Common\Adapter\Socket\MercureHubPublisher:
arguments:
$hub: '@mercure.hub.default'
Fight\Common\Application\Socket\Publisher:
alias: Fight\Common\Adapter\Socket\MercureHubPublisher
Configure the hub URL and JWT provider in mercure.yaml:
# config/packages/mercure.yaml
mercure:
hubs:
default:
url: '%env(MERCURE_URL)%'
public_url: '%env(MERCURE_PUBLIC_URL)%'
jwt:
secret: '%env(MERCURE_JWT_SECRET)%'
publish: '*'
Option 2: Manual service definition¶
If you are not using MercureBundle, create the Hub manually:
# config/services.yaml
services:
Symfony\Component\Mercure\Hub:
arguments:
$url: '%env(MERCURE_URL)%'
$jwtProvider: '@mercure.jwt_provider'
Fight\Common\Adapter\Socket\MercureHubPublisher:
arguments:
$hub: '@Symfony\Component\Mercure\Hub'
Fight\Common\Application\Socket\Publisher:
alias: Fight\Common\Adapter\Socket\MercureHubPublisher
The JWT provider must implement Symfony\Component\Mercure\Jwt\TokenProviderInterface. For development you can use StaticTokenProvider:
services:
Symfony\Component\Mercure\Jwt\StaticTokenProvider:
arguments:
$token: '%env(MERCURE_JWT_TOKEN)%'
Symfony\Component\Mercure\Hub:
arguments:
$url: '%env(MERCURE_URL)%'
$jwtProvider: '@Symfony\Component\Mercure\Jwt\StaticTokenProvider'
Publishing Messages¶
Basic Public Update¶
use Fight\Common\Application\Socket\Publisher;
class BookController
{
public function __construct(private Publisher $publisher)
{
}
public function update(int $id): JsonResponse
{
// ... update the book ...
$this->publisher->push(
'https://example.com/books/' . $id,
json_encode(['status' => 'updated']),
);
return new JsonResponse(['status' => 'success']);
}
}
Topics are typically URL strings that clients subscribe to. The topic is passed directly to Mercure's Update object — it accepts both strings and arrays of strings.
Private Updates¶
To send private updates (visible only to the target user), pass the topic and data as usual. Privacy is controlled by the Mercure JWT token, not by the Publisher interface. If you need to mark an update as private, construct the Update directly and call $hub->publish() instead:
use Symfony\Component\Mercure\Update;
$this->hub->publish(new Update(
'https://example.com/user/' . $userId . '/notifications',
$message,
private: true,
));
Error Handling¶
MercureHubPublisher::push() wraps any exception thrown by HubInterface::publish() in a SocketException:
use Fight\Common\Application\Socket\Exception\SocketException;
try {
$this->publisher->push($topic, $message);
} catch (SocketException $e) {
// Hub unreachable, JWT invalid, etc.
// $e->getPrevious() contains the original exception
}
SocketException extends SystemException (a domain-level exception). Both extend PHP's \RuntimeException, so they can be caught at any level.
Complete Example¶
The following example configures the Mercure hub, registers the publisher, and uses it in a controller to broadcast a notification when a book is updated.
Configuration¶
# config/packages/mercure.yaml
mercure:
hubs:
default:
url: '%env(MERCURE_URL)%'
jwt:
secret: '%env(MERCURE_JWT_SECRET)%'
publish: '*'
# config/services.yaml
services:
_defaults:
autowire: true
autoconfigure: true
Fight\Common\Adapter\Socket\MercureHubPublisher: ~
Fight\Common\Application\Socket\Publisher:
alias: Fight\Common\Adapter\Socket\MercureHubPublisher
Controller¶
<?php
declare(strict_types=1);
namespace App\Controller;
use Fight\Common\Application\Socket\Publisher;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;
class BookController extends AbstractController
{
public function __construct(private Publisher $publisher)
{
}
#[Route('/books/{id}', methods: ['PATCH'])]
public function update(int $id, Request $request): JsonResponse
{
$data = json_decode($request->getContent(), true);
// ... persist the update ...
$this->publisher->push(
'https://example.com/books/' . $id,
json_encode([
'title' => $data['title'] ?? null,
'status' => 'updated',
]),
);
return new JsonResponse(['status' => 'success'], 200);
}
}