josbeir / cakephp-mercure
Mercure plugin for CakePHP
Installs: 19
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
Type:cakephp-plugin
pkg:composer/josbeir/cakephp-mercure
Requires
- php: >=8.2
- cakephp/cakephp: ^5.0.1
- firebase/php-jwt: ^6.11
Requires (Dev)
- cakephp/cakephp-codesniffer: ^5.2
- cakephp/plugin-installer: ^2.0.1
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.1.3 || ^12.0
- rector/rector: ^2.1
README
CakePHP Mercure Plugin
Push real-time updates to clients using the Mercure protocol.
Table of Contents
- Overview
- Installation
- Configuration
- Basic Usage
- Authorization
- Mercure Discovery
- Advanced Configuration
- Testing
- API Reference
- Contributing
- License
Overview
This plugin provides integration between CakePHP applications and the Mercure protocol, enabling real-time push capabilities for modern web applications.
Mercure is an open protocol built on top of Server-Sent Events (SSE) that allows you to:
- Push updates from your server to clients in real-time
- Create live-updating UIs without complex WebSocket infrastructure
- Broadcast data changes to multiple connected users
- Handle authorization for private updates
- Automatically reconnect with missed update retrieval
Common use cases include live dashboards, collaborative editing, real-time notifications, and chat applications.
Installation
Installing the Plugin
Important
Minimum Requirements:
- PHP 8.2 or higher
- CakePHP 5.0.1 or higher
Install the plugin using Composer:
composer require josbeir/cakephp-mercure
Load the plugin in your Application.php:
// src/Application.php public function bootstrap(): void { parent::bootstrap(); $this->addPlugin('Mercure'); }
Alternatively, you can add it to config/plugins.php:
// config/plugins.php return [ 'Mercure' => [], ];
Running a Mercure Hub
Mercure requires a hub server to manage persistent SSE connections. Download the official hub from Mercure.rocks.
For development, you can run the hub using Docker:
docker run -d \
-e SERVER_NAME=:3000 \
-e MERCURE_PUBLISHER_JWT_KEY='!ChangeThisMercureHubJWTSecretKey!' \
-e MERCURE_SUBSCRIBER_JWT_KEY='!ChangeThisMercureHubJWTSecretKey!' \
-p 3000:3000 \
dunglas/mercure
If you're using DDEV, you can install the Mercure add-on:
ddev get Rindula/ddev-mercure
For more information, see the DDEV Mercure add-on.
The hub will be available at http://localhost:3000/.well-known/mercure.
Tip
Using FrankenPHP? You're good to go! FrankenPHP has Mercure built in—no separate hub needed. See the FrankenPHP Mercure documentation for details.
Configuration
The plugin comes with sensible defaults and multiple configuration options.
Quick Setup (Environment Variables):
For development, the fastest way to get started is using environment variables in your .env file:
MERCURE_URL=http://localhost:3000/.well-known/mercure MERCURE_PUBLIC_URL=http://localhost:3000/.well-known/mercure MERCURE_JWT_SECRET=!ChangeThisMercureHubJWTSecretKey!
Configuration Files:
The plugin loads configuration in this order:
- Plugin defaults -
vendor/josbeir/cakephp-mercure/config/mercure.php(loaded automatically) - Your overrides -
config/app_mercure.php(optional, loaded after plugin defaults)
Create config/app_mercure.php in your project to customize any settings. Your values will override the plugin defaults.
Cross-Subdomain Setup:
Note
If your Mercure hub runs on a different subdomain than your CakePHP application (e.g., hub.example.com vs app.example.com), you must configure the cookie domain:
# Allow cookie sharing across subdomains MERCURE_COOKIE_DOMAIN=.example.com
This enables the authorization cookie to be accessible by both your application and the Mercure hub. Without this setting, authorization will fail for cross-subdomain requests.
For a complete list of available environment variables, see the plugin's config/mercure.php file.
Basic Usage
The plugin provides multiple integration points depending on your use case:
- Controllers: Use the
MercureComponentto centrally manage both authorization and subscriptions as topics - Templates: Use the
MercureHelperto generate Mercure topic URLs for EventSource subscriptions in your views and templates. - Services & Manual Control: Use the
Publisherfacade to publish updates and theAuthorizationfacade for direct response manipulation when you need lower-level control (e.g., outside controllers/views, such as in background jobs or custom middleware).
Tip
Note: Facades (Publisher, Authorization) can be used in any context where a CakePHP component or helper does not fit, such as in queue jobs, commands, models, or other non-HTTP or background processing code. This makes them ideal for use outside of controllers and views.
Choosing Your Authorization Strategy
Pick the approach that best fits your workflow:
| Scenario | Recommended Approach | Method to Use |
|---|---|---|
| Authorize in controller, display URL in template | MercureComponent + MercureHelper |
$this->Mercure->authorize() in controller, $this->Mercure->url($topics) in template |
| Public topics (no authorization) | MercureHelper |
$this->Mercure->url($topics) |
| Manual response control | Authorization facade |
Authorization::setCookie($response, $subscribe) |
Publishing Updates
Use the Publisher facade to send updates to the Mercure hub:
use Mercure\Publisher; use Mercure\Update\Update; // In a controller or service $update = new Update( topics: 'https://example.com/books/1', data: json_encode(['status' => 'OutOfStock']) ); Publisher::publish($update);
The topics parameter identifies the resource being updated. It should be a unique IRI (Internationalized Resource Identifier), typically the resource's URL.
You can publish to multiple topics simultaneously:
$update = new Update( topics: [ 'https://example.com/books/1', 'https://example.com/notifications', ], data: json_encode(['message' => 'Book status changed']) ); Publisher::publish($update);
Tip
Using MercureComponent in Controllers: If you're publishing from a controller, the MercureComponent provides convenient methods that eliminate the need to manually create Update objects or call the Publisher facade:
// In your controller public function initialize(): void { parent::initialize(); $this->loadComponent('Mercure.Mercure'); } public function update($id) { $book = $this->Books->get($id); $book = $this->Books->patchEntity($book, $this->request->getData()); $this->Books->save($book); // Publish JSON directly - no need for Publisher facade $this->Mercure->publishJson( topics: "/books/{$id}", data: ['status' => $book->status, 'title' => $book->title] ); // Or publish a rendered element $this->Mercure->publishView( topics: "/books/{$id}", element: 'Books/item', data: ['book' => $book] ); }
See the MercureComponent API Reference for all available methods.
Publishing JSON Data
For convenience when publishing JSON data, use the JsonUpdate class which automatically encodes arrays and objects to JSON:
use Mercure\Publisher; use Mercure\Update\JsonUpdate; // Simple array - no need to call json_encode() $update = JsonUpdate::create( topics: 'https://example.com/books/1', data: ['status' => 'OutOfStock', 'quantity' => 0] ); Publisher::publish($update); // Or use the fluent builder pattern $update = (new JsonUpdate('https://example.com/books/1')) ->data(['status' => 'OutOfStock', 'quantity' => 0]) ->build(); Publisher::publish($update);
You can customize JSON encoding options:
// With custom JSON encoding options $update = JsonUpdate::create( topics: 'https://example.com/books/1', data: ['title' => 'Book & Title', 'price' => 19.99], jsonOptions: JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); // Or using the fluent builder $update = (new JsonUpdate('https://example.com/books/1')) ->data(['title' => 'Book & Title', 'price' => 19.99]) ->jsonOptions(JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ->build(); Publisher::publish($update);
For private updates and event metadata:
$update = JsonUpdate::create( topics: 'https://example.com/users/123/notifications', data: ['message' => 'New notification', 'unread' => 5], private: true ); // Or chain multiple options with the fluent builder $update = (new JsonUpdate('https://example.com/books/1')) ->data(['title' => 'New Book', 'price' => 29.99]) ->private() ->id(Text::uuid()) ->type('book.created') ->retry(5000) ->build(); Publisher::publish($update);
Publishing Rendered Views
Use the ViewUpdate class to automatically render CakePHP views or elements and publish the rendered HTML.
Note
This is especially handy when using JavaScript frameworks like htmx (for instance, using the htmx-sse extension), Hotwire (with Turbo Streams), or similar reactive libraries, which can consume and swap HTML fragments received over Mercure for seamless real-time UI updates.
use Mercure\Publisher; use Mercure\Update\ViewUpdate; // Render an element $update = ViewUpdate::create( topics: 'https://example.com/books/1', element: 'Books/item', viewVars: ['book' => $book] ); // Or use the fluent builder pattern $update = (new ViewUpdate('https://example.com/books/1')) ->element('Books/item') ->viewVars(['book' => $book]) ->build(); Publisher::publish($update);
You can also render full templates:
// Render a template $update = ViewUpdate::create( topics: 'https://example.com/notifications', template: 'Notifications/item', viewVars: ['notification' => $notification] ); // Or with the fluent builder - add view options too $update = (new ViewUpdate('https://example.com/notifications')) ->template('Notifications/item') ->viewVars(['notification' => $notification]) ->viewOptions(['key' => 'value']) ->build(); Publisher::publish($update);
For private updates with event metadata:
$update = ViewUpdate::create( topics: 'https://example.com/users/123/messages', element: 'Messages/item', viewVars: ['message' => $message], private: true ); // Or chain all options with the fluent builder $update = (new ViewUpdate('https://example.com/users/123/messages')) ->element('Messages/item') ->viewVars(['message' => $message]) ->private() ->id('msg-456') ->type('message.new') ->build(); Publisher::publish($update);
Subscribing to Updates
The plugin provides a View Helper to generate Mercure URLs in your templates.
First, load the helper in AppView:
// In src/View/AppView.php public function initialize(): void { parent::initialize(); $this->loadHelper('Mercure.Mercure'); }
Then subscribe to updates from your templates:
// In your template <div id="book-status">Available</div> <script> // For public topics (no authorization needed) const eventSource = new EventSource('<?= $this->Mercure->url(['https://example.com/books/1']) ?>'); eventSource.onmessage = (event) => { const data = JSON.parse(event.data); document.getElementById('book-status').textContent = data.status; }; </script>
Subscribe to multiple topics:
<script> // Subscribe to multiple topics const url = '<?= $this->Mercure->url([ 'https://example.com/books/1', 'https://example.com/notifications' ]) ?>'; const eventSource = new EventSource(url); eventSource.onmessage = (event) => { console.log('Update received:', event.data); }; </script>
If you need to access the Mercure URL from an external JavaScript file, store it in a data element:
<script type="application/json" id="mercure-url"> <?= json_encode( $this->Mercure->url(['https://example.com/books/1']), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG ) ?> </script>
Then retrieve it from your JavaScript:
const url = JSON.parse(document.getElementById('mercure-url').textContent); const eventSource = new EventSource(url); eventSource.onmessage = (event) => { console.log('Update received:', event.data); };
Authorization
Publishing Private Updates
Mark updates as private to restrict access to authorized subscribers:
$update = new Update( topics: 'https://example.com/users/123/messages', data: json_encode(['text' => 'Private message']), private: true ); Publisher::publish($update);
Private updates are only delivered to subscribers with valid JWT tokens containing matching topic selectors.
Setting Authorization Cookies
Using the Component
For centralized authorization logic, use the MercureComponent in controllers. Topics added via the component are automatically available in your views:
class BooksController extends AppController { public function initialize(): void { parent::initialize(); $this->loadComponent('Mercure.Mercure'); } public function view($id) { $book = $this->Books->get($id); $userId = $this->request->getAttribute('identity')->id; // Using builder pattern $this->Mercure ->addTopic('https://example.com/books/123') // You can also set this using MercureHelper or using the defaultTopics option. ->addSubscribe("https://example.com/books/{$id}") ->addSubscribe("https://example.com/notifications/{$id}") ->authorize() // This sets the actual JWT cookie. // Or direct authorization $this->Mercure->authorize( subscribe: ["https://example.com/books/{$id}"], additionalClaims: ['sub' => $userId] // Optional ); $this->set('book', $book); } public function logout() { // Clear authorization on logout $this->Mercure->clearAuthorization(); // Removes the JWT cookie. return $this->redirect(['action' => 'login']); } }
The component provides separation of concerns (authorization in controller, URLs in template). You can also enable automatic discovery headers:
// In AppController $this->loadComponent('Mercure.Mercure', [ 'autoDiscover' => true, // Automatically add discovery headers ]);
Setting Default Topics
You can configure default topics that will be automatically merged with any topics you provide to url(). This is useful when you want certain topics (like notifications or global alerts) to be included in every subscription:
// In your `AppView` using the helper public function initialize(): void { parent::initialize(); // Load helper with default topics $this->loadHelper('Mercure', [ 'defaultTopics' => [ 'https://example.com/notifications', 'https://example.com/alerts' ] ]); }
You can also set default topics using the MercureComponent in your controller:
// In your controller using the component public function initialize(): void { parent::initialize(); $this->loadComponent('Mercure.Mercure', [ 'defaultTopics' => [ 'https://example.com/notifications', 'https://example.com/alerts' ] ]); }
Now every call to MercureHelper::url() will automatically include these default topics:
// In your template <script> // This will subscribe to: /notifications, /alerts, AND /books/123 const url = '<?= $this->Mercure->url(['/books/123']) ?>'; const eventSource = new EventSource(url, { withCredentials: true }); </script> // You can also add topics dynamically: $this->Mercure->addTopic('/user/' . $userId . '/messages'); $this->Mercure->addTopics(['/books/456', '/comments/789']); // These will be merged with configured defaults const url = '<?= $this->Mercure->url(['/books/123']) ?>'; // Result includes: /notifications, /alerts, /user/{id}/messages, /books/456, /comments/789, AND /books/123
Using the Facade classes
For more control or when not using controllers, you can use the Authorization facade directly:
use Mercure\Authorization; public function view($id) { $book = $this->Books->get($id); // Allow this user to subscribe to updates for this book $response = Authorization::setCookie( $this->response, subscribe: ["https://example.com/books/{$id}"] ); $this->set('book', $book); return $response; }
The cookie must be set before establishing the EventSource connection. The Mercure hub and your CakePHP application should share the same domain (different subdomains are allowed).
Mercure Discovery
The Mercure protocol supports automatic hub discovery via HTTP Link headers. This allows clients to discover the hub URL without hardcoding it, making your application more flexible and following the Mercure specification.
Using the Component
Add the discovery header from your controller using the MercureComponent:
// In your controller action $this->Mercure->discover();
This adds a Link header to the response:
Link: <https://mercure.example.com/.well-known/mercure>; rel="mercure"
Clients can then discover the hub URL from the response headers:
fetch('/api/resource') .then(response => { const linkHeader = response.headers.get('Link'); // Parse the Link header to extract the Mercure hub URL const match = linkHeader.match(/<([^>]+)>;\s*rel="mercure"/); if (match) { const hubUrl = match[1]; const eventSource = new EventSource(hubUrl + '?topic=/api/resource'); } });
Using Middleware
This is an alternative approach to add the discovery header automatically to all responses by using middleware:
// In src/Application.php use Mercure\Http\Middleware\MercureDiscoveryMiddleware; public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue { $middlewareQueue // ... other middleware ->add(new MercureDiscoveryMiddleware()); return $middlewareQueue; }
The middleware automatically adds the Link header with rel="mercure" to all responses, making the hub URL discoverable by any client.
Tip
The discovery header uses the public_url configuration (or falls back to url if not set), ensuring clients always receive the correct publicly-accessible hub URL.
Advanced Configuration
JWT Token Strategies
The plugin supports multiple JWT generation strategies:
1. Secret-based (default):
'jwt' => [ 'secret' => env('MERCURE_JWT_SECRET'), 'algorithm' => 'HS256', 'publish' => ['*'], 'subscribe' => ['*'], ]
2. Static token:
'jwt' => [ 'value' => env('MERCURE_JWT_TOKEN'), ]
3. Custom provider:
'jwt' => [ 'provider' => \App\Mercure\CustomTokenProvider::class, ]
Implement Mercure\Jwt\TokenProviderInterface:
namespace App\Mercure; use Mercure\Jwt\TokenProviderInterface; class CustomTokenProvider implements TokenProviderInterface { public function getJwt(): string { // Generate and return JWT token return $this->generateToken(); } }
4. Custom factory:
'jwt' => [ 'factory' => \App\Mercure\CustomTokenFactory::class, 'secret' => env('MERCURE_JWT_SECRET'), 'publish' => ['*'], ]
Implement Mercure\Jwt\TokenFactoryInterface:
namespace App\Mercure; use Mercure\Jwt\TokenFactoryInterface; class CustomTokenFactory implements TokenFactoryInterface { public function __construct( private string $secret, private string $algorithm ) {} public function create(array $subscribe = [], array $publish = [], array $additionalClaims = []): string { // Create and return JWT token } }
HTTP Client Options
Configure the HTTP client used to communicate with the Mercure hub:
'http_client' => [ 'timeout' => 30, 'ssl_verify_peer' => false, // For local development only ]
Cookie Configuration
The authorization cookie contains a JWT token that authenticates subscribers to private topics. JWT expiry is automatically calculated based on cookie lifetime settings.
'cookie' => [ 'name' => 'mercureAuthorization', // Lifetime in seconds (0 for session cookie) 'lifetime' => 3600, // 1 hour // Or use explicit expiry datetime // 'expires' => '+1 hour', // Omit both to use PHP's session.cookie_lifetime setting 'domain' => '.example.com', 'path' => '/', 'secure' => true, // HTTPS only (recommended) 'httponly' => true, // Prevents XSS token theft 'samesite' => 'strict', // CSRF protection ]
JWT Expiry Management:
The plugin automatically sets the JWT exp claim based on cookie lifetime, following this priority:
additionalClaims['exp']- Per-request overridecookie.expires- Explicit datetime ('+1 hour', etc.)cookie.lifetime- Seconds (3600for 1 hour,0for session)ini_get('session.cookie_lifetime')- PHP session setting- Default: +1 hour
Session cookies (lifetime: 0) automatically get a 1-hour JWT expiry for security.
Security Notes:
httponly: true(default) prevents JavaScript access while still allowing EventSource connectionssamesite: 'strict'(default) provides CSRF protectionsecure: truerequires HTTPS (recommended for production)- JWT tokens always expire - no infinite authorization
For more details, see the CakePHP Cookie documentation.
Testing
For testing, mock the Publisher service to avoid actual HTTP calls:
use Mercure\Publisher; use Mercure\Service\PublisherInterface; use Mercure\TestSuite\MockPublisher; // In your test public function testPublishing(): void { // Se the mock publisher Publisher::setInstance(new MockPublisher()); // Test your code that publishes updates $this->MyService->doSomething(); // Clean up Publisher::clear(); }
Similarly for Authorization:
use Mercure\Authorization; use Mercure\Service\AuthorizationInterface; public function testAuthorization(): void { $mockAuth = $this->createMock(AuthorizationInterface::class); Authorization::setInstance($mockAuth); // Your tests here Authorization::clear(); }
API Reference
Publisher
| Method | Returns | Description |
|---|---|---|
publish(Update $update) |
bool |
Publish an update to the hub |
setInstance(PublisherInterface $publisher) |
void |
Set custom instance (for testing) |
clear() |
void |
Clear singleton instance |
MercureComponent
Controller component for centralized authorization with separation of concerns and automatic dependency injection.
Loading the Component:
public function initialize(): void { parent::initialize(); $this->loadComponent('Mercure.Mercure', [ 'autoDiscover' => true, // Optional: auto-add discovery headers 'defaultTopics' => [ // Optional: topics available in all views '/notifications', '/global/alerts' ] ]); }
Methods:
| Method | Returns | Description |
|---|---|---|
addTopic(string $topic) |
$this |
Add a topic for the view to subscribe to |
addTopics(array $topics) |
$this |
Add multiple topics for the view |
getTopics() |
array |
Get all topics added in the component |
resetTopics() |
$this |
Reset all accumulated topics |
addSubscribe(string $topic, array $additionalClaims = []) |
$this |
Add a topic to authorize with optional JWT claims |
addSubscribes(array $topics, array $additionalClaims = []) |
$this |
Add multiple topics to authorize with optional JWT claims |
getSubscribe() |
array |
Get accumulated subscribe topics |
getAdditionalClaims() |
array |
Get accumulated JWT claims |
resetSubscribe() |
$this |
Reset accumulated subscribe topics |
resetAdditionalClaims() |
$this |
Reset accumulated JWT claims |
authorize(array $subscribe = [], array $additionalClaims = []) |
$this |
Set authorization cookie (merges with accumulated state, then resets) |
clearAuthorization() |
$this |
Clear authorization cookie |
discover() |
$this |
Add Mercure discovery Link header |
publish(Update $update) |
bool |
Publish an update to the Mercure hub |
publishJson(string|array $topics, mixed $data, ...) |
bool |
Publish JSON data (auto-encodes) |
publishSimple(string|array $topics, string $data, ...) |
bool |
Publish simple string data (no encoding) |
publishView(string|array $topics, ?string $template, ?string $element, array $data, ...) |
bool |
Publish rendered view/element |
getCookieName() |
string |
Get the cookie name |
Topic Management:
Topics added in the controller are automatically available in MercureHelper in your views:
// In controller public function view($id) { $book = $this->Books->get($id); // Add topics that will be available in the view $this->Mercure ->addTopic("/books/{$id}") ->addTopic("/user/{$userId}/updates") ->authorize(["/books/{$id}"]); $this->set('book', $book); } // In template - topics are automatically included const url = '<?= $this->Mercure->url() ?>'; // Subscribes to: /books/123 and /user/456/updates (from component)
Authorization Builder Pattern:
Build up authorization topics and claims fluently, then call authorize():
// Build up gradually with claims $this->Mercure ->addSubscribe('/books/123', ['sub' => $userId]) ->addSubscribe('/notifications/*', ['role' => 'admin']) ->authorize() ->discover(); // Add multiple at once $this->Mercure->addSubscribes( ['/books/123', '/notifications/*'], ['sub' => $userId, 'role' => 'admin'] ); // Mix builder and direct parameters $this->Mercure ->addSubscribe('/books/123') ->authorize(['/notifications/*'], ['sub' => $userId]); // Chain with topic management $this->Mercure ->addTopic('/books/123') // For EventSource ->addSubscribe('/books/123', ['sub' => $userId]) // For authorization ->authorize() ->discover();
Claims accumulate across multiple addSubscribe() calls. The authorize() method automatically resets accumulated state after setting the cookie.
Publishing convenience methods make it easy to publish updates directly from controllers:
// Publish JSON data $this->Mercure->publishJson( topics: '/books/123', data: ['status' => 'updated', 'title' => $book->title] ); // Publish rendered element $this->Mercure->publishView( topics: '/books/123', element: 'Books/item', data: ['book' => $book] ); // Publish rendered template with layout $this->Mercure->publishView( topics: '/notifications', template: 'Notifications/item', layout: 'ajax', data: ['notification' => $notification] ); // For advanced use cases, publish an Update object directly $update = new Update('/books/123', json_encode(['data' => 'value'])); $this->Mercure->publish($update);
Authorization
Static facade for direct authorization management (alternative to component).
| Method | Returns | Description |
|---|---|---|
setCookie(Response $response, array $subscribe, array $additionalClaims) |
Response |
Set authorization cookie |
clearCookie(Response $response) |
Response |
Clear authorization cookie |
addDiscoveryHeader(Response $response) |
Response |
Add Mercure discovery Link header |
getCookieName() |
string |
Get the cookie name |
MercureHelper
| Method | Returns | Description |
|---|---|---|
url(array|string|null $topics, array $subscribe, array $additionalClaims) |
string |
Get hub URL and optionally authorize (only sets cookie when $subscribe is provided). Merges with default topics if configured. |
addTopic(string $topic) |
$this |
Add a single topic to default topics (fluent interface) |
addTopics(array $topics) |
$this |
Add multiple topics to default topics (fluent interface) |
Configuration Options:
| Option | Type | Default | Description |
|---|---|---|---|
defaultTopics |
array |
[] |
Topics to automatically merge with every subscription (read-only, not mutated by addTopic()/addTopics()) |
Update
Base class for Mercure updates. For most use cases, consider using JsonUpdate or ViewUpdate instead.
Constructor:
new Update( string|array $topics, string $data, bool $private = false, ?string $id = null, ?string $type = null, ?int $retry = null )
Constructor Parameters:
| Parameter | Type | Description |
|---|---|---|
$topics |
string|array |
Topic IRI(s) for the update |
$data |
string |
Update content (typically JSON) |
$private |
bool |
Whether the update requires authorization |
$id |
?string |
Optional SSE event ID |
$type |
?string |
Optional SSE event type |
$retry |
?int |
Optional reconnection time in milliseconds |
Methods:
| Method | Returns | Description |
|---|---|---|
getTopics() |
array |
Get topics |
getData() |
string |
Get data |
isPrivate() |
bool |
Check if private |
getId() |
?string |
Get event ID |
getType() |
?string |
Get event type |
getRetry() |
?int |
Get retry value |
JsonUpdate
Specialized Update class that automatically encodes data to JSON. Supports both static factory method and fluent builder pattern.
Fluent Builder Pattern (Recommended):
use Mercure\Update\JsonUpdate; // Basic usage $update = (new JsonUpdate('/books/1')) ->data(['status' => 'OutOfStock', 'quantity' => 0]) ->build(); // With all options $update = (new JsonUpdate('/books/1')) ->data(['title' => 'Book', 'price' => 29.99]) ->jsonOptions(JSON_UNESCAPED_UNICODE) ->private() ->id(Text::uuid()) ->type('book.updated') ->retry(5000) ->build();
Builder Methods:
| Method | Parameter | Returns | Description |
|---|---|---|---|
data(mixed $data) |
Data to encode | $this |
Set data to encode as JSON |
jsonOptions(int $options) |
JSON options | $this |
Set JSON encoding options |
private(bool $private = true) |
Private flag | $this |
Mark as private update |
id(string $id) |
Event ID | $this |
Set SSE event ID |
type(string $type) |
Event type | $this |
Set SSE event type |
retry(int $retry) |
Retry delay (ms) | $this |
Set retry delay |
build() |
- | Update |
Build and return Update |
Static Factory Method:
JsonUpdate::create( string|array $topics, mixed $data, bool $private = false, ?string $id = null, ?string $type = null, ?int $retry = null, int $jsonOptions = JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR ): Update
ViewUpdate
Specialized Update class that automatically renders CakePHP views or elements. Supports both static factory method and fluent builder pattern.
Fluent Builder Pattern (Recommended):
use Mercure\Update\ViewUpdate; // Render element $update = (new ViewUpdate('/books/1')) ->element('Books/item') ->viewVars(['book' => $book]) ->build(); // Render template with all options $update = (new ViewUpdate('/notifications')) ->template('Notifications/item') ->viewVars(['notification' => $notification]) ->layout('ajax') ->viewOptions(['key' => 'value']) ->private() ->id('notif-123') ->type('notification.new') ->build();
Builder Methods:
| Method | Parameter | Returns | Description |
|---|---|---|---|
template(string $template) |
Template name | $this |
Set template to render |
element(string $element) |
Element name | $this |
Set element to render |
viewVars(array $viewVars) |
View variables | $this |
Set view variables |
set(string $key, mixed $value) |
Key, value | $this |
Set single view variable |
layout(?string $layout) |
Layout name | $this |
Set layout (null to disable) |
viewOptions(array $options) |
ViewBuilder options | $this |
Set ViewBuilder options |
private(bool $private = true) |
Private flag | $this |
Mark as private update |
id(string $id) |
Event ID | $this |
Set SSE event ID |
type(string $type) |
Event type | $this |
Set SSE event type |
retry(int $retry) |
Retry delay (ms) | $this |
Set retry delay |
build() |
- | Update |
Build and return Update |
Static Factory Method:
ViewUpdate::create( string|array $topics, ?string $template = null, ?string $element = null, array $viewVars = [], ?string $layout = null, bool $private = false, ?string $id = null, ?string $type = null, ?int $retry = null, array $viewOptions = [] ): Update
Parameters:
| Parameter | Type | Description |
|---|---|---|
$topics |
string|array |
Topic IRI(s) for the update |
$template |
?string |
Template to render (e.g., 'Books/view') |
$element |
?string |
Element to render (e.g., 'Books/item') |
$data |
array |
View variables to pass to the template/element |
$layout |
?string |
Layout to use (null for no layout) |
$private |
bool |
Whether this is a private update |
$id |
?string |
Optional SSE event ID |
$type |
?string |
Optional SSE event type |
$retry |
?int |
Optional reconnection time in milliseconds |
Note
Either template or element must be specified, but not both.
Example:
use Mercure\Update\ViewUpdate; // Render an element $update = ViewUpdate::create( topics: '/books/1', element: 'Books/item', data: ['book' => $book] ); // Render a template with layout $update = ViewUpdate::create( topics: '/dashboard', template: 'Dashboard/stats', layout: 'ajax', data: ['stats' => $stats] );
MercureDiscoveryMiddleware
A PSR-15 middleware that automatically adds the Mercure discovery Link header to all responses.
Usage:
// In src/Application.php use Mercure\Http\Middleware\MercureDiscoveryMiddleware; public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue { $middlewareQueue->add(new MercureDiscoveryMiddleware()); return $middlewareQueue; }
The middleware adds a Link header to every response:
Link: <https://mercure.example.com/.well-known/mercure>; rel="mercure"
This allows clients to automatically discover the Mercure hub URL without hardcoding it in your application.
For more information about the Mercure protocol, visit mercure.rocks.
Contributing
Contributions are welcome! Please follow these guidelines:
-
Code Quality: Ensure all code passes quality checks:
composer cs-check # Check code style composer stan # Run PHPStan analysis composer rector-check # Run rectoring composer test # Run tests
-
Code Style: Follow CakePHP coding standards. Use
composer cs-fixto automatically fix style issues. -
Tests: Add tests for new features and ensure all tests pass.
-
Documentation: Update the README and inline documentation as needed.
-
Pull Requests: Submit PRs against the
mainbranch with a clear description of changes.
License
MIT License. See LICENSE.md for details.