Application · Adapter
Sockets
Publish real-time updates through public or private application ports, then bind Mercure or Laravel broadcasting at the edge.
Sockets is a transport-neutral publication boundary, not a durable event log. Publisher publishes
public updates; PrivatePublisher adds private publication intent. Mercure and Laravel adapters are
available, but authentication, authorization, retry, retention, and client subscription remain
consumer and operator responsibilities.
Sockets provides public and private publication ports with Symfony Mercure and Laravel broadcasting adapters. It publishes real-time messages to the selected transport; it is not a durable event store, job queue, retry engine, or proof that a subscriber received a message.
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.
Laravel broadcasting — LaravelBroadcastPublisher wraps Laravel's configured Broadcaster and
publishes ['message' => $message] under a consumer-selected event name. LaravelPrivatePublisher
decorates a Publisher and prefixes the topic with private-; channel authorization and broadcaster
configuration remain application-owned.
Installing the Mercure Component¶
This library declares symfony/mercure as a suggested dev dependency. Your project must add it to require:
Fight Common's supported adapter line declares symfony/mercure ^0.7 as the optional package.
Wiring Up the Publisher¶
Laravel applications construct LaravelBroadcastPublisher from the configured broadcaster and an
event name, then bind it to Publisher. Bind LaravelPrivatePublisher to PrivatePublisher only when
the application's channel authorization recognizes the private- naming convention. No automatic
socket service provider is shipped.
use Fight\Common\Adapter\Socket\Laravel\LaravelBroadcastPublisher;
use Fight\Common\Adapter\Socket\Laravel\LaravelPrivatePublisher;
$public = new LaravelBroadcastPublisher($broadcaster, 'fight.message');
$private = new LaravelPrivatePublisher($public);
The remaining examples show the Mercure composition path.
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, select the separate PrivatePublisher port and its
PrivateMercureHubPublisher adapter. This is independent of the existing public
Publisher/MercureHubPublisher selection; application composition owns topic
authorization and Mercure JWT configuration.
use Fight\Common\Adapter\Socket\PrivateMercureHubPublisher;
use Fight\Common\Application\Socket\PrivatePublisher;
$privatePublisher = new PrivateMercureHubPublisher($hub);
$privatePublisher->pushPrivate(
'https://example.com/users/42',
json_encode(['message' => 'private'], JSON_THROW_ON_ERROR)
);
PrivateMercureHubPublisher marks its Mercure Update private. It does not
authorize subscriptions, create credentials, or transform the supplied topic or
payload. Install symfony/mercure in the consuming application (it is a Composer
suggestion, not a Fight Common production dependency), then bind either or both
ports explicitly:
Fight\Common\Application\Socket\Publisher:
alias: Fight\Common\Adapter\Socket\MercureHubPublisher
Fight\Common\Application\Socket\PrivatePublisher:
alias: Fight\Common\Adapter\Socket\PrivateMercureHubPublisher
For Laravel, LaravelPrivatePublisher marks privacy only through the private- topic prefix. That
prefix is not authorization by itself: the consuming application must authenticate subscribers and
enforce channel entitlement through its configured broadcaster.
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.
Mercure and Laravel public publishers wrap underlying publish failures in SocketException.
LaravelPrivatePublisher delegates to its wrapped Publisher, so it preserves that publisher's
failure behavior. None of these adapters retry or buffer a failed publication.
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);
}
}
Client-Side Subscription¶
const url = new URL('https://hub.example.com/.well-known/mercure');
url.searchParams.append('topic', 'https://example.com/books/1');
const eventSource = new EventSource(url);
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Book updated:', data);
};