$30 off During Our Annual Pro Sale. View Details »

Pedal to the metal: introducing Symfony Turbo

Pedal to the metal: introducing Symfony Turbo

Hotwire Turbo is a tiny library recently introduced by DHH (the creator of Ruby on Rails) allowing to have the speed of Single-Page Apps without having to write any JavaScript!

As part of the Symfony UX initiative, I created an official integration between Turbo and Symfony. With Symfony Turbo, you can get rid of JavaScript and enjoy using Twig again!

During this talk, we'll discover how the library works, how to leverage it to enhance your Twig templates, how to add real-time features to your websites…

Screen recordings:

Live Edit: https://www.youtube.com/watch?v=d2N71fnVU-c
Live Comments: https://www.youtube.com/watch?v=WcEynac4tlE

Kévin Dunglas

April 09, 2021

More Decks by Kévin Dunglas

Other Decks in Programming


  1. Turbo Photo by Ye Massa

  2. @dunglas Kévin Dunglas ❏ Founder of Les-Tilleuls.coo p ❏ Creator

    of Mercure.rocks and API Platform ❏ Symfony Core Team Member @dunglas .social
  3. @dunglas Les-Tilleuls.coop Symfony, JavaScript and cloud expert s ✊ Self-managed,

    100% employee-owne d 🦄 50 people, 1,000% growth in 6 year s 👷 contact@les-tilleuls.coop
  4. @dunglas Do You Need JavaScript?

  5. None
  6. None
  7. @dunglas Hotwire aka DHH’s “NEW MAGIC” ❏ Turbo: the heart

    of Hotwire, the topic of this tal k ❏ Stimulus: when you really need JS (~20% use cases ) ❏ Strada: mobile hybrid apps, not released ye t ❏ Created by David Heinemeier Hansson et al.
 (Ruby on Rails, Basecamp, Hey, racing driver)
  8. None
  9. None
  10. @dunglas Symfony UX Turbo : Getting Started

  11. @dunglas The Symfony UX Initiative

  12. @dunglas Tooling ❏ Symfony CLI: bootstrap a project, run a

    local web server and… a surprise ! ❏ Webpack Encore: process and compile assets, Twig integratio n ❏ Stimulus Bridge: automatically loads JS files in your Symfony apps
  13. @dunglas Create a Symfony UX Turbo Project # Create a

    new Symfony 5.3 project symfony new sf—turbo --full cd sf-turbo # Install Symfony UX Turbo composer require symfony/ux-turbo-mercure # Install and compile the JS dependencies yarn install yarn encore dev # Start the local web server symfony serve -d
  14. @dunglas Enable the Webpack Encore Integration {# templates/base.html.twig #} {#

    ... #} {% block javascripts %} {{ encore_entry_script_tags('app') }} {% endblock %}
  15. None
  16. @dunglas Turbo Drive

  17. @dunglas Turbo Drive ❏ Enhances page level navigation:
 no more

    “white flicker ” ❏ Watches for clicks and form submissions ❏ Loads pages in the background using fetch( ) ❏ Replaces the <body>, merges the <head > ❏ Changes browser’s history using history.pushStat e ❏ Customizable progress ba r ❏ Programmatic API (Turbo.visit()) and event system
  18. @dunglas Turbo Drive Is Automatically Enabled {# templates/conference/show.html.twig #} {#

    … #} <a href="{{ path('conference_index') }}">back to list</a> <a href="{{ path('conference_edit', {'id': conference.id}) }}" data-turbo-action="replace" > edit </a> <a href="/blog" data-turbo="false">Our standalone blog</a>
  19. @dunglas Click on a link

  20. @dunglas Turbo Drive and Forms #[Route('/conference')] class ConferenceController extends AbstractController

    { #[Route('/{id}/edit')] public function edit(Request $request, Conference $conference) { $form = $this->createForm(ConferenceType::class, $conference); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { // … return $this->redirectToRoute('conference_index', [], Response::HTTP_SEE_OTHER); } // New in Symfony 5.3: a 4(22) status code MUST be returned if the form is invalid return $this->renderForm( 'conference/edit.html.twig', ['conference' => $conference, 'form' => $form], ); }
  21. None
  22. @dunglas Turbo Frames

  23. @dunglas Turbo Frames ❏ Functionally similar to old school HTML

    frame s ❏ Update parts of the page (blocks ) ❏ Capture links and forms in this fram e ❏ The content of the frame is extracted from the response, and the existing content is replace d ❏ The response can contain the full page, or only a fragmen t ❏ A Web Component is used to delimit frame s ❏ A page can contain multiple frames
  24. @dunglas Turbo Frames {# templates/base.html.twig #} <!DOCTYPE html> {# …

    #} <header>Navbar displayed on every pages</header> <main> <turbo-frame id="body"> Frame content, replaced on click {% block body %}{% endblock %} </turbo-frame> </main>
  25. None
  26. @dunglas Lazy Loading Frames

  27. @dunglas Lazy Loading Frames ❏ Turbo can also lazy loads

    frames, client-side ❏ This allows to dramatically improve cache dynamic s ❏ The page can be divided in blocks, each block can have a different cache tim e ❏ Usage examples : ❏ cart: the main content is in cache, but not the cart which is user-specifi c ❏ breaking news: the main content is in cache with a TTL of a few hours, but not the “breaking news” block
  28. @dunglas This looks like “ESI” isn’t it? Yes! But client-side.

  29. @dunglas Trust me, with modern JS frameworks, ESI are a

  30. @dunglas But with Symfony, it’s easy and natively supported

  31. @dunglas Lazy Loading Frames in Symfony ❏ Reuse the existing

    fragment subsystem (ESI, hinclude ) ❏ Routing is handled automaticall y ❏ You can pass variables to fragment s ❏ Generated URLs are signed (HMAC ) ❏ Best with a cache server (Varnish, Souin, Cloudflare… ) ❏ Require Symfony 5.3 + ❏ Can be mixed with server-side ESI (SEO)
  32. @dunglas Lazy Loading Frame: Controllers #[Route('/')] #[Cache(public: true, maxage: 3600)]

    public function index() { // … } #[Cache(public: false, maxage: 0)] public function cart() { return $this->render('cart.html.twig'); }
  33. @dunglas Lazy Loading Frame: Templates {# templates/base.html.twig #} <header> Navbar

    displayed on every pages <turbo-frame 
 src="{{ fragment_uri(controller('App\\Controller\\MyController::cart')) }}" ></turbo-frame> </header> {# templates/cart.html.twig #} <turbo-frame id="cart"> Cart content </turbo-frame>
  34. @dunglas Lazy Loading Frame: Cache Per Block

  35. None
  36. @dunglas Turbo Streams

  37. @dunglas Turbo Streams ❏ Add real-time capabilities to your websites

    ! ❏ Stream page changes as fragments of HTM L ❏ Wrap changes in a custom HTML elemen t ❏ The server can push the changes to all connected users using a real-time protocol such as Mercure or Websockets
  38. @dunglas Turbo Streams in Symfony ❏ Natively supported 🎉 ❏

    Use the Mercure protocol under the hoo d ❏ Developer-friendly API (new in MercureBundle 0.3, thanks @azjezz ) ❏ Native authorization support, aka private updates (new in MercureBundle 0.3, thanks @azjezz)
  39. @dunglas

  40. @dunglas Show Template {# templates/conference/show.html.twig #} <div {{ turbo_stream_listen('conference:' ~

    conference.id) }}> <h1 id="name">{{ conference.name }}</h1> <p id="description">{{ conference.description }}</p> </div>
  41. @dunglas Edit Controller #[Route('/{id}/edit')] public function edit(Request $request, Conference $conference,

    HubInterface $hub): Response { $form = $this->createForm(ConferenceType::class, $conference); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { // … $hub->publish( new Update( 'conference:'.$conference->getId(), $this->renderView( 'conference/edit.stream.html.twig', ['conference' => $conference] ) ) ); return $this->redirectToRoute('conference_index', [], Response::HTTP_SEE_OTHER); } return $this->renderForm( 'conference/edit.html.twig', ['conference' => $conference, 'form' => $form] ); }
  42. @dunglas Stream Template {# templates/conference/edit.stream.html.twig #} <turbo-stream action="update" target="name"> <template>

    {{ conference.name }} </template> </turbo-stream> <turbo-stream action="update" target="description"> <template> {{ conference.description }} </template> </turbo-stream>
  43. None
  44. @dunglas Where Is The Mercure Hub?! ❏ Symfony CLI now

    includes a native Mercure hub! (thanks @tgalopin and @fabpot ) ❏ It is detected and used by Symfony automatically in development ❏ In production : ❏ Use the official hub (binary, Docker image… ) ❏ Use a managed versio n ❏ Write your own Hub (open protocol)
  45. None
  46. @dunglas Broadcast:
 Turbo Streams X Doctrine

  47. @dunglas Turbo Streams X Doctrine ❏ If you use Doctrine,

    we can do better ! ❏ Symfony UX Turbo is shipped with
 an integration with Doctrine ORM ! ❏ The UI can always be up to date with changes made to database ! ❏ Supported by MakerBundle
  48. @dunglas Create a Broadcasted Entity Using MakerBundle bin/console make:entity --broadcast

 bin/console make:crud Comment // src/Entity/Comment.php // ... use Symfony\UX\Turbo\Attribute\Broadcast; /** * @ORM\Entity */ #[Broadcast] class Comment { // ... }
  49. @dunglas Update the Generated Template {# templates/broadcast/Comment.stream.html.twig #} {% block

    create %} <turbo-stream action="append" target="comments"> <template> <div id="{{ 'comment_' ~ id }}"> {{ entity.content }} </div> </template> </turbo-stream> {% endblock %} {% block update %} <turbo-stream action="update" target="comment_{{ id }}"> <template> {{ entity.content }} </template> </turbo-stream> {% endblock %} {% block remove %} <turbo-stream action="remove" target="comment_{{ id }}"></turbo-stream> {% endblock %}
  50. @dunglas Subscribe and List Existing Comments {# templates/comment/show.html.twig #} <h1>Live

    Comments</h1> <div id="comments" {{ turbo_stream_listen('App\\Entity\\Comment') }}> {% for comment in comments %} <div id="{{ 'comment_' ~ comment.id }}"> {{ comment.content }} </div> {% endfor %} </div>
  51. None
  52. @dunglas 0 lines of JS!

  53. None
  54. @dunglas Going Further

  55. @dunglas Turbo Native ❏ Wraps Turbo websites in native iOS

    and Android app s ❏ Webview-based
  56. Testing: Panther already supports Turbo and Mercure!

  57. @dunglas Hotwire or a “modern” JS framework? It depends of

    the use case ! ❏ For traditional websites (CMS, e-commerce…), Hotwire and Symfony UX dramatically reduce the complexity of your majestic monolith, without compromises regarding the user experienc e ❏ For (most) webapps (offline-first, Jamstack, real-time geolocation…) and microservices architectures using a JS framework such as Next, Nuxt or SvelteKit with a JSON API is better suited.
  58. @dunglas Symfony gives you the choice:
 with API Platform

    build your API in minutes
 then scaffold a Next.js, Nuxt.js or React Native app!
  59. None
  60. dunglas Thanks! If you like this project, sponsor me on