Дата обновления перевода 2021-12-24
Передача данных клиентам, используя протокол Mercure¶
Трансляция данных с серверов клиентам в реальном времени является требованием для множества совеременных веб и мобильных приложений.
Создание UI, реагирующего в режиме живого времени на изменения, сделанные другими пользователями (например, пользователь изменяет данные, которые в текущий момент просматривают несколько других пользователей, и все UI автоматически обновляются), уведомляющего пользователя, когда была выполнена асинхронная работа, или создание чат-приложений - наиболее распространенные случаи применения, требующие “пуш” возможностей.
Symfony предоставляет доступный компонент, который строится над протоколом Mercure, специально спроектированным для этого класса случаев применения.
Mercure - это открытый протокол, созданный с нуля, для публикации обновлений с сервера клиентам. Это современная и эффективная альтернатива поллингу, основанному на таймере, и WebSocket.
Так как он строится поверх Событий, отправленных сервером (SSE), Mercure поддерживается сразу после установки в большинстве современных браузеров (Edge и IE требуют `polyfill`_), и имеет реализации на высоком уровне во многих языках программирования.
Mercure поставляется с механизмом авторизации, автоматическим переподключением в случае проблем сети с извлечением утерянных обновлений, наличием API, пуш-сообщения “без-соединения” для смартфонов и автоматическим обнаружением (поддерживаемый клиент может автоматически обнаружить и подписаться на обновления заданного ресурса, благодаря специфическому HTTP-заголовку).
Все эти функции поддерживаются в интеграции Symfony.
В отличие от WebSocket, который совместим только с HTTP 1.x, Mercure использует возможности мультиплексирования, предоставленные HTTP/2 и HTTP/3 (но также поддерживает и более старые версии HTTP).
В этой записи вы можете увидеть, как веб-API Symfony использует Mercure и платформу API, чтобы делать обновления в прямом эфире в приложении React и мобильном приложении (React Native), которые генерируются с использованием генератора клиентов платформы API.
Установка¶
Установка компонента Symfony¶
В приложениях, использующих Symfony Flex, запустите эту команду, чтобы установить поддержку Mercure, перед его использованием:
1 | $ composer require mercure
|
Запуск хаба Mercure¶
Чтобы управлять устойчивыми соединениям, Mercure полагается на хаб: определенный сервер, который обрабатывает постоянные SSE-соединения с клиентами. Приложение Symfony публикует обновления в хабе, который будет транслировать их клиентам.

