### Install Symfony UX Turbo Source: https://symfony.com/bundles/ux-turbo/current/index Installs the symfony/ux-turbo bundle using Composer. This command fetches and installs the necessary package and its dependencies. It assumes you are using Composer for package management in your Symfony project. ```bash composer require symfony/ux-turbo ``` -------------------------------- ### Install Mercure Bundle for Chat Functionality Source: https://symfony.com/bundles/ux-turbo/current/index This command installs the Mercure bundle, a prerequisite for enabling asynchronous HTML updates to all connected clients using the Mercure protocol. This is part of setting up a real-time chat system with Symfony UX Turbo. ```bash $ composer require symfony/mercure-bundle ``` -------------------------------- ### Install JavaScript Assets with WebpackEncore Source: https://symfony.com/bundles/ux-turbo/current/index Installs JavaScript assets using npm and restarts the Webpack Encore development server. This is necessary when using WebpackEncore to ensure the new Turbo Drive JavaScript assets are available. Not required if using AssetMapper. ```bash npm install --force npm run watch ``` -------------------------------- ### Listen to Entity Updates in Twig Source: https://symfony.com/bundles/ux-turbo/current/index These examples show how to use the `turbo_stream_listen()` Twig helper to subscribe to real-time updates for Doctrine entities. You can listen to a specific entity instance by passing the entity object, or listen to all entities of a specific class by passing its Fully Qualified Class Name (FQCN). This enables dynamic updates in your frontend. ```twig
``` ```twig
``` -------------------------------- ### Lazy Loading Content with Turbo Frames in Twig Source: https://symfony.com/bundles/ux-turbo/current/index Turbo Frames can be used to lazy load content by specifying a `src` attribute on the `` tag. This causes the browser to fetch the content from the specified URL and insert it into the frame when the page loads. The `home.html.twig` example shows a Turbo Frame with a `src` attribute, acting as a placeholder. ```twig {# home.html.twig #} {% extends 'base.html.twig' %} {% block body %} A placeholder. {% endblock %} ``` -------------------------------- ### Using Turbo Frames for Scoped Page Updates in Twig Source: https://symfony.com/bundles/ux-turbo/current/index Turbo Frames allow for partial page updates by scoping content within `` tags. When a link within a frame is clicked, only the content inside that frame is updated. The `home.html.twig` example shows a basic Turbo Frame, and `another-page.html.twig` demonstrates how its content replaces the existing frame's content. ```twig {# home.html.twig #} {% extends 'base.html.twig' %} {% block body %} This block is scoped, the rest of the page will not change if you click here! {% endblock %} ``` ```twig {# another-page.html.twig #} {% extends 'base.html.twig' %} {% block body %}
This will be discarded
The content of this block will replace the content of the Turbo Frame! The rest of the HTML generated by this template (outside of the Turbo Frame) will be ignored. {% endblock %} ``` -------------------------------- ### Manual Form Response Handling for Older Symfony Versions with Turbo Drive Source: https://symfony.com/bundles/ux-turbo/current/index For Symfony versions prior to 6.2, manual adjustment is required to return a 422 status code on form validation errors when using Turbo Drive. This involves explicitly creating a `Response` object with the appropriate status code based on the form's submission and validity. The example shows how to conditionally set the status code. ```php #[Route('/product/new')] public function newProduct(Request $request): Response { $form = $this->createForm(ProductFormType::class); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { // save... } $response = new Response(null, $form->isSubmitted() ? 422 : 200); return $this->render('product/new.html.twig', [ 'form' => $form->createView() ], $response); } ``` -------------------------------- ### Adding Values to Submit Buttons for Turbo Drive Source: https://symfony.com/bundles/ux-turbo/current/index When a form has multiple submit buttons, Turbo Drive requires each button to have a `value` attribute to correctly identify which button was clicked. This ensures that the controller can adapt its program flow based on the specific button pressed. The example demonstrates adding a `value` to submit buttons within a form builder. ```php $builder // ... ->add('save', SubmitType::class, [ 'label' => 'Create Task', 'attr' => [ 'value' => 'create-task' ] ]) ->add('saveAndAdd', SubmitType::class, [ 'label' => 'Save and Add', 'attr' => [ 'value' => 'save-and-add' ] ]); ``` -------------------------------- ### PHP: Testing Turbo Frames with Symfony Panther Source: https://symfony.com/bundles/ux-turbo/current/index This PHP code snippet demonstrates how to write UI tests for Turbo Frames using Symfony Panther. It sets up a Panther client, navigates to a page, interacts with a link, and asserts that specific content appears on the page, simulating user interaction in a real browser. ```php // tests/TurboFrameTest.php namespace App\Tests; use Symfony\Component\Panther\PantherTestCase; class TurboFrameTest extends PantherTestCase { public function testFrame(): void { $client = self::createPantherClient(); $client->request('GET', '/'); $client->clickLink('This block is scoped, the rest of the page will not change if you click here!'); $this->assertSelectorWillContain('body', 'This will replace the content of the Turbo Frame!'); } } ``` -------------------------------- ### Multiple Mercure Hubs Configuration (YAML) Source: https://symfony.com/bundles/ux-turbo/current/index Shows how to configure multiple Mercure hubs within the `mercure.yaml` configuration file. This allows for using different Mercure instances for broadcasting updates, providing flexibility in deployment and scaling. ```yaml # config/packages/mercure.yaml mercure: hubs: hub1: url: https://hub1.example.net/.well-known/mercure jwt: snip hub2: url: https://hub2.example.net/.well-known/mercure jwt: snip ``` -------------------------------- ### Publishing Mercure Updates Programmatically (PHP) Source: https://symfony.com/bundles/ux-turbo/current/index Illustrates how to programmatically publish Mercure updates using the `HubInterface`. This is useful for sending custom updates or when direct entity broadcasting is not sufficient. It demonstrates injecting the `HubInterface` and calling the `publish` method. ```php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Mercure\HubInterface; use Symfony\Component\Mercure\Update; class MyController extends AbstractController { public function publish(HubInterface $hub1): Response { $id = $hub1->publish(new Update('topic', 'content')); return new Response("Update #{$id} published."); } } ``` -------------------------------- ### Configure Mercure Hubs in Symfony Source: https://symfony.com/bundles/ux-turbo/current/index This YAML configuration sets up the default Mercure hub, specifying its public and private URLs, and JWT credentials for publishing. Ensure these environment variables are correctly set in your .env file for the application to connect to the Mercure hub. ```yaml mercure: hubs: default: url: '%env(MERCURE_URL)%' public_url: '%env(MERCURE_PUBLIC_URL)%' jwt: secret: '%env(MERCURE_JWT_SECRET)%' publish: '*' ``` -------------------------------- ### PHP Controller for Form Reset with Turbo Streams Source: https://symfony.com/bundles/ux-turbo/current/index This PHP controller snippet demonstrates how to handle form submissions and prepare an empty form instance to be passed into the Turbo stream response. It clones the initial form to create an empty version that will be rendered in the stream template for resetting. ```php // src/Controller/TaskController.php // ... class TaskController extends AbstractController { public function new(Request $request): Response { $form = $this->createForm(TaskType::class, new Task()); $emptyForm = clone $form; $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { // ... if (TurboBundle::STREAM_FORMAT === $request->getPreferredFormat()) { $request->setRequestFormat(TurboBundle::STREAM_FORMAT); return $this->renderBlock('task/new.html.twig', 'success_stream', [ 'task' => $task, 'form' => $emptyForm, ]); } // ... return $this->redirectToRoute('task_success', [], Response::HTTP_SEE_OTHER); } return $this->render('task/new.html.twig', [ 'form' => $form, ]); } } ``` -------------------------------- ### Configure Turbo Drive Reloading Attributes Source: https://symfony.com/bundles/ux-turbo/current/index Configures Webpack Encore to automatically add 'defer' to script tags and 'data-turbo-track:reload' to script and link tags. This ensures that JavaScript files are loaded asynchronously and that Turbo Drive triggers a full page refresh when JS/CSS files change. ```yaml # config/packages/webpack_encore.yaml webpack_encore: # ... script_attributes: defer: true 'data-turbo-track': reload link_attributes: 'data-turbo-track': reload ``` -------------------------------- ### Broadcast Attribute with Mercure Options (PHP) Source: https://symfony.com/bundles/ux-turbo/current/index Demonstrates the usage of the `#[Broadcast]` attribute with various options, including Mercure-specific settings like topics with Expression Language, private updates, and template rendering. This allows fine-grained control over how entity changes are broadcast. ```php namespace App\Entity; use Symfony\UX\Turbo\Attribute\Broadcast; #[Broadcast(topics: ['@="book_detail" ~ entity.getId()', 'books'], template: 'book_detail.stream.html.twig', private: true)] #[Broadcast(topics: ['@="book_list" ~ entity.getId()', 'books'], template: 'book_list.stream.html.twig', private: true)] class Book { // ... } ``` -------------------------------- ### Specifying Broadcast Transports for an Entity (PHP) Source: https://symfony.com/bundles/ux-turbo/current/index Demonstrates how to explicitly define the list of transports to be used for broadcasting entity changes via the `#[Broadcast]` attribute. This allows overriding the default behavior and directing updates to specific hubs. ```php namespace App\Entity; use Symfony\UX\Turbo\Attribute\Broadcast; #[Broadcast(transports: ['hub1', 'hub2'])] /** ... */ class Book { // ... } ``` -------------------------------- ### Configure Broadcast Template Prefixes Source: https://symfony.com/bundles/ux-turbo/current/index This configuration allows you to customize the directory where Symfony UX Turbo looks for broadcast templates. By default, it maps entity namespaces to specific template subdirectories. This is useful for organizing your broadcast templates more effectively within your project structure. ```yaml # config/packages/turbo.yaml turbo: broadcast: entity_template_prefixes: App\Entity\: broadcast/ ``` -------------------------------- ### Enable Doctrine Entity Broadcasting with Attribute Source: https://symfony.com/bundles/ux-turbo/current/index This code demonstrates how to enable real-time broadcasting for Doctrine entities using the `#[Broadcast]` attribute in Symfony. This attribute, when applied to an entity class, automatically triggers broadcasts on entity creation, updates, and deletions. Ensure the entity has an identifier (e.g., an `id` property) for proper functioning. ```php namespace App\Entity; use Doctrine\ORM\Mapping as ORM; use Symfony\UX\Turbo\Attribute\Broadcast; #[ORM\Entity] #[Broadcast] // 🔥 The magic happens here class Book { #[ORM\Column, ORM\Id, ORM\GeneratedValue(strategy: "AUTO")] public ?int $id = null; #[ORM\Column] public string $title = ''; } ``` -------------------------------- ### Configure Page Refresh Method Source: https://symfony.com/bundles/ux-turbo/current/index This Twig code generates a meta tag to specify the method used for refreshing the page. The `method` parameter can be set to 'replace' for a full replacement or 'morph' for a more dynamic, in-place update. ```twig {{ turbo_refresh_method(method: 'replace') }} ``` -------------------------------- ### Display Chat Interface with Turbo Streams Source: https://symfony.com/bundles/ux-turbo/current/index This Twig template renders the main chat interface. It includes a container for messages that listens to the 'chat' Mercure topic using `turbo_stream_listen()`. The message submission form is placed within a `` to allow for non-disruptive form submissions and resets. The `turbo_stream_listen` function automatically sets up a Stimulus controller to manage the Mercure subscription. ```twig {# chat/index.html.twig #} {% extends 'base.html.twig' %} {% block body %}

Chat

{# The messages will be displayed here. "turbo_stream_listen()" automatically registers a Stimulus controller that subscribes to the "chat" topic as managed by the transport. All connected users will receive the new messages! #}
{{ form(form) }} {# The form is displayed in a Turbo Frame, with this trick a new empty form is displayed after every post, but the rest of the page will not change. #} {% endblock %} ``` -------------------------------- ### Register Custom Turbo Stream Listen Renderer Source: https://symfony.com/bundles/ux-turbo/current/index This PHP code implements a custom Turbo stream listener renderer, conforming to the TurboStreamListenRendererInterface. It leverages StimulusHelper to create Stimulus attributes, allowing for custom controller configurations and topic subscriptions. This enables dynamic stream listening based on provided topics. ```php namespace App\Turbo; use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem; use Symfony\UX\StimulusBundle\Helper\StimulusHelper; use Symfony\UX\Turbo\Twig\TurboStreamListenRendererInterface; use Twig\Environment; #[AsTaggedItem(index: 'my-transport')] class TurboStreamListenRenderer implements TurboStreamListenRendererInterface { public function __construct( private StimulusHelper $stimulusHelper, ) {} public function renderTurboStreamListen(Environment $env, $topic): string { $stimulusAttributes = $this->stimulusHelper->createStimulusAttributes(); $stimulusAttributes->addController('your_stimulus_controller', [ /* controller values such as topic */ ]); return (string) $stimulusAttributes; } } ``` -------------------------------- ### Broadcast Template Structure for Entity Changes Source: https://symfony.com/bundles/ux-turbo/current/index This Twig template defines how an entity's changes (create, update, remove) are broadcasted. By convention, Symfony UX Turbo looks for a `ClassName.stream.html.twig` file. This template must include `create`, `update`, and `remove` blocks, each containing Turbo Stream actions to update the DOM on the client-side. Variables like `entity`, `id`, `action`, and `options` are available. ```twig {# templates/broadcast/Book.stream.html.twig #} {% block create %} {% endblock %} {% block update %} {% endblock %} {% block remove %} {% endblock %} ``` -------------------------------- ### Enable Versioning in Webpack Encore Source: https://symfony.com/bundles/ux-turbo/current/index Enables file versioning in Webpack Encore, which is crucial for Turbo Drive's ability to detect changes in JavaScript and CSS files. Versioning modifies filenames when content changes, allowing Turbo Drive to trigger a full page reload. ```javascript // webpack.config.js Encore. // ... .enableVersioning(Encore.isProduction()) ``` -------------------------------- ### PHP: Handling Turbo Streams in Symfony Controller Source: https://symfony.com/bundles/ux-turbo/current/index This PHP code illustrates how to handle Turbo Streams within a Symfony controller action. It checks if the request prefers the Turbo Stream format and, if so, sets the response format accordingly to send only the necessary HTML for partial updates. Otherwise, it falls back to a standard redirect. ```php // src/Controller/TaskController.php namespace App\Controller; // ... use App\Entity\Task; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\UX\Turbo\TurboBundle; class TaskController extends AbstractController { public function new(Request $request): Response { $form = $this->createForm(TaskType::class, new Task()); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $task = $form->getData(); // ... perform some action, such as saving the task to the database // 🔥 The magic happens here! 🔥 if (TurboBundle::STREAM_FORMAT === $request->getPreferredFormat()) { // If the request comes from Turbo, set the content type as text/vnd.turbo-stream.html and only send the HTML to update $request->setRequestFormat(TurboBundle::STREAM_FORMAT); return $this->renderBlock('task/new.html.twig', 'success_stream', ['task' => $task]); } // If the client doesn't support JavaScript, or isn't using Turbo, the form still works as usual. // Symfony UX Turbo is all about progressively enhancing your applications! return $this->redirectToRoute('task_success', [], Response::HTTP_SEE_OTHER); } return $this->render('task/new.html.twig', [ 'form' => $form, ]); } } ``` -------------------------------- ### Stimulus Controller for Turbo Streams (HTML) Source: https://symfony.com/bundles/ux-turbo/current/index Shows how to generate the HTML attribute that registers a Stimulus controller for listening to Turbo Streams. This is achieved by using the `turbo_stream_listen()` Twig function, specifying the entity class and the desired transport. ```html
``` -------------------------------- ### Prepend Content with Twig Turbo Stream Source: https://symfony.com/bundles/ux-turbo/current/index Renders a turbo-stream element to prepend content to a specified target DOM element. This is achieved using a dedicated Twig component. ```twig Content to prepend to container designated with the dom_id. {# renders as: #} ``` -------------------------------- ### Configure Page Refresh Method and Scroll Behavior Source: https://symfony.com/bundles/ux-turbo/current/index This Twig code generates meta tags to control how a page refreshes and how the scroll position is handled. The `method` can be set to 'replace' or 'morph', and `scroll` can be 'reset' or 'preserve'. This allows for fine-grained control over the user experience during page updates. ```twig {{ turbo_refreshes_with(method: 'replace', scroll: 'reset') }} ``` -------------------------------- ### Twig: Turbo Stream Replace Action Source: https://symfony.com/bundles/ux-turbo/current/index This Twig snippet demonstrates a Turbo Stream with a 'replace' action. It targets an element with the ID 'my_div_id' and replaces its content with the provided template, allowing for dynamic updates of specific page sections without a full page reload. ```twig {# bottom of new.html.twig #} {% block success_stream %} {% endblock %} ``` -------------------------------- ### Enable Mercure Stream Controller in Symfony Source: https://symfony.com/bundles/ux-turbo/current/index This configuration snippet shows how to enable the Mercure Turbo stream controller within the `assets/controllers.json` file. By setting 'enabled' to true, you activate the functionality for broadcasting updates. ```json "@symfony/ux-turbo": { "mercure-turbo-stream": { "enabled": true, - "enabled": false, "fetch": "eager" } }, ``` -------------------------------- ### Publish Chat Messages with Symfony Controller Source: https://symfony.com/bundles/ux-turbo/current/index This PHP controller action handles incoming chat messages. It creates a form, processes submissions, and publishes new messages to the 'chat' Mercure topic using the HubInterface. The message content is rendered using a Twig stream template before being sent to clients. It also ensures the form is reset after a successful post. ```php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Mercure\HubInterface; use Symfony\Component\Mercure\Update; class ChatController extends AbstractController { public function chat(Request $request, HubInterface $hub): Response { $form = $this->createFormBuilder() ->add('message', TextType::class, ['attr' => ['autocomplete' => 'off']]) ->add('send', SubmitType::class) ->getForm(); $emptyForm = clone $form; // Used to display an empty form after a POST request $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $data = $form->getData(); // 🔥 The magic happens here! 🔥 // The HTML update is pushed to the client using Mercure $hub->publish(new Update( 'chat', $this->renderView('chat/message.stream.html.twig', ['message' => $data['message']]) )); // Force an empty form to be rendered below // It will replace the content of the Turbo Frame after a post $form = $emptyForm; } return $this->render('chat/index.html.twig', [ 'form' => $form, ]); } } ``` -------------------------------- ### Twig: Turbo Frame Component Source: https://symfony.com/bundles/ux-turbo/current/index The `` Twig component allows for the creation of Turbo Frames, which enable partial page updates. It can be used with an ID, or with additional attributes like 'loading' and 'src'. Content can also be passed directly to the frame. ```twig {# renders as: #} ``` ```twig {# renders as: #} ``` ```twig A placeholder. {# renders as: #} A placeholder. ``` -------------------------------- ### Refresh Page with Twig Turbo Stream Source: https://symfony.com/bundles/ux-turbo/current/index Renders a turbo-stream element to refresh the current page. Supports debouncing with a `requestId` for controlled refreshes. ```twig {# without [request-id] #} {# renders as: #} {# debounced with [request-id] #} {# renders as: #} ``` -------------------------------- ### Detecting Turbo Frame Requests in Symfony Controller Source: https://symfony.com/bundles/ux-turbo/current/index In a Symfony controller, you can detect if a request was initiated by a Turbo Frame by checking the `Turbo-Frame` header in the `Request` object. The header's value will be the ID of the frame that triggered the request, or `null` if the request was not made by a Turbo Frame. This allows for conditional logic based on the request origin. ```php // src/Controller/MyController.php namespace App\Controller; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; class MyController { #[Route('/')] public function home(Request $request): Response { // Get the frame ID (will be null if the request hasn't been triggered by a Turbo Frame) $frameId = $request->headers->get('Turbo-Frame'); // ... } } ``` -------------------------------- ### Symfony 6.2+ Auto Form Response Handling with Turbo Drive Source: https://symfony.com/bundles/ux-turbo/current/index In Symfony 6.2 and later, the `render()` method automatically handles form submissions with Turbo Drive by returning a 422 status code on validation errors. This prevents the default 200 status code, signaling Turbo Drive to re-render the form with errors. The controller action creates a form, handles the request, and renders the template. ```php #[Route('/product/new', name: 'product_new')] public function newProduct(Request $request): Response { $form = $this->createForm(ProductFormType::class, null, [ 'action' => $this->generateUrl('product_new'), ]); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { // save... return $this->redirectToRoute('product_list'); } return $this->render('product/new.html.twig', [ 'form' => $form, ]); } ``` -------------------------------- ### Insert Before Element with Twig Turbo Stream Source: https://symfony.com/bundles/ux-turbo/current/index Renders a turbo-stream element to insert new content before a specified target DOM element. Utilizes a Twig component for this action. ```twig Content to place before the element designated with the dom_id. {# renders as: #} ``` -------------------------------- ### Replace Element with Twig Turbo Stream Source: https://symfony.com/bundles/ux-turbo/current/index Renders a turbo-stream element to replace an existing element in the DOM, identified by a target ID. Supports optional morphing for smoother transitions. ```twig Content to replace the element designated with the dom_id. {# renders as: #} {# with morphing #} Content to replace the element. {# renders as: #} ``` -------------------------------- ### Configure Page Refresh Scroll Behavior Source: https://symfony.com/bundles/ux-turbo/current/index This Twig code generates a meta tag to control the scroll behavior when a page is refreshed. The `scroll` parameter can be set to 'reset' to return to the top of the page or 'preserve' to maintain the current scroll position. ```twig {{ turbo_refresh_scroll(scroll: 'reset') }} ``` -------------------------------- ### Append Content with Twig Turbo Stream Source: https://symfony.com/bundles/ux-turbo/current/index Renders a turbo-stream element to append content to a specified target DOM element. Uses a Twig component for easy integration. ```twig Content to append to container designated with the dom_id. {# renders as: #} ``` -------------------------------- ### Force Full Page Reload Meta Tag Source: https://symfony.com/bundles/ux-turbo/current/index This Twig code generates a meta tag that forces a complete reload of the page. This is typically used when significant changes have occurred that require a full refresh rather than a partial update. ```twig {{ turbo_page_requires_reload() }} ``` -------------------------------- ### Register Custom Turbo Broadcaster Source: https://symfony.com/bundles/ux-turbo/current/index This PHP code defines a custom broadcaster for Turbo, implementing the BroadcasterInterface. It's designed to be triggered when an object marked with the #[Broadcast] attribute is modified. The `broadcast` method contains the logic for handling entity changes and broadcasting updates. ```php namespace App\Turbo; use Symfony\UX\Turbo\Attribute\Broadcast; use Symfony\UX\Turbo\Broadcaster\BroadcasterInterface; class Broadcaster implements BroadcasterInterface { public function broadcast(object $entity, string $action): void { // This method will be called every time an object marked with the #[Broadcast] attribute is changed $attribute = (new \ReflectionClass($entity))->getAttributes(Broadcast::class)[0] ?? null; // ... } } ``` -------------------------------- ### Append New Chat Messages with Turbo Stream Source: https://symfony.com/bundles/ux-turbo/current/index This Twig template defines how new chat messages are rendered and appended to the message list. It uses a `` tag with the 'append' action, targeting the '#messages' element. The actual message content is placed within a `