Skip to content

Application · Adapter

Sockets

Publish real-time updates through public or private application ports, then bind Mercure or Laravel broadcasting at the edge.

Application code chooses topics and payloads; adapters translate them into the selected real-time transport.

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

  1. Overview
  2. Installing the Mercure Component
  3. Wiring Up the Publisher
  4. Publishing Messages
  5. Error Handling
  6. 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:

public function push(string $topic, string $message): void;

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 broadcastingLaravelBroadcastPublisher 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:

composer require symfony/mercure

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.

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