Официальная и общедоступная реализация (AGPL) хаба доступна для скачивания в виде статической бинарности с Mercure.rocks. Изображение Docker, схема Helm для Kubernetes и управляемый Хаб высокой доступности (High Availability Hub) также предоставляются.
Если вы используете Symfony Docker или `Распределение платформы API`_, хаб Mercure автоматически устанавливается, и ваше приложение Symfony автоматически конфигурируется для его использования. Вы можете сразу перейти к следующему разделу.
Если вы используете Локальный веб-сервер Symfony, хаб Mercure будет автоматически доступен в качестве сервиса Docker, благодаря его :ref:`интеграции с Docker <symfony-server-docker>.
Убедитесь в том, что последние версии Docker и Docker Compose правильно установлены на
вашем компьютере и запустите Локальный веб-сервер Symfony с опцией --no-tls
:
1 | $ symfony server:start --no-tls -d
|
Установка пакета Symfony¶
Выполните эту команду, чтобы установить поддержку Mercure, перед его использованием:
1 | $ composer require mercure
|
Symfony Flex автоматически установил и сконфигурировал
MercureBundle. Он также создал (если это было необходимо) и сконфигурировал
определение Docker Compose, которое предоставляет сервис Mercure. Выполните docker-compose up
,
чтобы запустить его.
Конфигурация¶
Предпочитаемый способ конфигурирования MercureBundle - с использованием переменных окружения.
Когда MercureBundle будет установлен, файл .env
вашего проекта, будет обновлен
рецептом Flex, чтобы включать в себя доступные переменные окружения.
Если вы используете Локальный веб-сервер Symfony, Symfony Docker или Распространение платформы API, приложение Symfony конфигурируется автоматически, и вы можете сразу перейти к следующему разделу.
В других случаях, установите URL вашего хаба как значения переменных окружения
MERCURE_URL
и MERCURE_PUBLIC_URL
.
Иногда приложению Symfony может понадобиться вызвать другой URL (обычно для публикации)
и другого клиента JavaScript. Это особенно распространено, когда приложение Symfony должно
использовать локальный URL, а код JavaScript клиентской стороны - публичный. В таком случае,
MERCURE_URL
должен содержать локальный URL, который будет использован приложением
Symfony (например, https://mercure/.well-known/mercure
), а MERCURE_PUBLIC_URL
-
публичнодоступным URL (например, https://example.com/.well-known/mercure
).
Клиенты также должны иметь веб-токен JSON (JWT), чтобы хаб Mercure был авторизован для публикации обновлений, и, иногда, подписок.
Этот JWT должен храниться в переменной окружения MERCURE_JWT_TOKEN
.
JWT должен быть подписан тем же секретным ключом, который используется хабом
для верификации JWT (!ChangeMe!
в нашем примере).
Его полезная нагрузка должна содержать как минимум следующую структуру,
чтобы иметь разрешение на публикацию:
1 2 3 4 5 | {
"mercure": {
"publish": []
}
}
|
Так как массив пустой, приложение Symfony будет иметь право на публикацию только общедоступных обновлений (см. раздел авторизация для более детальной информации).
Tip
Веб-сайт jwt.io - это удобный способ создания и подписания токенов JWT. Посмотрите на этот пример JWT, который гарантирует права публикации на все темы (и заметьте звездочку в массиве). Не забудьте правильно установить ваш секретный ключ в нижней части правой панели формы!
Caution
Не помещайте секретный ключ в MERCURE_JWT_TOKEN
, он не будет работать!
Эта переменная окружения должна содержать JWT, подписанный серкетным ключом.
Также, не забудьте, что и секретный ключ, и JWT, должны быть… секретными!
Если вы не хотите использовать предоставленные переменные окружения, используйте следующую конфигурацию:
- YAML
1 2 3 4 5 6 7
# config/packages/mercure.yaml mercure: hubs: default: url: https://mercure-hub.example.com/.well-known/mercure jwt: secret: '!ChangeMe!'
- XML
1 2 3 4 5 6 7 8 9 10
<!-- config/packages/mercure.xml --> <?xml version="1.0" encoding="UTF-8" ?> <config> <hub name="default" url="https://mercure-hub.example.com/.well-known/mercure" > <jwt secret="!ChangeMe!"/> </hub> </config>
- PHP
1 2 3 4 5 6 7 8 9 10 11
// config/packages/mercure.php $container->loadFromExtension('mercure', [ 'hubs' => [ 'default' => [ 'url' => 'https://mercure-hub.example.com/.well-known/mercure', 'jwt' => [ 'secret' => '!ChangeMe!', ], ], ], ]);
Базовое использование¶
Публикация¶
Компонент Mercure предоставляет объект значения Update
, представляющий собой
обновление для публикации. Он также предоставляет сервис Publisher
для запуска
обновлений в хабе.
Сервис Publisher
может быть внедрен, используя автомонтирования
в любой другой сервис, включая контроллеры:
// src/Controller/PublishController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Mercure\Update;
class PublishController extends AbstractController
{
public function publish(HubInterface $hub): Response
{
$update = new Update(
'https://example.com/books/1',
json_encode(['status' => 'OutOfStock'])
);
$hub->publish($update);
return new Response('published!');
}
}
Первым параметром для передачи конструктору Update
, является обновляемая
тема. Эта тема должна быть IRI (Интернационализированный идентификатор ресурса,
RFC 3987): уникальный идентификатор запускаемого ресурса.
Обычно, этот параметр содержит изначальный URL ресурса, переданного клиенту, но он может быть любой строкой или IRI, и не должен быть существующим URL (схоже с пространствами имен XML).
Второй параметр конструктора - содержание обновления. Это может быть что угодно, хранимое в любом формате. Однако, сериализация ресурса в формате гипермедиа, вроде JSON-LD, Atom, HTML или XML является рекомендуемой.
Подписки¶
Подписка на обновления JavaScript в шаблоне Twig является однозначной:
1 2 3 4 5 6 7 | <script>
const eventSource = new EventSource("{{ mercure('https://example.com/books/1')|escape('js') }}");
eventSource.onmessage = event => {
// Будет взыван каждый раз, когда сервер публикует обновление
console.log(JSON.parse(event.data));
}
</script>
|
Функция Twig mercure()
сгенерирует URL хаба Mercure в соответствии с конфигурацией.
URL будет включать в себя параметры запроса topic
, соответствующие темам, переданным
в качестве первого аргумента.
Если вы хотите получить доступ к этому URL из внешнего файла JavaScript, сгенерируйте URL в соответствующему HTML-элементе:
1 2 3 | <script type="application/json" id="mercure-url">
{{ mercure('https://example.com/books/1')|json_encode(constant('JSON_UNESCAPED_SLASHES') b-or constant('JSON_HEX_TAG'))|raw }}
</script>
|
Затем извлеките его из своего файла JS:
1 2 3 | const url = JSON.parse(document.getElementById("mercure-url").textContent);
const eventSource = new EventSource(url);
// ...
|
Mercure также позволяет подписываться на несколько тем, и использовать
Шаблоны URI или специальное значение *
(соответствующее всем темам), в
качестве паттернов:
1 2 3 4 5 6 7 8 9 10 11 12 | <script>
{# Подпишитесь на обновления нескольких источников Книг и на все источники Обзоров, совпадающие с заданным паттерном #}
const eventSource = new EventSource("{{ mercure([
'https://example.com/books/1',
'https://example.com/books/2',
'https://example.com/reviews/{id}'
])|escape('js') }}");
eventSource.onmessage = event => {
console.log(JSON.parse(event.data));
}
</script>
|
Tip
Google Chrome DevTools нативно интегрируют `практичнsq UI`_, отображающий полученные события в реальном времени:

Чтобы использовать его:
- откройте DevTools
- выберите вкладку “Network”
- нажмите на запрос к хабу Mercure
- нажмите на подвкладку “EventStream”.
Tip
Протестируйте, совпадает ли Шаблон URI с URL, используя онлайн-отладчик
Обнаружение¶
Протокол Mercure имеет механизм обнаружения. Для его использования,
приложение Symfony должно показать URL хаба Mercure в HTTP-заголовке
Link
.

Вы можете создать заголовки Link
с помощью компонента WebLink,
используя метод помощника AbstractController::addLink
:
// src/Controller/DiscoverController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Mercure\Discovery;
class DiscoverController extends AbstractController
{
public function __invoke(Request $request, Discovery $discovery): JsonResponse
{
// Link: <http://localhost:3000/.well-known/mercure>; rel="mercure"
$discovery->addLink($request);
return $this->json([
'@id' => '/books/1',
'availability' => 'https://schema.org/InStock',
]);
}
}
Затем, этот заголовок может быть проанализирован с клиентской стороны, чтобы обнаружить URL хаба, и подписаться на него:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // Извлеките изначальный источник, обслуживаемый веб-API Symfony
fetch('/books/1') // Has Link: <http://localhost:3000/.well-known/mercure>; rel="mercure"
.then(response => {
// Извлеките URL хаба из заголовка Link
const hubUrl = response.headers.get('Link').match(/<([^>]+)>;\s+rel=(?:mercure|"[^"]*mercure[^"]*")/)[1];
// Добавьте в начало тему(ы) для подписки в качестве параметра запроса
const hub = new URL(hubUrl);
hub.searchParams.append('topic', 'http://example.com/books/{id}');
// Подпишитесь на обновления
const eventSource = new EventSource(hub);
eventSource.onmessage = event => console.log(event.data);
});
|
Авторизация¶
Mercure также позволяет запускать обновления только для авторизованных
клиентов. Чтобы сделать это, отметьте обновление как приватное, установив
третий параметр конструктора Update
как true
:
// src/Controller/Publish.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mercure\Update;
class PublishController extends AbstractController
{
public function __invoke(HubInterface $hub): Response
{
$update = new Update(
'http://example.com/books/1',
json_encode(['status' => 'OutOfStock']),
true // private
);
// JWT издателя должен содержать эту тему, совпадающий с ней шаблон URI или * в mercure.publish, иначе вы получите ошибку 401
// JWT подписчика должен содержать эту тему, совпадающий с ней шаблон URI или * в mercure.subscribe, чтобы получить обновление
$hub->publish($update);
return new Response('private update published!');
}
}
Для того, чтобы подписаться на приватные обновления, подписчики должны предоставить хабу JWT, содержащий выборщик темы, совпадающий с темой обновления.
Чтобы предоставить этот JWT, подписчик может использовать куки, или HTTP-заголовок
Authorization
.
Куки могут быть установлены Symfony автоматически, путем передачи соответствующих
опций функции Twig mercure()
. Куки, установленные Symfony, будут автоматически
переданы браузерами хабу Mercure, если атрибут withCredentials
класса EventSource
установлен как true
. Затем, Хаб верифицирует валидность предоставленного JWT, и
излечет из него селекторы тем.
1 2 3 4 5 | <script>
const eventSource = new EventSource("{{ mercure('https://example.com/books/1', { subscribe: 'https://example.com/books/1' })|escape('js') }}", {
withCredentials: true
});
</script>
|
Поддерживаемые опции:
subscribe
: список селекторов тем для включения в заявление JWTmercure.subscribe
publish
: список селекторов тем для включения в заявление JWTmercure.publish
additionalClaims
: дополнительные заявления для включения в JWT (дата истечения срока годности, ID токена…)
Использование куки - наиболее защищенный и предпочитаемый путь, когда клиент является веб-браузером. Если клиент - не веб-браузер, лучше пойти путем использования заголовка авторизации.
Caution
Чтобы использовать метод аутентификации куки, приложение Symfony и Хаб должны быть поданы с одного домена (могут быть разные под-домены).
Tip
Нативная реализация EventSource не позволяет указывать заголовки. Например, авторизацию, использующую токен Bearer. Чтобы достичь этого, используйте полизаполнение
1 2 3 4 5 6 7 | <script>
const es = new EventSourcePolyfill("{{ mercure('https://example.com/books/1') }}", {
headers: {
'Authorization': 'Bearer ' + token,
}
});
</script>
|
Программная установка куки¶
Иногда, может быть удобно установить куки авторизации из вашего кода, а не использовать
функцию Twig. MercureBundle предоставляет удобный сервис,
Authorization
, чтобы сделать это.
В следующем примере контроллера, добавленный куки содержит JWT, который сам содержит соответствующий селектор темы.
А вот и контроллер:
// src/Controller/DiscoverController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Mercure\Authorization;
use Symfony\Component\Mercure\Discovery;
class DiscoverController extends AbstractController
{
public function publish(Request $request, Discovery $discovery, Authorization $authorization): JsonResponse
{
$discovery->addLink($request);
$authorization->setCookie($request, ['https://example.com/books/1']);
return $this->json([
'@id' => '/demo/books/1',
'availability' => 'https://schema.org/InStock'
]);
}
}
Программное генерирование JWT, используемых для публикации¶
Вместо хранения JWT напрямую в конфигурации, вы можете создать
поставщик токенов, который будет возвращать токен, используемый
объектом HubInterface
:
// src/Mercure/MyTokenProvider.php
namespace App\Mercure;
use Symfony\Component\Mercure\JWT\TokenProviderInterface;
final class MyTokenProvider implements TokenProviderInterface
{
public function getToken(): string
{
return 'the-JWT';
}
}
Затем, сошлитесь на этот сервис в конфигурации пакета:
- YAML
1 2 3 4 5 6 7
# config/packages/mercure.yaml mercure: hubs: default: url: https://mercure-hub.example.com/.well-known/mercure jwt: provider: App\Mercure\MyTokenProvider
- XML
1 2 3 4 5 6 7 8 9 10
<!-- config/packages/mercure.xml --> <?xml version="1.0" encoding="UTF-8" ?> <config> <hub name="default" url="https://mercure-hub.example.com/.well-known/mercure" > <jwt provider="App\Mercure\MyTokenProvider"/> </hub> </config>
- PHP
1 2 3 4 5 6 7 8 9 10 11 12 13
// config/packages/mercure.php use App\Mercure\MyJwtProvider; $container->loadFromExtension('mercure', [ 'hubs' => [ 'default' => [ 'url' => 'https://mercure-hub.example.com/.well-known/mercure', 'jwt' => [ 'provider' => MyJwtProvider::class, ] ], ], ]);
Этот метод особенно удобен при использовании токенов, которые имеют срок окончания действия, который может быть программно обновлен.
Веб-API¶
При создании веб-API, удобно, когда есть возможность незамедлительно отправлять новые версии ресурсов всем подсоединенным устройствам, и обновлять их просмотр.
Платформа API может использовать компонент Mercure для автоматического запуска обновлений, каждый раз при создании, изменении или удалении API-ресурса.
Начните с установки библиотеки, используя ее официальный рецепт:
1 | $ composer require api
|
Затем, создания следующей сущности достаточно для того, чтобы получить полноценный гипермедиа API и автоматическую трансляцию обновлений через хаб 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 string $name = '';
#[ORM\Column]
public string $status = '';
}
Как показано в этой записи, генератор клиентов платформы API также позволяет автоматически генерировать код полных приложений React и React Native из этого API. Эти приложения будут отображать содержания обновлений Mercure в режиме реального времени.
Прочтите документацию платформы API, чтобы узнать больше о ее поддержке Mercure.
Тестирование¶
Во время модульного тестирования обновления Mercure отправлять не надо.
Вместо этого вы можете использовать MockHub:
// tests/FunctionalTest.php
namespace App\Tests\Unit\Controller;
use App\Controller\MessageController;
use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Mercure\JWT\StaticTokenProvider;
use Symfony\Component\Mercure\MockHub;
use Symfony\Component\Mercure\Update;
class MessageControllerTest extends TestCase
{
public function testPublishing()
{
$hub = new MockHub('https://internal/.well-known/mercure', new StaticTokenProvider('foo'), function(Update $update): string {
// $this->assertTrue($update->isPrivate());
return 'id';
});
$controller = new MessageController($hub);
// ...
}
}
Во время функционального тестирования вы можете декорировать хаб:
// tests/Functional/Fixtures/HubStub.php
namespace App\Tests\Functional\Fixtures;
use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Mercure\Update;
class HubStub implements HubInterface
{
public function publish(Update $update): string
{
return 'id';
}
// реализуйте остальные методы HubInterface здесь
}
HubStub декорирует сервис хаба по умолчанию, поэтому никакие обновления на самом деле не отправляются. Вот реализация HubStub:
1 2 3 | # config/services_test.yaml
App\Tests\Functional\Fixtures\HubStub:
decorates: mercure.hub.default
|
Tip
Symfony Panther имеет функцию для тестирования приложений с использованием Mercure.
Отладка¶
New in version 0.2: Панель WebProfiler была представлена в MercureBundle 0.2.
Подключите панель в вашей конфигурации следующим образом:
MercureBundle поставляется с панелью отладки. Установите пакет Debug, чтобы подключить ее:
.. code-block:: terminal
$ composer require –dev symfony/debug-pack

Асинхронный запуск¶
Tip
Асинхронный запуск не поощряется. Большинство хабов Mercure уже обрабатывают публикации асинхронно, и использование Messenger обычно не требуется.
Вместо вызова сервиса Publisher
напрямую, вы можете также позволить Symfony
запускать обновления асинхронно, благодаря предоставленной интеграции с компонентом
Messenger.
Для начала, убедитесь, что установили компонент Messenger и правильно сконфигурировали транспорт (если вы этого не сделаете, обработчик будет вызван асинхронно).
Затем, запустите Mercure Update
в автобуме сообщений Messenger, он
будет обработан автоматически:
// src/Controller/PublishController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mercure\Update;
use Symfony\Component\Messenger\MessageBusInterface;
class PublishController extends AbstractController
{
public function publish(MessageBusInterface $bus): Response
{
$update = new Update(
'https://example.com/books/1',
json_encode(['status' => 'OutOfStock'])
);
// Синхронно или асинхронно (Doctrine, RabbitMQ, Kafka...)
$bus->dispatch($update);
return new Response('published!');
}
}
Двигаемся дальше¶
- Протокол Mercure также поддерживается компонентом Notifier. Используйте его для отправки пуш-уведомлений веб-браузерам.
- Symfony UX Turbo - это библиотека, использующая Mercure для предоставления такого же опыта, как и с одностраничными приложениями, но без написания единой строчки JavaScript!
Эта документация является переводом официальной документации Symfony и предоставляется по свободной лицензии CC BY-SA 3.0.