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..2eaf1f27e6c 100644 --- a/src/Turbo/config/services.php +++ b/src/Turbo/config/services.php @@ -16,6 +16,7 @@ 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 +46,15 @@ ->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'), + ]) + ->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..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); @@ -42,8 +46,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 +63,29 @@ public function renderTurboStreamListen(Environment $env, $topic): string $controllerAttributes['topic'] = current($topics); } + 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(); $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..2f512100b38 --- /dev/null +++ b/src/Turbo/src/Twig/TurboRuntime.php @@ -0,0 +1,54 @@ + + * + * 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\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, + ) { + } + + /** + * @param object|string|array $topic + * @param array $options + */ + public function renderTurboStreamListen(Environment $env, $topic, ?string $transport = null, array $options = []): string + { + $options['transport'] = $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); + } + + $renderer = $this->turboStreamListenRenderers->get($transport); + + return $renderer instanceof TurboStreamListenRendererWithOptionsInterface + ? $renderer->renderTurboStreamListen($env, $topic, $options) // @phpstan-ignore-line + : $renderer->renderTurboStreamListen($env, $topic); + } +} 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/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 b44d993139f..789164254f5 100644 --- a/src/Turbo/src/Twig/TwigExtension.php +++ b/src/Turbo/src/Twig/TwigExtension.php @@ -11,9 +11,6 @@ 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; @@ -22,34 +19,10 @@ */ 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"', + ]; } }