Skip to content

Commit b880d95

Browse files
committed
[Turbo] Add support for providing multiple mercure topics to turbo_stream_listen
1 parent 3ea19c1 commit b880d95

8 files changed

+150
-16
lines changed

src/Turbo/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- Add `<twig:Turbo:Stream>` component
66
- Add `<twig:Turbo:Frame>` component
77
- Add support for custom actions in `TurboStream` and `TurboStreamResponse`
8+
- Add support for providing multiple mercure topics to `turbo_stream_listen`
89

910
## 2.21.0
1011

src/Turbo/assets/dist/turbo_stream_controller.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@ import { Controller } from '@hotwired/stimulus';
22
export default class extends Controller {
33
static values: {
44
topic: StringConstructor;
5+
topics: ArrayConstructor;
56
hub: StringConstructor;
67
};
78
es: EventSource | undefined;
89
url: string | undefined;
910
readonly topicValue: string;
11+
readonly topicsValue: string[];
1012
readonly hubValue: string;
1113
readonly hasHubValue: boolean;
1214
readonly hasTopicValue: boolean;
15+
readonly hasTopicsValue: boolean;
1316
initialize(): void;
1417
connect(): void;
1518
disconnect(): void;

src/Turbo/assets/dist/turbo_stream_controller.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,19 @@ class default_1 extends Controller {
66
const errorMessages = [];
77
if (!this.hasHubValue)
88
errorMessages.push('A "hub" value pointing to the Mercure hub must be provided.');
9-
if (!this.hasTopicValue)
10-
errorMessages.push('A "topic" value must be provided.');
9+
if (!this.hasTopicValue && !this.hasTopicsValue)
10+
errorMessages.push('Either "topic" or "topics" value must be provided.');
1111
if (errorMessages.length)
1212
throw new Error(errorMessages.join(' '));
1313
const u = new URL(this.hubValue);
14-
u.searchParams.append('topic', this.topicValue);
14+
if (this.hasTopicValue) {
15+
u.searchParams.append('topic', this.topicValue);
16+
}
17+
else {
18+
this.topicsValue.forEach((topic) => {
19+
u.searchParams.append('topic', topic);
20+
});
21+
}
1522
this.url = u.toString();
1623
}
1724
connect() {
@@ -29,6 +36,7 @@ class default_1 extends Controller {
2936
}
3037
default_1.values = {
3138
topic: String,
39+
topics: Array,
3240
hub: String,
3341
};
3442

src/Turbo/assets/src/turbo_stream_controller.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,34 @@ import { connectStreamSource, disconnectStreamSource } from '@hotwired/turbo';
1616
export default class extends Controller {
1717
static values = {
1818
topic: String,
19+
topics: Array,
1920
hub: String,
2021
};
2122
es: EventSource | undefined;
2223
url: string | undefined;
2324

2425
declare readonly topicValue: string;
26+
declare readonly topicsValue: string[];
2527
declare readonly hubValue: string;
2628
declare readonly hasHubValue: boolean;
2729
declare readonly hasTopicValue: boolean;
30+
declare readonly hasTopicsValue: boolean;
2831

2932
initialize() {
3033
const errorMessages: string[] = [];
3134
if (!this.hasHubValue) errorMessages.push('A "hub" value pointing to the Mercure hub must be provided.');
32-
if (!this.hasTopicValue) errorMessages.push('A "topic" value must be provided.');
35+
if (!this.hasTopicValue && !this.hasTopicsValue)
36+
errorMessages.push('Either "topic" or "topics" value must be provided.');
3337
if (errorMessages.length) throw new Error(errorMessages.join(' '));
3438

3539
const u = new URL(this.hubValue);
36-
u.searchParams.append('topic', this.topicValue);
40+
if (this.hasTopicValue) {
41+
u.searchParams.append('topic', this.topicValue);
42+
} else {
43+
this.topicsValue.forEach((topic) => {
44+
u.searchParams.append('topic', topic);
45+
});
46+
}
3747

3848
this.url = u.toString();
3949
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\Turbo\Bridge\Mercure;
13+
14+
final class TopicSet
15+
{
16+
/**
17+
* @param array<string|object> $topics
18+
*/
19+
public function __construct(
20+
private array $topics,
21+
) {
22+
}
23+
24+
/**
25+
* @return array<string|object>
26+
*/
27+
public function getTopics(): array
28+
{
29+
return $this->topics;
30+
}
31+
}

src/Turbo/src/Bridge/Mercure/TurboStreamListenRenderer.php

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,30 @@ public function __construct(
4343
}
4444

4545
public function renderTurboStreamListen(Environment $env, $topic): string
46+
{
47+
if ($topic instanceof TopicSet) {
48+
$topics = array_map(\Closure::fromCallable([$this, 'resolveTopic']), $topic->getTopics());
49+
} else {
50+
$topics = [$this->resolveTopic($topic)];
51+
}
52+
53+
$controllerAttributes = ['hub' => $this->hub->getPublicUrl()];
54+
if (1 < \count($topics)) {
55+
$controllerAttributes['topics'] = $topics;
56+
} else {
57+
$controllerAttributes['topic'] = current($topics);
58+
}
59+
60+
$stimulusAttributes = $this->stimulusHelper->createStimulusAttributes();
61+
$stimulusAttributes->addController(
62+
'symfony/ux-turbo/mercure-turbo-stream',
63+
$controllerAttributes,
64+
);
65+
66+
return (string) $stimulusAttributes;
67+
}
68+
69+
private function resolveTopic(object|string $topic): string
4670
{
4771
if (\is_object($topic)) {
4872
$class = $topic::class;
@@ -51,18 +75,14 @@ public function renderTurboStreamListen(Environment $env, $topic): string
5175
throw new \LogicException(\sprintf('Cannot listen to entity of class "%s" as the PropertyAccess component is not installed. Try running "composer require symfony/property-access".', $class));
5276
}
5377

54-
$topic = \sprintf(Broadcaster::TOPIC_PATTERN, rawurlencode($class), rawurlencode(implode('-', $id)));
55-
} elseif (!preg_match('/[^a-zA-Z0-9_\x7f-\xff\\\\]/', $topic) && class_exists($topic)) {
56-
// Generate a URI template to subscribe to updates for all objects of this class
57-
$topic = \sprintf(Broadcaster::TOPIC_PATTERN, rawurlencode($topic), '{id}');
78+
return \sprintf(Broadcaster::TOPIC_PATTERN, rawurlencode($class), rawurlencode(implode('-', $id)));
5879
}
5980

60-
$stimulusAttributes = $this->stimulusHelper->createStimulusAttributes();
61-
$stimulusAttributes->addController(
62-
'symfony/ux-turbo/mercure-turbo-stream',
63-
['topic' => $topic, 'hub' => $this->hub->getPublicUrl()]
64-
);
81+
if (!preg_match('/[^a-zA-Z0-9_\x7f-\xff\\\\]/', $topic) && class_exists($topic)) {
82+
// Generate a URI template to subscribe to updates for all objects of this class
83+
return \sprintf(Broadcaster::TOPIC_PATTERN, rawurlencode($topic), '{id}');
84+
}
6585

66-
return (string) $stimulusAttributes;
86+
return $topic;
6787
}
6888
}

src/Turbo/src/Twig/TwigExtension.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\UX\Turbo\Twig;
1313

1414
use Psr\Container\ContainerInterface;
15+
use Symfony\UX\Turbo\Bridge\Mercure\TopicSet;
1516
use Twig\Environment;
1617
use Twig\Extension\AbstractExtension;
1718
use Twig\TwigFunction;
@@ -35,7 +36,7 @@ public function getFunctions(): array
3536
}
3637

3738
/**
38-
* @param object|string $topic
39+
* @param object|string|array<object|string> $topic
3940
*/
4041
public function turboStreamListen(Environment $env, $topic, ?string $transport = null): string
4142
{
@@ -45,6 +46,10 @@ public function turboStreamListen(Environment $env, $topic, ?string $transport =
4546
throw new \InvalidArgumentException(\sprintf('The Turbo stream transport "%s" does not exist.', $transport));
4647
}
4748

49+
if (\is_array($topic)) {
50+
$topic = new TopicSet($topic);
51+
}
52+
4853
return $this->turboStreamListenRenderers->get($transport)->renderTurboStreamListen($env, $topic);
4954
}
5055
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\Turbo\Tests\Bridge\Mercure;
13+
14+
use App\Entity\Book;
15+
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
16+
17+
final class TurboStreamListenRendererTest extends KernelTestCase
18+
{
19+
/**
20+
* @dataProvider provideTestCases
21+
*/
22+
public function testRenderTurboStreamListen(string $template, array $context, string $expectedResult)
23+
{
24+
$this->assertSame($expectedResult, self::getContainer()->get('twig')->createTemplate($template)->render($context));
25+
}
26+
27+
public static function provideTestCases(): iterable
28+
{
29+
$book = new Book();
30+
$book->id = 123;
31+
32+
yield [
33+
"{{ turbo_stream_listen('a_topic') }}",
34+
[],
35+
'data-controller="symfony--ux-turbo--mercure-turbo-stream" data-symfony--ux-turbo--mercure-turbo-stream-hub-value="http://127.0.0.1:3000/.well-known/mercure" data-symfony--ux-turbo--mercure-turbo-stream-topic-value="a_topic"',
36+
];
37+
38+
yield [
39+
"{{ turbo_stream_listen('App\\Entity\\Book') }}",
40+
[],
41+
'data-controller="symfony--ux-turbo--mercure-turbo-stream" data-symfony--ux-turbo--mercure-turbo-stream-hub-value="http://127.0.0.1:3000/.well-known/mercure" data-symfony--ux-turbo--mercure-turbo-stream-topic-value="AppEntityBook"',
42+
];
43+
44+
yield [
45+
'{{ turbo_stream_listen(book) }}',
46+
['book' => $book],
47+
'data-controller="symfony--ux-turbo--mercure-turbo-stream" data-symfony--ux-turbo--mercure-turbo-stream-hub-value="http://127.0.0.1:3000/.well-known/mercure" data-symfony--ux-turbo--mercure-turbo-stream-topic-value="https://symfony.com/ux-turbo/App%5CEntity%5CBook/123"',
48+
];
49+
50+
yield [
51+
"{{ turbo_stream_listen(['a_topic', 'App\\Entity\\Book', book]) }}",
52+
['book' => $book],
53+
'data-controller="symfony--ux-turbo--mercure-turbo-stream" data-symfony--ux-turbo--mercure-turbo-stream-hub-value="http://127.0.0.1:3000/.well-known/mercure" data-symfony--ux-turbo--mercure-turbo-stream-topics-value="[&quot;a_topic&quot;,&quot;AppEntityBook&quot;,&quot;https:\/\/symfony.com\/ux-turbo\/App%5CEntity%5CBook\/123&quot;]"',
54+
];
55+
}
56+
}

0 commit comments

Comments
 (0)