From abcc723b32d30cf0954406c65883a10f596706e2 Mon Sep 17 00:00:00 2001 From: Fan2Shrek Date: Thu, 12 Dec 2024 22:21:24 +0100 Subject: [PATCH] Add options to turbo stream --- src/Turbo/CHANGELOG.md | 4 ++ .../assets/dist/turbo_stream_controller.d.ts | 2 + .../assets/dist/turbo_stream_controller.js | 3 +- .../assets/src/turbo_stream_controller.ts | 4 +- src/Turbo/config/services.php | 12 +++- .../Mercure/TurboStreamListenRenderer.php | 10 ++- .../DependencyInjection/TurboExtension.php | 2 +- src/Turbo/src/Twig/TurboRuntime.php | 71 +++++++++++++++++++ .../TurboStreamListenRendererInterface.php | 2 +- src/Turbo/src/Twig/TwigExtension.php | 31 +------- .../Mercure/TurboStreamListenRendererTest.php | 8 +++ 11 files changed, 115 insertions(+), 34 deletions(-) create mode 100644 src/Turbo/src/Twig/TurboRuntime.php diff --git a/src/Turbo/CHANGELOG.md b/src/Turbo/CHANGELOG.md index 5a0f7d0f4f5..81c6a4c0ed6 100644 --- a/src/Turbo/CHANGELOG.md +++ b/src/Turbo/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +## 2.23.0 + +- Add support for providing options to the EventSource via `turbo_stream_listen` + ## 2.22.0 - Add `` component diff --git a/src/Turbo/assets/dist/turbo_stream_controller.d.ts b/src/Turbo/assets/dist/turbo_stream_controller.d.ts index 2806afea3cc..cc4db88a562 100644 --- a/src/Turbo/assets/dist/turbo_stream_controller.d.ts +++ b/src/Turbo/assets/dist/turbo_stream_controller.d.ts @@ -4,11 +4,13 @@ export default class extends Controller { topic: StringConstructor; topics: ArrayConstructor; hub: StringConstructor; + withCredentials: BooleanConstructor; }; es: EventSource | undefined; url: string | undefined; readonly topicValue: string; readonly topicsValue: string[]; + readonly withCredentialsValue: boolean; readonly hubValue: string; readonly hasHubValue: boolean; readonly hasTopicValue: boolean; diff --git a/src/Turbo/assets/dist/turbo_stream_controller.js b/src/Turbo/assets/dist/turbo_stream_controller.js index 3d55567c772..d5962232feb 100644 --- a/src/Turbo/assets/dist/turbo_stream_controller.js +++ b/src/Turbo/assets/dist/turbo_stream_controller.js @@ -23,7 +23,7 @@ class default_1 extends Controller { } connect() { if (this.url) { - this.es = new EventSource(this.url); + this.es = new EventSource(this.url, { withCredentials: this.withCredentialsValue }); connectStreamSource(this.es); } } @@ -38,6 +38,7 @@ default_1.values = { topic: String, topics: Array, hub: String, + withCredentials: Boolean, }; export { default_1 as default }; diff --git a/src/Turbo/assets/src/turbo_stream_controller.ts b/src/Turbo/assets/src/turbo_stream_controller.ts index 4c8fd4d915a..aaa19c78396 100644 --- a/src/Turbo/assets/src/turbo_stream_controller.ts +++ b/src/Turbo/assets/src/turbo_stream_controller.ts @@ -18,12 +18,14 @@ export default class extends Controller { topic: String, topics: Array, hub: String, + withCredentials: Boolean, }; es: EventSource | undefined; url: string | undefined; declare readonly topicValue: string; declare readonly topicsValue: string[]; + declare readonly withCredentialsValue: boolean; declare readonly hubValue: string; declare readonly hasHubValue: boolean; declare readonly hasTopicValue: boolean; @@ -50,7 +52,7 @@ export default class extends Controller { connect() { if (this.url) { - this.es = new EventSource(this.url); + this.es = new EventSource(this.url, { withCredentials: this.withCredentialsValue }); connectStreamSource(this.es); } } diff --git a/src/Turbo/config/services.php b/src/Turbo/config/services.php index 0cd1e6a1d5f..ba88ba6fb9b 100644 --- a/src/Turbo/config/services.php +++ b/src/Turbo/config/services.php @@ -11,6 +11,8 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; +use Symfony\Component\Mercure\Authorization; +use Symfony\UX\Turbo\Twig\TurboRuntime; use Symfony\UX\Turbo\Broadcaster\BroadcasterInterface; use Symfony\UX\Turbo\Broadcaster\IdAccessor; use Symfony\UX\Turbo\Broadcaster\ImuxBroadcaster; @@ -45,9 +47,17 @@ ->decorate('turbo.broadcaster.imux') ->set('turbo.twig.extension', TwigExtension::class) - ->args([tagged_locator('turbo.renderer.stream_listen', 'transport'), abstract_arg('default')]) ->tag('twig.extension') + ->set('turbo.twig.runtime', TurboRuntime::class) + ->args([ + tagged_locator('turbo.renderer.stream_listen', 'transport'), + abstract_arg('default'), + service(Authorization::class)->nullOnInvalid(), + service('request_stack')->nullOnInvalid(), + ]) + ->tag('twig.runtime') + ->set('turbo.doctrine.event_listener', BroadcastListener::class) ->args([ service('turbo.broadcaster.imux'), diff --git a/src/Turbo/src/Bridge/Mercure/TurboStreamListenRenderer.php b/src/Turbo/src/Bridge/Mercure/TurboStreamListenRenderer.php index 68eadd82079..249038c6480 100644 --- a/src/Turbo/src/Bridge/Mercure/TurboStreamListenRenderer.php +++ b/src/Turbo/src/Bridge/Mercure/TurboStreamListenRenderer.php @@ -42,8 +42,12 @@ public function __construct( $this->stimulusHelper = $stimulus; } - public function renderTurboStreamListen(Environment $env, $topic): string + public function renderTurboStreamListen(Environment $env, $topic /* array $eventSourceOptions = [] */): string { + if (\func_num_args() > 2) { + $eventSourceOptions = func_get_arg(2); + } + $topics = $topic instanceof TopicSet ? array_map($this->resolveTopic(...), $topic->getTopics()) : [$this->resolveTopic($topic)]; @@ -55,6 +59,10 @@ public function renderTurboStreamListen(Environment $env, $topic): string $controllerAttributes['topic'] = current($topics); } + if (isset($eventSourceOptions, $eventSourceOptions['withCredentials'])) { + $controllerAttributes['withCredentials'] = $eventSourceOptions['withCredentials']; + } + $stimulusAttributes = $this->stimulusHelper->createStimulusAttributes(); $stimulusAttributes->addController( 'symfony/ux-turbo/mercure-turbo-stream', diff --git a/src/Turbo/src/DependencyInjection/TurboExtension.php b/src/Turbo/src/DependencyInjection/TurboExtension.php index ca53f6d2b09..761d8e4b27e 100644 --- a/src/Turbo/src/DependencyInjection/TurboExtension.php +++ b/src/Turbo/src/DependencyInjection/TurboExtension.php @@ -37,7 +37,7 @@ public function load(array $configs, ContainerBuilder $container): void $loader = (new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/../config'))); $loader->load('services.php'); - $container->getDefinition('turbo.twig.extension')->replaceArgument(1, $config['default_transport']); + $container->getDefinition('turbo.twig.runtime')->replaceArgument(1, $config['default_transport']); $this->registerTwig($config, $container); $this->registerBroadcast($config, $container, $loader); diff --git a/src/Turbo/src/Twig/TurboRuntime.php b/src/Turbo/src/Twig/TurboRuntime.php new file mode 100644 index 00000000000..ba37e9fcbfc --- /dev/null +++ b/src/Turbo/src/Twig/TurboRuntime.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Turbo\Twig; + +use Psr\Container\ContainerInterface; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Mercure\Authorization; +use Symfony\UX\Turbo\Bridge\Mercure\TopicSet; +use Twig\Extension\RuntimeExtensionInterface; +use Twig\Environment; + +/** + * @author Pierre Ambroise + * + * @internal + */ +class TurboRuntime implements RuntimeExtensionInterface +{ + public function __construct( + private ContainerInterface $turboStreamListenRenderers, + private string $default, + private ?Authorization $authorization = null, + private ?RequestStack $requestStack = null, + ) { + } + + /** + * @param object|string|array $topic + * @param array $options + */ + public function renderTurboStreamListen(Environment $env, $topic, ?string $transport = null, array $options = []): string + { + $transport ??= $this->default; + + if (!$this->turboStreamListenRenderers->has($transport)) { + throw new \InvalidArgumentException(\sprintf('The Turbo stream transport "%s" does not exist.', $transport)); + } + + if (\is_array($topic)) { + $topic = new TopicSet($topic); + } + + if ( + null !== $this->authorization + && null !== $this->requestStack + && (isset($options['subscribe']) || isset($options['publish']) || isset($options['additionalClaims'])) + && null !== $request = $this->requestStack->getMainRequest() + ) { + $this->authorization->setCookie( + $request, + $options['subscribe'] ?? [], + $options['publish'] ?? [], + $options['additionalClaims'] ?? [], + $transport, + ); + + unset($options['subscribe'], $options['publish'], $options['additionalClaims']); + } + + return $this->turboStreamListenRenderers->get($transport)->renderTurboStreamListen($env, $topic, $options); + } +} diff --git a/src/Turbo/src/Twig/TurboStreamListenRendererInterface.php b/src/Turbo/src/Twig/TurboStreamListenRendererInterface.php index 3670e40bd28..240721317f1 100644 --- a/src/Turbo/src/Twig/TurboStreamListenRendererInterface.php +++ b/src/Turbo/src/Twig/TurboStreamListenRendererInterface.php @@ -23,5 +23,5 @@ interface TurboStreamListenRendererInterface /** * @param string|object $topic */ - public function renderTurboStreamListen(Environment $env, $topic): string; + public function renderTurboStreamListen(Environment $env, $topic /* , array $eventSourceOptions = [] */): string; } diff --git a/src/Turbo/src/Twig/TwigExtension.php b/src/Turbo/src/Twig/TwigExtension.php index b44d993139f..a32c29adbed 100644 --- a/src/Turbo/src/Twig/TwigExtension.php +++ b/src/Turbo/src/Twig/TwigExtension.php @@ -11,45 +11,20 @@ namespace Symfony\UX\Turbo\Twig; -use Psr\Container\ContainerInterface; -use Symfony\UX\Turbo\Bridge\Mercure\TopicSet; -use Twig\Environment; +use Symfony\UX\Turbo\Twig\TurboRuntime; use Twig\Extension\AbstractExtension; use Twig\TwigFunction; /** * @author Kévin Dunglas + * @author Pierre Ambroise */ final class TwigExtension extends AbstractExtension { - public function __construct( - private ContainerInterface $turboStreamListenRenderers, - private string $default, - ) { - } - public function getFunctions(): array { return [ - new TwigFunction('turbo_stream_listen', $this->turboStreamListen(...), ['needs_environment' => true, 'is_safe' => ['html']]), + new TwigFunction('turbo_stream_listen', [TurboRuntime::class, 'renderTurboStreamListen'], ['needs_environment' => true, 'is_safe' => ['html']]), ]; } - - /** - * @param object|string|array $topic - */ - public function turboStreamListen(Environment $env, $topic, ?string $transport = null): string - { - $transport ??= $this->default; - - if (!$this->turboStreamListenRenderers->has($transport)) { - throw new \InvalidArgumentException(\sprintf('The Turbo stream transport "%s" does not exist.', $transport)); - } - - if (\is_array($topic)) { - $topic = new TopicSet($topic); - } - - return $this->turboStreamListenRenderers->get($transport)->renderTurboStreamListen($env, $topic); - } } diff --git a/src/Turbo/tests/Bridge/Mercure/TurboStreamListenRendererTest.php b/src/Turbo/tests/Bridge/Mercure/TurboStreamListenRendererTest.php index 9b19ba4db09..108715b06f8 100644 --- a/src/Turbo/tests/Bridge/Mercure/TurboStreamListenRendererTest.php +++ b/src/Turbo/tests/Bridge/Mercure/TurboStreamListenRendererTest.php @@ -71,5 +71,13 @@ public static function provideTestCases(): iterable ? '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="["a_topic","AppEntityBook","https:\/\/symfony.com\/ux-turbo\/App%5CEntity%5CBook\/123"]"' : '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="["a_topic","AppEntityBook","https:\/\/symfony.com\/ux-turbo\/App%5CEntity%5CBook\/123"]"', ]; + + yield [ + "{{ turbo_stream_listen('a_topic', 'default', { withCredentials: true }) }}", + [], + $newEscape + ? '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"' + : '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"', + ]; } }