diff --git a/docs/README.md b/docs/README.md index 94032101..f2f3e184 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,6 +8,7 @@ ## Detailed documentation * [Configuration](sections/configuration.md) * [Cron](sections/cron.md) for Cronjob management +* [Real-Time Progress](sections/realtime_progress.md) with Mercure/FrankenPHP * [Mailing](sections/mailing.md) * [Miscellaneous](sections/misc.md) * [Tips](sections/tips.md) and maximum IDE support diff --git a/docs/sections/realtime_progress.md b/docs/sections/realtime_progress.md new file mode 100644 index 00000000..befaecc0 --- /dev/null +++ b/docs/sections/realtime_progress.md @@ -0,0 +1,425 @@ +# Real-Time Progress with Mercure + +This guide explains how to display real-time queue job progress in the browser using [Mercure](https://mercure.rocks/) and Server-Sent Events (SSE). This is particularly useful with [FrankenPHP](https://frankenphp.dev/) which has Mercure built-in. + +## Overview + +Instead of polling the server or requiring page refreshes, the queue worker pushes updates directly to the browser: + +1. User triggers a background job +2. Job runs in queue worker process +3. Each progress step publishes a Mercure update +4. Browser receives updates instantly via EventSource + +## Requirements + +- [josbeir/cakephp-mercure](https://github.com/josbeir/cakephp-mercure) plugin +- Mercure hub (standalone or built into FrankenPHP) + +## Setup + +### 1. Install the Mercure Plugin + +```bash +composer require josbeir/cakephp-mercure +``` + +Load the plugin: + +```php +// config/plugins.php +return [ + 'Mercure' => [], + // ... +]; +``` + +### 2. Configure Mercure + +Create `config/app_mercure.php`: + +```php + [ + // Internal URL for server-side publishing (inside container/server) + 'url' => 'http://localhost/.well-known/mercure', + + // External URL for browser EventSource connections + 'public_url' => 'https://your-domain.com/.well-known/mercure', + + 'jwt' => [ + 'secret' => 'your-mercure-jwt-secret', + 'algorithm' => 'HS256', + 'publish' => ['*'], + 'subscribe' => [], + ], + + 'cookie' => [ + 'name' => 'mercureAuthorization', + 'secure' => true, + 'httponly' => true, + 'samesite' => CookieInterface::SAMESITE_LAX, + ], + ], +]; +``` + +Load it in `config/bootstrap.php`: + +```php +Configure::load('app_mercure'); +``` + +### 3. FrankenPHP with Mercure + +If using FrankenPHP, add Mercure to your Caddyfile or environment: + +``` +CADDY_SERVER_EXTRA_DIRECTIVES= +mercure { + publisher_jwt your-mercure-jwt-secret + subscriber_jwt your-mercure-jwt-secret + anonymous + cors_origins * +} +``` + +For DDEV, create `.ddev/docker-compose.mercure.yaml`: + +```yaml +services: + web: + environment: + - |- + CADDY_SERVER_EXTRA_DIRECTIVES= + mercure { + publisher_jwt your-mercure-jwt-secret + subscriber_jwt your-mercure-jwt-secret + anonymous + cors_origins * + } +``` + +## Creating a Queue Task with Mercure Updates + +```php +publishUpdate($topic, [ + 'status' => 'started', + 'progress' => 0, + 'message' => 'Job started', + 'jobId' => $jobId, + ]); + } + + for ($i = 1; $i <= $steps; $i++) { + // Do actual work here... + sleep(1); + + $progress = (int)(($i / $steps) * 100); + + // Update queue progress (for DB tracking) + $this->QueuedJobs->updateProgress($jobId, $i / $steps, "Step {$i} of {$steps}"); + + // Publish Mercure update (for real-time UI) + if ($mercureConfigured) { + $this->publishUpdate($topic, [ + 'status' => 'progress', + 'progress' => $progress, + 'step' => $i, + 'totalSteps' => $steps, + 'message' => "Processing step {$i} of {$steps}", + 'jobId' => $jobId, + ]); + } + } + + // Publish completion event + if ($mercureConfigured) { + $this->publishUpdate($topic, [ + 'status' => 'completed', + 'progress' => 100, + 'message' => 'Job completed successfully!', + 'jobId' => $jobId, + ]); + } + } + + protected function publishUpdate(string $topic, array $data): void { + try { + Publisher::publish(JsonUpdate::create( + topics: $topic, + data: $data, + )); + } catch (\Exception $e) { + $this->io->error('Mercure publish failed: ' . $e->getMessage()); + } + } +} +``` + +## Controller + +```php +request->getSession()->id(); + $topic = '/jobs/user/' . $sid; + + $this->set('topic', $topic); + $this->set('mercurePublicUrl', Configure::read('Mercure.public_url')); + } + + public function startJob() { + $this->request->allowMethod('post'); + + $queuedJobsTable = $this->fetchTable('Queue.QueuedJobs'); + $sid = $this->request->getSession()->id(); + $topic = '/jobs/user/' . $sid; + + $queuedJobsTable->createJob( + 'MyProgress', + ['topic' => $topic], + ['reference' => 'user-job-' . $sid], + ); + + $this->Flash->success('Job started!'); + return $this->redirect(['action' => 'progress']); + } +} +``` + +## Template with EventSource + +```php + + +
Waiting for job...
+