Skip to content

Commit 2295430

Browse files
Fan2ShrekKocal
authored andcommitted
[Turbo] Add support for authentication to the EventSource via turbo_stream_listen
1 parent a7dc6a0 commit 2295430

16 files changed

+233
-36
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## 2.24.0
44

55
- Add Twig Extensions for `meta` tags
6+
- Add support for authentication to the EventSource via `turbo_stream_listen`
67

78
## 2.22.0
89

assets/dist/turbo_stream_controller.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ export default class extends Controller {
44
topic: StringConstructor;
55
topics: ArrayConstructor;
66
hub: StringConstructor;
7+
withCredentials: BooleanConstructor;
78
};
89
es: EventSource | undefined;
910
url: string | undefined;
1011
readonly topicValue: string;
1112
readonly topicsValue: string[];
13+
readonly withCredentialsValue: boolean;
1214
readonly hubValue: string;
1315
readonly hasHubValue: boolean;
1416
readonly hasTopicValue: boolean;

assets/dist/turbo_stream_controller.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class default_1 extends Controller {
2323
}
2424
connect() {
2525
if (this.url) {
26-
this.es = new EventSource(this.url);
26+
this.es = new EventSource(this.url, { withCredentials: this.withCredentialsValue });
2727
connectStreamSource(this.es);
2828
}
2929
}
@@ -38,6 +38,7 @@ default_1.values = {
3838
topic: String,
3939
topics: Array,
4040
hub: String,
41+
withCredentials: Boolean,
4142
};
4243

4344
export { default_1 as default };

assets/src/turbo_stream_controller.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ export default class extends Controller {
1818
topic: String,
1919
topics: Array,
2020
hub: String,
21+
withCredentials: Boolean,
2122
};
2223
es: EventSource | undefined;
2324
url: string | undefined;
2425

