From e95cbabc87be75174e8626d4634f4b7f82e32138 Mon Sep 17 00:00:00 2001 From: Fan2Shrek Date: Thu, 12 Dec 2024 22:21:24 +0100 Subject: [PATCH 1/2] 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 | 30 +------- .../Mercure/TurboStreamListenRendererTest.php | 8 +++ 11 files changed, 114 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..a9de14c32e9 100644 --- a/src/Turbo/config/services.php +++ b/src/Turbo/config/services.php @@ -11,11 +11,13 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; +use Symfony\Component\Mercure\Authorization; use Symfony\UX\Turbo\Broadcaster\BroadcasterInterface; use Symfony\UX\Turbo\Broadcaster\IdAccessor; use Symfony\UX\Turbo\Broadcaster\ImuxBroadcaster; use Symfony\UX\Turbo\Broadcaster\TwigBroadcaster; use Symfony\UX\Turbo\Doctrine\BroadcastListener; +use Symfony\UX\Turbo\Twig\TurboRuntime; use Symfony\UX\Turbo\Twig\TwigExtension; /* @@ -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..681a3627a1b --- /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\Environment; +use Twig\Extension\RuntimeExtensionInterface; + +/** + * @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..55e785968ac 100644 --- a/src/Turbo/src/Twig/TwigExtension.php +++ b/src/Turbo/src/Twig/TwigExtension.php @@ -11,45 +11,19 @@ namespace Symfony\UX\Turbo\Twig; -use Psr\Container\ContainerInterface; -use Symfony\UX\Turbo\Bridge\Mercure\TopicSet; -use Twig\Environment; 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"', + ]; } } From e56b74ca63e52ac2ce9311327c2ac0e3c42ec69f Mon Sep 17 00:00:00 2001 From: Fan2Shrek Date: Sat, 14 Dec 2024 23:46:22 +0100 Subject: [PATCH 2/2] Try to prevent BC --- src/Turbo/config/services.php | 3 -- .../Mercure/TurboStreamListenRenderer.php | 31 ++++++++++++++++--- src/Turbo/src/Twig/TurboRuntime.php | 27 +++------------- ...reamListenRendererWithOptionsInterface.php | 19 ++++++++++++ src/Turbo/src/Twig/TwigExtension.php | 1 - 5 files changed, 51 insertions(+), 30 deletions(-) create mode 100644 src/Turbo/src/Twig/TurboStreamListenRendererWithOptionsInterface.php diff --git a/src/Turbo/config/services.php b/src/Turbo/config/services.php index a9de14c32e9..2eaf1f27e6c 100644 --- a/src/Turbo/config/services.php +++ b/src/Turbo/config/services.php @@ -11,7 +11,6 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; -use Symfony\Component\Mercure\Authorization; use Symfony\UX\Turbo\Broadcaster\BroadcasterInterface; use Symfony\UX\Turbo\Broadcaster\IdAccessor; use Symfony\UX\Turbo\Broadcaster\ImuxBroadcaster; @@ -53,8 +52,6 @@ ->args([ tagged_locator('turbo.renderer.stream_listen', 'transport'), abstract_arg('default'), - service(Authorization::class)->nullOnInvalid(), - service('request_stack')->nullOnInvalid(), ]) ->tag('twig.runtime') diff --git a/src/Turbo/src/Bridge/Mercure/TurboStreamListenRenderer.php b/src/Turbo/src/Bridge/Mercure/TurboStreamListenRenderer.php index 249038c6480..b9123ba3cdb 100644 --- a/src/Turbo/src/Bridge/Mercure/TurboStreamListenRenderer.php +++ b/src/Turbo/src/Bridge/Mercure/TurboStreamListenRenderer.php @@ -11,10 +11,12 @@ namespace Symfony\UX\Turbo\Bridge\Mercure; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Mercure\Authorization; use Symfony\Component\Mercure\HubInterface; use Symfony\UX\StimulusBundle\Helper\StimulusHelper; use Symfony\UX\Turbo\Broadcaster\IdAccessor; -use Symfony\UX\Turbo\Twig\TurboStreamListenRendererInterface; +use Symfony\UX\Turbo\Twig\TurboStreamListenRendererWithOptionsInterface; use Symfony\WebpackEncoreBundle\Twig\StimulusTwigExtension; use Twig\Environment; @@ -23,7 +25,7 @@ * * @author Kévin Dunglas */ -final class TurboStreamListenRenderer implements TurboStreamListenRendererInterface +final class TurboStreamListenRenderer implements TurboStreamListenRendererWithOptionsInterface { private StimulusHelper $stimulusHelper; @@ -31,6 +33,8 @@ public function __construct( private HubInterface $hub, StimulusHelper|StimulusTwigExtension $stimulus, private IdAccessor $idAccessor, + private ?Authorization $authorization = null, + private ?RequestStack $requestStack = null, ) { if ($stimulus instanceof StimulusTwigExtension) { 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); @@ -59,8 +63,27 @@ public function renderTurboStreamListen(Environment $env, $topic /* array $event $controllerAttributes['topic'] = current($topics); } - if (isset($eventSourceOptions, $eventSourceOptions['withCredentials'])) { - $controllerAttributes['withCredentials'] = $eventSourceOptions['withCredentials']; + if (isset($eventSourceOptions)) { + if ( + null !== $this->authorization + && null !== $this->requestStack + && (isset($eventSourceOptions['subscribe']) || isset($eventSourceOptions['publish']) || isset($eventSourceOptions['additionalClaims'])) + && null !== $request = $this->requestStack->getMainRequest() + ) { + $this->authorization->setCookie( + $request, + $eventSourceOptions['subscribe'] ?? [], + $eventSourceOptions['publish'] ?? [], + $eventSourceOptions['additionalClaims'] ?? [], + $eventSourceOptions['transport'] ?? null, + ); + + unset($eventSourceOptions['subscribe'], $eventSourceOptions['publish'], $eventSourceOptions['additionalClaims'], $eventSourceOptions['transport']); + } + + if (isset($eventSourceOptions['withCredentials'])) { + $controllerAttributes['withCredentials'] = $eventSourceOptions['withCredentials']; + } } $stimulusAttributes = $this->stimulusHelper->createStimulusAttributes(); diff --git a/src/Turbo/src/Twig/TurboRuntime.php b/src/Turbo/src/Twig/TurboRuntime.php index 681a3627a1b..2f512100b38 100644 --- a/src/Turbo/src/Twig/TurboRuntime.php +++ b/src/Turbo/src/Twig/TurboRuntime.php @@ -12,8 +12,6 @@ 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\Environment; use Twig\Extension\RuntimeExtensionInterface; @@ -28,8 +26,6 @@ class TurboRuntime implements RuntimeExtensionInterface public function __construct( private ContainerInterface $turboStreamListenRenderers, private string $default, - private ?Authorization $authorization = null, - private ?RequestStack $requestStack = null, ) { } @@ -39,7 +35,7 @@ public function __construct( */ public function renderTurboStreamListen(Environment $env, $topic, ?string $transport = null, array $options = []): string { - $transport ??= $this->default; + $options['transport'] = $transport ??= $this->default; if (!$this->turboStreamListenRenderers->has($transport)) { throw new \InvalidArgumentException(\sprintf('The Turbo stream transport "%s" does not exist.', $transport)); @@ -49,23 +45,10 @@ public function renderTurboStreamListen(Environment $env, $topic, ?string $trans $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, - ); + $renderer = $this->turboStreamListenRenderers->get($transport); - unset($options['subscribe'], $options['publish'], $options['additionalClaims']); - } - - return $this->turboStreamListenRenderers->get($transport)->renderTurboStreamListen($env, $topic, $options); + return $renderer instanceof TurboStreamListenRendererWithOptionsInterface + ? $renderer->renderTurboStreamListen($env, $topic, $options) // @phpstan-ignore-line + : $renderer->renderTurboStreamListen($env, $topic); } } diff --git a/src/Turbo/src/Twig/TurboStreamListenRendererWithOptionsInterface.php b/src/Turbo/src/Twig/TurboStreamListenRendererWithOptionsInterface.php new file mode 100644 index 00000000000..6364fe3b97b --- /dev/null +++ b/src/Turbo/src/Twig/TurboStreamListenRendererWithOptionsInterface.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Turbo\Twig; + +/** + * @internal + */ +interface TurboStreamListenRendererWithOptionsInterface extends TurboStreamListenRendererInterface +{ +} diff --git a/src/Turbo/src/Twig/TwigExtension.php b/src/Turbo/src/Twig/TwigExtension.php index 55e785968ac..789164254f5 100644 --- a/src/Turbo/src/Twig/TwigExtension.php +++ b/src/Turbo/src/Twig/TwigExtension.php @@ -16,7 +16,6 @@ /** * @author Kévin Dunglas - * @author Pierre Ambroise */ final class TwigExtension extends AbstractExtension {