Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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
Tweet

More Decks by Kévin Dunglas

Other Decks in Programming

Transcript

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

    of Mercure.rocks and API Platform ❏ Symfony Core Team Member @dunglas .social
  2. @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 👷 [email protected]
  3. @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)
  4. @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
  5. @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
  6. @dunglas Enable the Webpack Encore Integration {# templates/base.html.twig #} {#

    ... #} {% block javascripts %} {{ encore_entry_script_tags('app') }} {% endblock %}
  7. @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
  8. @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>
  9. @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], ); }
  10. @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
  11. @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>
  12. @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
  13. @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)
  14. @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'); }
  15. @dunglas Lazy Loading Frame: Templates {# templates/base.html.twig #} <header> Navbar

    displayed on every pages <turbo-frame 
 id="cart" 
 src="{{ fragment_uri(controller('App\\Controller\\MyController::cart')) }}" ></turbo-frame> </header> {# templates/cart.html.twig #} <turbo-frame id="cart"> Cart content </turbo-frame>
  16. @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
  17. @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)
  18. @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>
  19. @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] ); }
  20. @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>
  21. @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)
  22. @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
  23. @dunglas Create a Broadcasted Entity Using MakerBundle bin/console make:entity --broadcast

    Comment 
 bin/console make:crud Comment // src/Entity/Comment.php // ... use Symfony\UX\Turbo\Attribute\Broadcast; /** * @ORM\Entity */ #[Broadcast] class Comment { // ... }
  24. @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 %}
  25. @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>
  26. @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.
  27. @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!