2526
declare readonly topicValue: string;
2627
declare readonly topicsValue: string[];
28+
declare readonly withCredentialsValue: boolean;
2729
declare readonly hubValue: string;
2830
declare readonly hasHubValue: boolean;
2931
declare readonly hasTopicValue: boolean;
@@ -50,7 +52,7 @@ export default class extends Controller {
5052

5153
connect() {
5254
if (this.url) {
53-
this.es = new EventSource(this.url);
55+
this.es = new EventSource(this.url, { withCredentials: this.withCredentialsValue });
5456
connectStreamSource(this.es);
5557
}
5658
}

config/services.php

+8-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Symfony\UX\Turbo\Broadcaster\TwigBroadcaster;
1919
use Symfony\UX\Turbo\Doctrine\BroadcastListener;
2020
use Symfony\UX\Turbo\Request\RequestListener;
21+
use Symfony\UX\Turbo\Twig\TurboRuntime;
2122
use Symfony\UX\Turbo\Twig\TwigExtension;
2223

2324
/*
@@ -47,9 +48,15 @@
4748
->decorate('turbo.broadcaster.imux')
4849

4950
->set('turbo.twig.extension', TwigExtension::class)
50-
->args([tagged_locator('turbo.renderer.stream_listen', 'transport'), abstract_arg('default')])
5151
->tag('twig.extension')
5252

53+
->set('turbo.twig.runtime', TurboRuntime::class)
54+
->args([
55+
tagged_locator('turbo.renderer.stream_listen', 'transport'),
56+
abstract_arg('default_transport'),
57+
])
58+
->tag('twig.runtime')
59+
5360
->set('turbo.doctrine.event_listener', BroadcastListener::class)
5461
->args([
5562
service('turbo.broadcaster.imux'),

doc/index.rst

+3
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,9 @@ Let's create our chat::
754754
</turbo-frame>
755755
{% endblock %}
756756

757+
If you're using a private hub, you can add ``{ withCredentials: true }``
758+
as ``turbo_stream_listen()`` third argument to authenticate with the hub
759+
757760
.. code-block:: html+twig
758761

759762
{# chat/message.stream.html.twig #}

src/Bridge/Mercure/TurboStreamListenRenderer.php

+22-3
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,28 @@
1212
namespace Symfony\UX\Turbo\Bridge\Mercure;
1313

1414
use Symfony\Component\Mercure\HubInterface;
15+
use Symfony\Component\Mercure\Twig\MercureExtension;
1516
use Symfony\UX\StimulusBundle\Helper\StimulusHelper;
1617
use Symfony\UX\Turbo\Broadcaster\IdAccessor;
17-
use Symfony\UX\Turbo\Twig\TurboStreamListenRendererInterface;
18+
use Symfony\UX\Turbo\Twig\TurboStreamListenRendererWithOptionsInterface;
1819
use Symfony\WebpackEncoreBundle\Twig\StimulusTwigExtension;
1920
use Twig\Environment;
21+
use Twig\Error\RuntimeError;
2022

2123
/**
2224
* Renders the attributes to load the "mercure-turbo-stream" controller.
2325
*
2426
* @author Kévin Dunglas <[email protected]>
2527
*/
26-
final class TurboStreamListenRenderer implements TurboStreamListenRendererInterface
28+
final class TurboStreamListenRenderer implements TurboStreamListenRendererWithOptionsInterface
2729
{
2830
private StimulusHelper $stimulusHelper;
2931

3032
public function __construct(
3133
private HubInterface $hub,
3234
StimulusHelper|StimulusTwigExtension $stimulus,
3335
private IdAccessor $idAccessor,
36+
private Environment $twig,
3437
) {
3538
if ($stimulus instanceof StimulusTwigExtension) {
3639
trigger_deprecation('symfony/ux-turbo', '2.9', 'Passing an instance of "%s" as second argument of "%s" is deprecated, pass an instance of "%s" instead.', StimulusTwigExtension::class, __CLASS__, StimulusHelper::class);
@@ -42,8 +45,12 @@ public function __construct(
4245
$this->stimulusHelper = $stimulus;
4346
}
4447

45-
public function renderTurboStreamListen(Environment $env, $topic): string
48+
public function renderTurboStreamListen(Environment $env, $topic /* array $eventSourceOptions = [] */): string
4649
{
50+
if (\func_num_args() > 2) {
51+
$eventSourceOptions = func_get_arg(2);
52+
}
53+
4754
$topics = $topic instanceof TopicSet
4855
? array_map($this->resolveTopic(...), $topic->getTopics())
4956
: [$this->resolveTopic($topic)];
@@ -55,6 +62,18 @@ public function renderTurboStreamListen(Environment $env, $topic): string
5562
$controllerAttributes['topic'] = current($topics);
5663
}
5764

65+
if (isset($eventSourceOptions)) {
66+
try {
67+
$mercure = $this->twig->getExtension(MercureExtension::class);
68+
$mercure->mercure($topics, $eventSourceOptions);
69+
70+
if (isset($eventSourceOptions['withCredentials'])) {
71+
$controllerAttributes['withCredentials'] = $eventSourceOptions['withCredentials'];
72+
}
73+
} catch (RuntimeError $e) {
74+
}
75+
}
76+
5877
$stimulusAttributes = $this->stimulusHelper->createStimulusAttributes();
5978
$stimulusAttributes->addController(
6079
'symfony/ux-turbo/mercure-turbo-stream',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\DependencyInjection\Compiler;
13+
14+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
15+
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
use Symfony\Component\DependencyInjection\Reference;
17+
use Symfony\UX\Turbo\Bridge\Mercure\Broadcaster;
18+
use Symfony\UX\Turbo\Bridge\Mercure\TurboStreamListenRenderer;
19+
20+
/**
21+
* This compiler pass ensures that TurboStreamListenRenderer
22+
* and Broadcast are registered per Mercure hub.
23+
*
24+
* @author Pierre Ambroise<[email protected]>
25+
*/
26+
final class RegisterMercureHubsPass implements CompilerPassInterface
27+
{
28+
public function process(ContainerBuilder $container)
29+
{
30+
foreach ($container->findTaggedServiceIds('mercure.hub') as $hubId => $tags) {
31+
$name = str_replace('mercure.hub.', '', $hubId);
32+
33+
$container->register("turbo.mercure.$name.renderer", TurboStreamListenRenderer::class)
34+
->addArgument(new Reference($hubId))
35+
->addArgument(new Reference('turbo.mercure.stimulus_helper'))
36+
->addArgument(new Reference('turbo.id_accessor'))
37+
->addArgument(new Reference('twig'))
38+
->addTag('turbo.renderer.stream_listen', ['transport' => $name]);
39+
40+
foreach ($tags as $tag) {
41+
if (isset($tag['default']) && $tag['default'] && 'default' !== $name) {
42+
$container->getDefinition("turbo.mercure.$name.renderer")
43+
->addTag('turbo.renderer.stream_listen', ['transport' => 'default']);
44+
}
45+
}
46+
47+
$container->register("turbo.mercure.$name.broadcaster", Broadcaster::class)
48+
->addArgument($name)
49+
->addArgument(new Reference($hubId))
50+
->addTag('turbo.broadcaster');
51+
}
52+
}
53+
}

src/DependencyInjection/TurboExtension.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public function load(array $configs, ContainerBuilder $container): void
3737

3838
$loader = (new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/../config')));
3939
$loader->load('services.php');
40-
$container->getDefinition('turbo.twig.extension')->replaceArgument(1, $config['default_transport']);
40+
$container->getDefinition('turbo.twig.runtime')->replaceArgument(1, $config['default_transport']);
4141

4242
$this->registerTwig($config, $container);
4343
$this->registerBroadcast($config, $container, $loader);

src/TurboBundle.php

+3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
1616
use Symfony\Component\DependencyInjection\ContainerBuilder;
1717
use Symfony\Component\HttpKernel\Bundle\Bundle;
18+
use Symfony\UX\Turbo\DependencyInjection\Compiler\RegisterMercureHubsPass;
1819

1920
/**
2021
* @author Kévin Dunglas <[email protected]>
@@ -28,6 +29,8 @@ public function build(ContainerBuilder $container): void
2829
{
2930
parent::build($container);
3031

32+
$container->addCompilerPass(new RegisterMercureHubsPass());
33+
3134
$container->addCompilerPass(new class implements CompilerPassInterface {
3235
public function process(ContainerBuilder $container): void
3336
{

src/Twig/TurboRuntime.php

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Twig;
13+
14+
use Psr\Container\ContainerInterface;
15+
use Symfony\UX\Turbo\Bridge\Mercure\TopicSet;
16+
use Twig\Environment;
17+
use Twig\Extension\RuntimeExtensionInterface;
18+
19+
/**
20+
* @author Kévin Dunglas <[email protected]>
21+
* @author Pierre Ambroise <[email protected]>
22+
*
23+
* @internal
24+
*/
25+
final class TurboRuntime implements RuntimeExtensionInterface
26+
{
27+
public function __construct(
28+
private ContainerInterface $turboStreamListenRenderers,
29+
private readonly string $defaultTransport,
30+
) {
31+
}
32+
33+
/**
34+
* @param object|string|array<object|string> $topic
35+
* @param array<string, mixed> $options
36+
*/
37+
public function renderTurboStreamListen(Environment $env, $topic, ?string $transport = null, array $options = []): string
38+
{
39+
$options['transport'] = $transport ??= $this->defaultTransport;
40+
41+
if (!$this->turboStreamListenRenderers->has($transport)) {
42+
throw new \InvalidArgumentException(\sprintf('The Turbo stream transport "%s" does not exist.', $transport));
43+
}
44+
45+
if (\is_array($topic)) {
46+
$topic = new TopicSet($topic);
47+
}
48+
49+
$renderer = $this->turboStreamListenRenderers->get($transport);
50+
51+
return $renderer instanceof TurboStreamListenRendererWithOptionsInterface
52+
? $renderer->renderTurboStreamListen($env, $topic, $options) // @phpstan-ignore-line
53+
: $renderer->renderTurboStreamListen($env, $topic);
54+
}
55+
}

src/Twig/TurboStreamListenRendererInterface.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,5 @@ interface TurboStreamListenRendererInterface
2323
/**
2424
* @param string|object $topic
2525
*/
26-
public function renderTurboStreamListen(Environment $env, $topic): string;
26+
public function renderTurboStreamListen(Environment $env, $topic /* , array $eventSourceOptions = [] */): string;
2727
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Twig;
13+
14+
/**
15+
* @internal
16+
*/
17+
interface TurboStreamListenRendererWithOptionsInterface extends TurboStreamListenRendererInterface
18+
{
19+
}

src/Twig/TwigExtension.php

+1-28
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,6 @@
1111

1212
namespace Symfony\UX\Turbo\Twig;
1313

14-
use Psr\Container\ContainerInterface;
15-
use Symfony\UX\Turbo\Bridge\Mercure\TopicSet;
16-
use Twig\Environment;
1714
use Twig\Extension\AbstractExtension;
1815
use Twig\TwigFunction;
1916

@@ -28,16 +25,10 @@ final class TwigExtension extends AbstractExtension
2825
private const REFRESH_SCROLL_RESET = 'reset';
2926
private const REFRESH_SCROLL_PRESERVE = 'preserve';
3027

31-
public function __construct(
32-
private ContainerInterface $turboStreamListenRenderers,
33-
private string $default,
34-
) {
35-
}
36-
3728
public function getFunctions(): array
3829
{
3930
return [
40-
new TwigFunction('turbo_stream_listen', $this->turboStreamListen(...), ['needs_environment' => true, 'is_safe' => ['html']]),
31+
new TwigFunction('turbo_stream_listen', [TurboRuntime::class, 'renderTurboStreamListen'], ['needs_environment' => true, 'is_safe' => ['html']]),
4132
new TwigFunction('turbo_exempts_page_from_cache', $this->turboExemptsPageFromCache(...), ['is_safe' => ['html']]),
4233
new TwigFunction('turbo_exempts_page_from_preview', $this->turboExemptsPageFromPreview(...), ['is_safe' => ['html']]),
4334
new TwigFunction('turbo_page_requires_reload', $this->turboPageRequiresReload(...), ['is_safe' => ['html']]),
@@ -47,24 +38,6 @@ public function getFunctions(): array
4738
];
4839
}
4940

50-
/**
51-
* @param object|string|array<object|string> $topic
52-
*/
53-
public function turboStreamListen(Environment $env, $topic, ?string $transport = null): string
54-
{
55-
$transport ??= $this->default;
56-
57-
if (!$this->turboStreamListenRenderers->has($transport)) {
58-
throw new \InvalidArgumentException(\sprintf('The Turbo stream transport "%s" does not exist.', $transport));
59-
}
60-
61-
if (\is_array($topic)) {
62-
$topic = new TopicSet($topic);
63-
}
64-
65-
return $this->turboStreamListenRenderers->get($transport)->renderTurboStreamListen($env, $topic);
66-
}
67-
6841
/**
6942
* Generates a <meta> tag to disable caching of a page.
7043
*

tests/Bridge/Mercure/TurboStreamListenRendererTest.php

+8
Original file line numberDiff line numberDiff line change
@@ -71,5 +71,13 @@ public static function provideTestCases(): iterable
7171
? '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;]"'
7272
: 'data-controller="symfony--ux-turbo--mercure-turbo-stream" data-symfony--ux-turbo--mercure-turbo-stream-hub-value="http&#x3A;&#x2F;&#x2F;127.0.0.1&#x3A;3000&#x2F;.well-known&#x2F;mercure" data-symfony--ux-turbo--mercure-turbo-stream-topics-value="&#x5B;&quot;a_topic&quot;,&quot;AppEntityBook&quot;,&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;symfony.com&#x5C;&#x2F;ux-turbo&#x5C;&#x2F;App&#x25;5CEntity&#x25;5CBook&#x5C;&#x2F;123&quot;&#x5D;"',
7373
];
74+
75+
yield [
76+
"{{ turbo_stream_listen('a_topic', 'default', { withCredentials: true }) }}",
77+
[],
78+
$newEscape
79+
? '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" data-symfony--ux-turbo--mercure-turbo-stream-with-credentials-value="true"'
80+
: 'data-controller="symfony--ux-turbo--mercure-turbo-stream" data-symfony--ux-turbo--mercure-turbo-stream-hub-value="http&#x3A;&#x2F;&#x2F;127.0.0.1&#x3A;3000&#x2F;.well-known&#x2F;mercure" data-symfony--ux-turbo--mercure-turbo-stream-topic-value="a_topic" data-symfony--ux-turbo--mercure-turbo-stream-with-credentials-value="true"',
81+
];
7482
}
7583
}

0 commit comments

Comments
 (0)