Skip to content

Commit 4989736

Browse files
committed
feat: add subscribe builder pattern in component
1 parent 8044be7 commit 4989736

File tree

3 files changed

+493
-13
lines changed

3 files changed

+493
-13
lines changed

README.md

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -519,16 +519,27 @@ class BooksController extends AppController
519519
public function view($id)
520520
{
521521
$book = $this->Books->get($id);
522+
$userId = $this->request->getAttribute('identity')->id;
522523
523-
// Authorize subscriber for private topics
524-
$this->Mercure->authorize(["https://example.com/books/{$id}"]);
524+
// Fluent authorization with builder pattern
525+
$this->Mercure
526+
->addSubscribe("https://example.com/books/{$id}", ['sub' => $userId])
527+
->addSubscribe("https://example.com/notifications/*", ['role' => 'user'])
528+
->authorize()
529+
->discover();
525530
526-
// Or with additional JWT claims
531+
// Or direct authorization (backward compatible)
527532
$this->Mercure->authorize(
528533
subscribe: ["https://example.com/books/{$id}"],
529-
additionalClaims: ['user_id' => $this->request->getAttribute('identity')->id]
534+
additionalClaims: ['sub' => $userId]
530535
);
531536
537+
// Or build up gradually
538+
$this->Mercure
539+
->addSubscribe("https://example.com/books/{$id}")
540+
->addSubscribe("https://example.com/user/{$userId}/updates", ['sub' => $userId])
541+
->authorize(); // Uses accumulated topics and claims
542+
532543
$this->set('book', $book);
533544
}
534545
@@ -551,6 +562,36 @@ $this->loadComponent('Mercure.Mercure', [
551562
]);
552563
```
553564
565+
#### Authorization Builder Pattern
566+
567+
The component supports a fluent builder pattern for authorization, making it easy to build up topics and claims:
568+
569+
```php
570+
// Build authorization gradually
571+
$this->Mercure
572+
->addSubscribe('/books/123', ['sub' => $userId])
573+
->addSubscribe('/notifications/*', ['role' => 'admin'])
574+
->authorize();
575+
576+
// Add multiple topics at once
577+
$this->Mercure->addSubscribes(
578+
['/books/123', '/notifications/{id}'],
579+
['sub' => $userId, 'role' => 'admin']
580+
);
581+
582+
// Mix with direct parameters
583+
$this->Mercure
584+
->addSubscribe('/books/123')
585+
->authorize(['/notifications/{id}'], ['sub' => $userId]);
586+
587+
// Chain with topic management and discovery
588+
$this->Mercure
589+
->addTopic('/books/123') // For EventSource subscription
590+
->addSubscribe('/books/123', ['sub' => $userId]) // For authorization
591+
->authorize()
592+
->discover();
593+
```
594+
554595
#### Using the View Helper (Easiest)
555596
556597
For the simplest, self-contained approach, use the `MercureHelper::url()` method with the `subscribe` parameter. This **automatically handles both authorization and URL generation** in one call:
@@ -956,13 +997,13 @@ Topics added in the controller are automatically available in `MercureHelper` in
956997
public function view($id)
957998
{
958999
$book = $this->Books->get($id);
959-
1000+
9601001
// Add topics that will be available in the view
9611002
$this->Mercure
9621003
->addTopic("/books/{$id}")
9631004
->addTopic("/user/{$userId}/updates")
9641005
->authorize(["/books/{$id}"]);
965-
1006+
9661007
$this->set('book', $book);
9671008
}
9681009

src/Controller/Component/MercureComponent.php

Lines changed: 169 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,27 @@
3333
* public function view($id)
3434
* {
3535
* $book = $this->Books->get($id);
36+
* $userId = $this->Authentication->getIdentity()->id;
3637
*
3738
* // Add topics for this view (available in MercureHelper)
3839
* $this->Mercure->addTopic("/books/{$id}");
3940
*
40-
* // Authorize subscriber
41-
* $this->Mercure->authorize(["/books/{$id}"]);
41+
* // Fluent authorization with builder pattern
42+
* $this->Mercure
43+
* ->addSubscribe("/books/{$id}", ['sub' => $userId])
44+
* ->addSubscribe("/notifications/*", ['role' => 'user'])
45+
* ->authorize()
46+
* ->discover();
47+
*
48+
* // Or direct authorization (backward compatible)
49+
* $this->Mercure->authorize(["/books/{$id}"], ['sub' => $userId]);
4250
*
43-
* // Or chain methods
51+
* // Chain topics and authorization together
4452
* $this->Mercure
4553
* ->addTopic("/books/{$id}")
4654
* ->addTopic("/user/{$userId}/updates")
47-
* ->authorize(["/books/{$id}"])
55+
* ->addSubscribe("/books/{$id}", ['sub' => $userId])
56+
* ->authorize()
4857
* ->discover();
4958
*
5059
* $this->set('book', $book);
@@ -95,6 +104,20 @@ class MercureComponent extends Component
95104

96105
protected PublisherInterface $publisherService;
97106

107+
/**
108+
* Subscribe topics accumulated via addSubscribe()
109+
*
110+
* @var array<string>
111+
*/
112+
protected array $subscribe = [];
113+
114+
/**
115+
* Additional JWT claims accumulated via addSubscribe()
116+
*
117+
* @var array<string, mixed>
118+
*/
119+
protected array $additionalClaims = [];
120+
98121
/**
99122
* Default configuration
100123
*
@@ -148,35 +171,174 @@ public function beforeRender(EventInterface $event): void
148171
}
149172
}
150173

174+
/**
175+
* Add a single topic to authorize for subscription
176+
*
177+
* Accumulates topics and claims that will be used when authorize() is called.
178+
* Claims are merged across multiple calls.
179+
*
180+
* Example:
181+
* ```
182+
* // Topic only
183+
* $this->Mercure->addSubscribe('/books/123');
184+
*
185+
* // Topic with claims
186+
* $this->Mercure->addSubscribe('/books/123', ['sub' => $userId, 'role' => 'admin']);
187+
*
188+
* // Build up gradually
189+
* $this->Mercure
190+
* ->addSubscribe('/books/123', ['sub' => $userId])
191+
* ->addSubscribe('/notifications/*', ['role' => 'admin'])
192+
* ->authorize();
193+
* ```
194+
*
195+
* @param string $topic Topic pattern (e.g., '/books/123', '/notifications/*')
196+
* @param array<string, mixed> $additionalClaims JWT claims to merge
197+
* @return $this For method chaining
198+
*/
199+
public function addSubscribe(string $topic, array $additionalClaims = []): static
200+
{
201+
$this->subscribe[] = $topic;
202+
$this->additionalClaims = array_merge($this->additionalClaims, $additionalClaims);
203+
204+
return $this;
205+
}
206+
207+
/**
208+
* Add multiple topics to authorize for subscription
209+
*
210+
* Accumulates topics and claims that will be used when authorize() is called.
211+
* Claims are merged with any existing accumulated claims.
212+
*
213+
* Example:
214+
* ```
215+
* // Topics only
216+
* $this->Mercure->addSubscribes(['/books/123', '/notifications/*']);
217+
*
218+
* // Topics with claims
219+
* $this->Mercure->addSubscribes(
220+
* ['/books/123', '/notifications/*'],
221+
* ['sub' => $userId, 'role' => 'admin']
222+
* );
223+
* ```
224+
*
225+
* @param array<string> $topics Array of topic patterns
226+
* @param array<string, mixed> $additionalClaims JWT claims to merge
227+
* @return $this For method chaining
228+
*/
229+
public function addSubscribes(array $topics, array $additionalClaims = []): static
230+
{
231+
$this->subscribe = array_merge($this->subscribe, $topics);
232+
$this->additionalClaims = array_merge($this->additionalClaims, $additionalClaims);
233+
234+
return $this;
235+
}
236+
237+
/**
238+
* Get accumulated subscribe topics
239+
*
240+
* @return array<string>
241+
*/
242+
public function getSubscribe(): array
243+
{
244+
return $this->subscribe;
245+
}
246+
247+
/**
248+
* Get accumulated additional claims
249+
*
250+
* @return array<string, mixed>
251+
*/
252+
public function getAdditionalClaims(): array
253+
{
254+
return $this->additionalClaims;
255+
}
256+
257+
/**
258+
* Reset accumulated subscribe topics
259+
*
260+
* @return $this For method chaining
261+
*/
262+
public function resetSubscribe(): static
263+
{
264+
$this->subscribe = [];
265+
266+
return $this;
267+
}
268+
269+
/**
270+
* Reset accumulated additional claims
271+
*
272+
* @return $this For method chaining
273+
*/
274+
public function resetAdditionalClaims(): static
275+
{
276+
$this->additionalClaims = [];
277+
278+
return $this;
279+
}
280+
151281
/**
152282
* Authorize subscriber for private topics
153283
*
154284
* Sets an authorization cookie that allows the subscriber to access
155285
* private Mercure topics. The cookie must be set before establishing
156286
* the EventSource connection.
157287
*
288+
* Supports both direct parameters and accumulated state via builder methods.
289+
* Parameters are merged with accumulated state. Accumulated state is automatically
290+
* reset after authorization.
291+
*
158292
* Example:
159293
* ```
294+
* // Direct parameters (backward compatible)
160295
* $this->Mercure->authorize(['/feeds/123', '/notifications/*']);
161296
*
162297
* // With additional JWT claims
163298
* $this->Mercure->authorize(
164299
* subscribe: ['/feeds/123'],
165300
* additionalClaims: ['sub' => $userId, 'aud' => 'my-app']
166301
* );
302+
*
303+
* // Using accumulated state
304+
* $this->Mercure
305+
* ->addSubscribe('/books/123', ['sub' => $userId])
306+
* ->authorize();
307+
*
308+
* // Mixed (parameters merged with accumulated)
309+
* $this->Mercure
310+
* ->addSubscribe('/books/123')
311+
* ->authorize(['/notifications/*'], ['sub' => $userId]);
312+
*
313+
* // Chain with topics and discovery
314+
* $this->Mercure
315+
* ->addTopic('/books/123')
316+
* ->addSubscribe('/books/123', ['sub' => $userId])
317+
* ->authorize()
318+
* ->discover();
167319
* ```
168320
*
169-
* @param array<string> $subscribe Topics the subscriber can access
170-
* @param array<string, mixed> $additionalClaims Additional JWT claims to include
321+
* @param array<string> $subscribe Topics the subscriber can access (merged with addSubscribe())
322+
* @param array<string, mixed> $additionalClaims Additional JWT claims to include (merged with accumulated claims)
171323
* @return $this For method chaining
172324
* @throws \Mercure\Exception\MercureException
173325
*/
174326
public function authorize(array $subscribe = [], array $additionalClaims = []): static
175327
{
328+
// Merge parameter topics with accumulated topics
329+
$allSubscribe = array_unique(array_merge($this->subscribe, $subscribe));
330+
331+
// Merge parameter claims with accumulated claims (parameters take precedence)
332+
$allClaims = array_merge($this->additionalClaims, $additionalClaims);
333+
176334
$response = $this->getController()->getResponse();
177-
$response = $this->authorizationService->setCookie($response, $subscribe, $additionalClaims);
335+
$response = $this->authorizationService->setCookie($response, $allSubscribe, $allClaims);
178336
$this->getController()->setResponse($response);
179337

338+
// Reset accumulated state after authorization
339+
$this->resetSubscribe();
340+
$this->resetAdditionalClaims();
341+
180342
return $this;
181343
}
182344

0 commit comments

Comments
 (0)