PHPackages                             josbeir/cakephp-mercure - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. josbeir/cakephp-mercure

ActiveCakephp-plugin[API Development](/categories/api)

josbeir/cakephp-mercure
=======================

Mercure plugin for CakePHP

0.2.0(2mo ago)32.2kMITPHPPHP &gt;=8.2CI passing

Since Oct 18Pushed 2mo agoCompare

[ Source](https://github.com/josbeir/cakephp-mercure)[ Packagist](https://packagist.org/packages/josbeir/cakephp-mercure)[ RSS](/packages/josbeir-cakephp-mercure/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (9)Dependencies (7)Versions (11)Used By (0)

[![PHPStan Level 8](https://camo.githubusercontent.com/ff3c7f8c8667ce643f47e74532748f673482a5f95d7d4269f925f2eebbe5117e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048505374616e2d6c6576656c253230382d627269676874677265656e)](https://github.com/josbeir/cakephp-mercure)[![Build Status](https://github.com/josbeir/cakephp-mercure/actions/workflows/ci.yml/badge.svg)](https://github.com/josbeir/cakephp-mercure/actions)[![codecov](https://camo.githubusercontent.com/c1f329afacb70540e9d1f8310949f920a38bb1e273dbfdf711d54bf3a350f659/68747470733a2f2f636f6465636f762e696f2f6769746875622f6a6f73626569722f63616b657068702d6d6572637572652f67726170682f62616467652e7376673f746f6b656e3d345647574a5154574835)](https://codecov.io/github/josbeir/cakephp-mercure)[![License: MIT](https://camo.githubusercontent.com/fdf2982b9f5d7489dcf44570e714e3a15fce6253e0cc6b5aa61a075aac2ff71b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d79656c6c6f772e737667)](https://opensource.org/licenses/MIT)[![PHP Version](https://camo.githubusercontent.com/744f8821cc27dec8b0013ade48179731a44eadf4f943e0b1d9ffcb93f80177de/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d382e322532422d626c75652e737667)](https://www.php.net/releases/8.2/en.php)[![Packagist Downloads](https://camo.githubusercontent.com/7834588d028937b6d7e653240be5237855824aa1c2c149b85e97cdfb3efa5d5d/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6a6f73626569722f63616b657068702d6d657263757265)](https://packagist.org/packages/josbeir/cakephp-mercure)

CakePHP Mercure Plugin
======================

[](#cakephp-mercure-plugin)

Push real-time updates to clients using the Mercure protocol.

[![Mercure](https://camo.githubusercontent.com/6fe23edfb7bbad332ff3dc3b2725221652ee2ac2ae517af7720283f5e1d09b25/68747470733a2f2f6d6572637572652e726f636b732f7374617469632f6c6f676f2e737667)](https://mercure.rocks/)

Table of Contents
-----------------

[](#table-of-contents)

- [Overview](#overview)
- [Installation](#installation)
    - [Installing the Plugin](#installing-the-plugin)
    - [Running a Mercure Hub](#running-a-mercure-hub)
    - [Custom DDEV Setup with Nginx Proxy](#custom-ddev-setup-with-nginx-proxy)
- [Configuration](#configuration)
- [Basic Usage](#basic-usage)
    - [Choosing Your Authorization Strategy](#choosing-your-authorization-strategy)
    - [Publishing Updates](#publishing-updates)
        - [Publishing JSON Data](#publishing-json-data)
        - [Publishing Rendered Views](#publishing-rendered-views)
    - [Subscribing to Updates](#subscribing-to-updates)
- [Authorization](#authorization)
    - [Publishing Private Updates](#publishing-private-updates)
    - [Setting Authorization Cookies](#setting-authorization-cookies)
        - [Using the Component](#using-the-component)
        - [Setting Default Topics](#setting-default-topics)
        - [Using the Facade classes](#using-the-facade-classes)
- [Mercure Discovery](#mercure-discovery)
    - [Using the Component](#using-the-component-1)
    - [Using Middleware](#using-middleware)
- [Advanced Configuration](#advanced-configuration)
    - [JWT Token Strategies](#jwt-token-strategies)
    - [HTTP Client Options](#http-client-options)
    - [Cookie Configuration](#cookie-configuration)
- [Testing](#testing)
- [API Reference](#api-reference)
    - [Publisher](#publisher)
    - [MercureComponent](#mercurecomponent)
    - [Authorization](#authorization-1)
    - [MercureHelper](#mercurehelper)
    - [Update](#update)
    - [JsonUpdate](#jsonupdate)
    - [ViewUpdate](#viewupdate)
    - [MercureDiscoveryMiddleware](#mercurediscoverymiddleware)
- [Cookbook: Queue Job Progress Tracking](#cookbook-queue-job-progress-tracking)
    - [Creating a Progress Trait](#creating-a-progress-trait)
    - [Using the Trait in Queue Tasks](#using-the-trait-in-queue-tasks)
    - [Frontend: Real-time Progress Display](#frontend-real-time-progress-display)
    - [Controller for Triggering Jobs](#controller-for-triggering-jobs)
- [Contributing](#contributing)
- [License](#license)

Overview
--------

[](#overview)

This plugin provides integration between CakePHP applications and the [Mercure protocol](https://mercure.rocks/), 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
------------

[](#installation)

### Installing the Plugin

[](#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

[](#running-a-mercure-hub)

Mercure requires a hub server to manage persistent SSE connections. Download the official hub from [Mercure.rocks](https://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](https://addons.ddev.com/addons/Rindula/ddev-mercure).

The hub will be available at `http://localhost:3000/.well-known/mercure`.

### Custom DDEV Setup with Nginx Proxy

[](#custom-ddev-setup-with-nginx-proxy)

For environments with dynamic ports (like DDEV), you can set up a custom Mercure container with an nginx proxy. This allows using relative URLs that work regardless of the port.

Create `.ddev/docker-compose.mercure.yaml`:

```
services:
  mercure:
    image: dunglas/mercure
    container_name: ddev-${DDEV_SITENAME}-mercure
    restart: "no"
    expose:
      - "80"
    environment:
      SERVER_NAME: ':80'
      MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
      MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
      MERCURE_EXTRA_DIRECTIVES: |
        anonymous
        cors_origins *
    labels:
      com.ddev.site-name: ${DDEV_SITENAME}
      com.ddev.approot: ${DDEV_APPROOT}

  web:
    environment:
      - MERCURE_URL=http://mercure/.well-known/mercure
      - MERCURE_PUBLIC_URL=/mercure-hub
      - MERCURE_JWT_SECRET=!ChangeThisMercureHubJWTSecretKey!
```

Create `.ddev/nginx/mercure.conf`:

```
location /mercure-hub {
    rewrite ^/mercure-hub(.*)$ /.well-known/mercure$1 break;
    proxy_pass http://mercure:80;
    proxy_http_version 1.1;
    proxy_set_header Connection '';
    proxy_set_header Cache-Control 'no-cache';
    proxy_set_header X-Accel-Buffering 'no';
    proxy_buffering off;
    chunked_transfer_encoding off;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_read_timeout 24h;
    proxy_connect_timeout 1h;
}
```

Then configure relative URLs in `config/app_custom.php`:

```
'Mercure' => [
    'url' => env('MERCURE_URL', 'http://mercure/.well-known/mercure'),
    'public_url' => env('MERCURE_PUBLIC_URL', '/mercure-hub'),
    'jwt' => [
        'secret' => env('MERCURE_JWT_SECRET', '!ChangeThisMercureHubJWTSecretKey!'),
        'algorithm' => 'HS256',
        'publish' => ['*'],
    ],
],
```

Run `ddev restart` to apply the changes. Using a relative `public_url` like `/mercure-hub` means the browser will automatically use the current origin, making it work regardless of DDEV's dynamic port assignment.

Tip

**Using FrankenPHP?** You're good to go! FrankenPHP has Mercure built in—no separate hub needed. See the [FrankenPHP Mercure documentation](https://frankenphp.dev/docs/mercure/) for details.

Configuration
-------------

[](#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:

1. **Plugin defaults** - `vendor/josbeir/cakephp-mercure/config/mercure.php` (loaded automatically)
2. **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](https://github.com/josbeir/cakephp-mercure/blob/main/config/mercure.php).

Basic Usage
-----------

[](#basic-usage)

The plugin provides multiple integration points depending on your use case:

- **Controllers**: Use the `MercureComponent` to centrally manage both authorization and subscriptions as topics
- **Templates**: Use the `MercureHelper` to generate Mercure topic URLs for EventSource subscriptions in your views and templates.
- **Services &amp; Manual Control**: Use the `Publisher` facade to publish updates and the `Authorization` facade 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

[](#choosing-your-authorization-strategy)

Pick the approach that best fits your workflow:

ScenarioRecommended ApproachMethod 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

[](#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](#mercurecomponent) for all available methods.

#### Publishing JSON Data

[](#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

[](#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](https://htmx.org/) (for instance, using the [htmx-sse extension](https://htmx.org/extensions/sse/)), [Hotwire](https://hotwired.dev/) (with [Turbo Streams](https://turbo.hotwired.dev/handbook/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

[](#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
Available

// For public topics (no authorization needed)
const eventSource = new EventSource('
