hibira-io / symfony-bundle
Hibira realtime for Symfony: UX Turbo Streams and Messenger without running a Mercure hub, plus channel auth, webhooks and push
Package info
Type:symfony-bundle
pkg:composer/hibira-io/symfony-bundle
Requires
- php: >=8.1
- pusher/pusher-php-server: ^7.2
- symfony/config: ^6.4|^7.0|^8.0
- symfony/console: ^6.4|^7.0|^8.0
- symfony/dependency-injection: ^6.4|^7.0|^8.0
- symfony/event-dispatcher: ^6.4|^7.0|^8.0
- symfony/framework-bundle: ^6.4|^7.0|^8.0
- symfony/http-foundation: ^6.4|^7.0|^8.0
- symfony/http-kernel: ^6.4|^7.0|^8.0
- symfony/routing: ^6.4|^7.0|^8.0
Requires (Dev)
- phpunit/phpunit: ^10.5|^11.0|^12.0
- pusher/pusher-push-notifications: ^2.0
- symfony/expression-language: ^6.4|^7.0|^8.0
- symfony/messenger: ^6.4|^7.0|^8.0
- symfony/property-access: ^6.4|^7.0|^8.0
- symfony/security-bundle: ^6.4|^7.0|^8.0
- symfony/twig-bundle: ^6.4|^7.0|^8.0
- symfony/ux-turbo: ^2.20|^3.0
- symfony/yaml: ^6.4|^7.0|^8.0
Suggests
- pusher/pusher-push-notifications: iOS, Android and browser push notifications with Hibira Push (Beams-compatible)
- symfony/messenger: Send events from a worker: route Hibira\SymfonyBundle\Message\PublishEvent to an async transport
- symfony/security-bundle: Sign users in for private and presence channels and push user tokens
- symfony/ux-turbo: Broadcast Turbo Streams (#[Broadcast] entities, turbo_stream_listen) over Hibira instead of a Mercure hub
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-01 16:42:42 UTC
README
Realtime Symfony apps on Hibira, without running a
Mercure hub. Symfony UX Turbo Streams, Doctrine #[Broadcast] entities and
Symfony Messenger send their updates to Hibira, and the browser receives
them over WebSockets.
| Desktop | Phone |
|---|---|
![]() |
![]() |
What the bundle gives you:
- UX Turbo transport.
#[Broadcast]entities,turbo_stream_listen()andturbo_stream_from()/<twig:Turbo:Stream:From>work as they do with Mercure, with the transport namedhibira. - Messenger. Every event is a
PublishEventmessage. Route it to an async transport and a worker sends it, so a slow network never slows the request that saved the entity. - Signed channels. Each Turbo topic is a private channel that a browser can only join with the signature the page rendered for it.
- Size limit handling. A stream over your plan's event size limit becomes a Turbo page refresh instead of an error.
- Channel auth, webhooks and push.
POST /hibira/authfor your own private and presence channels, a verified webhook route that dispatches Symfony events, and push user tokens for iOS, Android and the web. bin/console hibira:checkto check the credentials and connection.
Supports Symfony 6.4, 7.x and 8.x (PHP 8.1+), UX Turbo 2 and 3.
Install
composer require hibira-io/symfony-bundle
Symfony Flex enables the bundle, adds the Stimulus controller to
assets/controllers.json and pusher-js to your import map (or
package.json with Webpack Encore).
Copy the app ID, key and secret from your app page on
hibira.io/portal into .env.local (the Free plan
needs no card). The secret is shown once, when you create the app or rotate
it.
HIBIRA_APP_ID=your-app-id HIBIRA_APP_KEY=your-app-key HIBIRA_APP_SECRET=your-app-secret
Import the routes:
# config/routes/hibira.yaml hibira: resource: '@HibiraSymfonyBundle/config/routes.php' prefix: /hibira
Then check the setup:
php bin/console hibira:check
On your app page, add your site's origin (for example
https://shop.example.com) to Allowed origins, or browsers can't
connect.
Configuration
Everything is optional. These are the defaults:
# config/packages/hibira.yaml hibira: app_id: '%env(default::HIBIRA_APP_ID)%' key: '%env(default::HIBIRA_APP_KEY)%' secret: '%env(default::HIBIRA_APP_SECRET)%' host: ws.hibira.io # the WebSocket host on your app page port: 443 scheme: https turbo: enabled: auto # on when symfony/ux-turbo is installed oversize: refresh # or "throw" messenger: enabled: auto # on when symfony/messenger is installed bus: messenger.default_bus push: instance_id: '%env(default::HIBIRA_PUSH_INSTANCE_ID)%' secret_key: '%env(default::HIBIRA_PUSH_SECRET_KEY)%' endpoint: https://ws.hibira.io
host, port and scheme take env placeholders too, such as
'%env(int:HIBIRA_PORT)%'.
Turbo Streams
Mark an entity with #[Broadcast] and give it a stream template, exactly as
in the UX Turbo docs:
use Symfony\UX\Turbo\Attribute\Broadcast; #[ORM\Entity] #[Broadcast] class Message { // ... }
{# templates/broadcast/Message.stream.html.twig #} {% block create %} <turbo-stream action="append" target="messages"> <template>{{ include('message/_message.html.twig', { message: entity }) }}</template> </turbo-stream> {% endblock %} {% block remove %} <turbo-stream action="remove" target="message_{{ id }}"></turbo-stream> {% endblock %}
Listen in a page. A class name follows every entity of that class, an entity follows that one entity, and any other string is a topic of your own:
{# UX Turbo 2 and 3 #} <ul id="messages" {{ turbo_stream_listen('App\\Entity\\Message') }}> {# UX Turbo 3.1+ #} {{ turbo_stream_from(message) }} <twig:Turbo:Stream:From topics="chat" />
Without Mercure installed, hibira becomes UX Turbo's default transport.
With both, pass it by name: turbo_stream_listen(topic, 'hibira'), or set
turbo.default_transport: hibira. #[Broadcast(transports: ['hibira'])]
limits an entity to Hibira.
Send a stream from anywhere, for example a Messenger handler reporting progress:
public function __construct(private Hibira $hibira) {} $this->hibira->turboStream('import-42', '<turbo-stream action="update" target="progress"><template>80%</template></turbo-stream>');
Each topic becomes the private channel private-turbo-<sha256(topic)>, and
the page carries an HMAC of that name, so a browser can't subscribe to a
topic its page didn't render. Free plans take events up to 4 KB, Hobby and
Starter 10 KB. A stream over the limit is replaced by
<turbo-stream action="refresh">, which reloads the page in place (set
turbo.oversize: throw to get a PayloadTooLargeException instead).
Messenger
Hibira::publish() and every Turbo broadcast dispatch
Hibira\SymfonyBundle\Message\PublishEvent. With no routing it is handled
during the request. Route it to a transport to send events from a worker:
# config/packages/messenger.yaml framework: messenger: transports: async: '%env(MESSENGER_TRANSPORT_DSN)%' routing: Hibira\SymfonyBundle\Message\PublishEvent: async
php bin/console messenger:consume async
Hibira::trigger() always sends right away.
Your own channels
use Hibira\SymfonyBundle\Hibira; $hibira->publish('private-orders.42', 'shipped', ['id' => 42]);
Private and presence channels are authorized by POST /hibira/auth, which
asks your listeners. Nobody gets in until a listener calls allow():
use Hibira\SymfonyBundle\Event\ChannelAuthorizationEvent; use Symfony\Component\EventDispatcher\Attribute\AsEventListener; #[AsEventListener] final class AuthorizeChannels { public function __invoke(ChannelAuthorizationEvent $event): void { if ($event->channel === 'private-orders.'.$event->user?->getId()) { $event->allow(); } if ($event->channel === 'presence-team' && $event->user !== null) { // Member ID defaults to the user identifier. $event->allow(memberInfo: ['name' => $event->user->getName()]); } } }
In the browser, add {{ hibira_meta_tags() }} to your layout's <head>
and use the shared client:
import { hibira } from "@hibira-io/symfony-bundle/hibira.js" hibira().subscribe("private-orders.42").bind("shipped", (order) => console.log(order))
With AssetMapper, add the module to importmap.php once:
'@hibira-io/symfony-bundle/hibira.js' => ['path' => '@hibira-io/symfony-bundle/hibira.js'].
Webhooks
Point your app's webhook at https://your-site/hibira/webhook. The bundle
checks X-Pusher-Signature (and, during a secret rotation,
X-Hibira-Previous-Signature) and dispatches one WebhookReceivedEvent per
event:
#[AsEventListener] public function onWebhook(WebhookReceivedEvent $event): void { if ($event->name === 'member_removed') { // $event->channel, $event->payload['user_id'] } }
Push notifications
composer require pusher/pusher-push-notifications
HIBIRA_PUSH_INSTANCE_ID=your-instance-id HIBIRA_PUSH_SECRET_KEY=your-secret-key
$hibira->push()->publishToUsers(['ada'], [ 'web' => ['notification' => ['title' => 'Order shipped', 'deep_link' => 'https://shop.example.com/orders/42']], ]);
GET /hibira/push/token?user_id=... returns a user token for the signed-in
user, for setUserId() in the web, Android and iOS SDKs.
Coming from Mercure
| Mercure | Hibira |
|---|---|
symfony/mercure-bundle and a hub to run |
hibira-io/symfony-bundle, hosted |
MERCURE_URL, MERCURE_PUBLIC_URL, MERCURE_JWT_SECRET |
HIBIRA_APP_ID, HIBIRA_APP_KEY, HIBIRA_APP_SECRET |
turbo_stream_listen(topic) |
unchanged (transport hibira) |
#[Broadcast] |
unchanged |
HubInterface::publish(new Update(...)) |
Hibira::turboStream() or Hibira::publish() |
Update routed through Messenger |
PublishEvent routed through Messenger |
| Private updates with JWT cookies | Signed private channels, ChannelAuthorizationEvent |
Example
example/ is a live board on FrankenPHP: Doctrine
#[Broadcast] → Messenger worker → Hibira → Turbo Streams. Put your
credentials in example/.env.local, then:
docker compose -f example/compose.yaml up --build
and open http://localhost:8000 in two windows.
Development
composer install vendor/bin/phpunit
中文摘要
hibira-io/symfony-bundle 讓 Symfony 專案不用自己架 Mercure hub,就能用
Hibira 推送即時更新:
- UX Turbo:
#[Broadcast]實體、turbo_stream_listen()、turbo_stream_from()照原本寫法使用,傳輸名稱為hibira。每個 topic 都是帶簽章的私有頻道。超過方案單一事件大小上限(Free 4 KB)時,自動改送 Turbo 頁面重新整理。 - Messenger:所有事件都是
PublishEvent訊息,路由到 async transport 就會改由 worker 發送。 - 頻道授權、Webhook、推播:
POST /hibira/auth(以ChannelAuthorizationEvent決定)、驗證簽章的 webhook 路由(派發WebhookReceivedEvent)、推播 user token。 - 安裝:
composer require hibira-io/symfony-bundle,在.env.local填入HIBIRA_APP_ID/HIBIRA_APP_KEY/HIBIRA_APP_SECRET,匯入路由後執行php bin/console hibira:check。記得在應用頁面的 Allowed origins 加入網站網域。 - 支援 Symfony 6.4、7.x、8.x(PHP 8.1+),UX Turbo 2 與 3。
example/是跑在 FrankenPHP 上的即時留言板範例。

