Fabien Potencier
Contributed by Fabien Potencier in #6829

About a month ago, I merged a complete refactoring of the sub-requests management of Symfony. In fact, I created a whole new sub-framework to handle the rendering of resource fragments via different strategies:

  • The code is now part of the HttpKernel component (instead of FrameworkBundle). It makes the code easily reusable outside of the Symfony full-stack framework (like in Silex, Drupal, or Laravel);
  • The code is more decoupled as it does not depend on the Dependency Injection or the Routing components anymore;
  • The code is now extensible and that allows for new rendering strategies to be created and integrated in a matter of minutes (like SSI support that should come in Symfony 2.3);
  • The code is now faster as we no longer create sub-requests when handling an ESI/HInclude (instead of two in Symfony 2.0 and 2.1);
  • But more importantly, extensive usage of ESIs or HIncludes is fun again as no dedicated routes need to be created (that makes Drupal happy again!).

Besides classical master requests, the HttpKernel component is now able to handle sub-requests. Let's me sum up the different strategies that are available: internal sub-requests, ESIs, HIncludes, and SSIs (in 2.3). But how do you choose the one to use?

First, even if all sub-requests are eventually handled by Symfony, they are processed at different stages during the request/response life cycle:

  • Internal sub-requests: Processed by Symfony directly;
  • SSIs: Processed by a web server (Apache, Nginx, ...);
  • ESIs: Processed by a reverse proxy (Varnish, ...);
  • HInclude: Processed by a web browser.

Then, depending on your architecture (do you have a reverse proxy? does your web server support SSIs?), some options might not be available for your project; but they are other differences that may help you make your choice:

  • HIncludes are the only sub-requests processed on the client side (with the help of some JavaScript), all other strategies are processed on the server side: HIncludes incur several round-trips between the server and the client whereas other strategies only involves one round-trip;
  • Internal sub-requests do not need any specific configuration or software, they work out the box;
  • ESIs and HIncludes allow you to set specific cache strategies for the sub-responses; others don't.

I won't talk about the internal code as you will probably never have to deal with it, but let me show you how you can use the new code; first, what we have today (which still works in 2.2):

1
2
3
4
5
6
7
8
{# an internal sub-request via a regular URL #}
{% render url('route_name') %}

{# an ESI tag via a regular URL #}
{% render url('route_name', { standalone: 'esi' }) %}

{# an HInclude tag via a regular URL #}
{% render url('route_name', { standalone: 'hinclude' }) %}

The first change is the availability of a new render() function, which work in the same way as the tag (which is now deprecated):

1
2
3
4
5
6
7
8
{# an internal sub-request via a regular URL #}
{{ render(url('route_name')) }}

{# an ESI tag via a regular URL #}
{{ render(url('route_name', { standalone: 'esi' })) }}

{# an HInclude tag via a regular URL #}
{{ render(url('route_name', { standalone: 'hinclude' })) }}

The standalone option was created at a time where the only options were true or false as we only supported ESI sub-requests. But as more options are now available, this option has been renamed to strategy:

1
2
3
4
5
6
7
8
{# an internal sub-request via a regular URL #}
{{ render(url('route_name')) }}

{# an ESI tag via a regular URL #}
{{ render(url('route_name', { strategy: 'esi' })) }}

{# an HInclude tag via a regular URL #}
{{ render(url('route_name', { strategy: 'hinclude' })) }}

The introduction of the render function also allows for some nice syntactic sugar that make calls shorter than before:

1
2
3
4
5
6
7
8
{# an internal sub-request via a regular URL #}
{{ render(url('route_name')) }}

{# an ESI tag via a regular URL #}
{{ render_esi(url('route_name')) }}

{# an HInclude tag via a regular URL #}
{{ render_hinclude(url('route_name')) }}

As you can see, this makes things shorter and more consistent. You might also have noticed that we are using URLs even for internal sub-requests. That's because some time ago, we fixed a major security issue in the way internal routes were secured in Symfony (internal routes are mainly used to handle ESI and HInclude tags). At that time, we announced that Symfony 2.2 would not support internal routes anymore and that all ESI and HInclude tags would have to be configured via the creation of proper routes... and indeed that's what we did at first, and that's what I've just used in the examples. Of course, being forced to create a specific route for each ESI or HInclude tag is cumbersome (especially if you heavily rely on them); so, there is now a better way:

1
2
3
4
5
6
7
8
{# an internal sub-request via a controller #}
{{ render(controller('Bundle:controller:action')) }}

{# an ESI tag via a controller #}
{{ render_esi(controller('Bundle:controller:action')) }}

{# an HInclude tag via a controller #}
{{ render_hinclude(controller('Bundle:controller:action')) }}

But I've just said that using an internal route (which a controller should probably use) was not secure, right? Yes, but we do not rely on internal routes anymore. The sub-requests are now caught by a listener (before the Routing kicks in) when you are using controllers. That's an implementation detail, but it means that we can now properly secure those calls automatically for you (we are using a mix of URL signing and IP checks depending on the strategy your are using).

Using proper URLs might be a good choice for HIncludes and controllers are probably better for ESIs and internal sub-requests... but that's really up to you.

That's the power of the new fragment sub-framework! I hope that you appreciate the simplicity of the new API and the fact that everything is secured automatically for you.

What about switching your project to Symfony 2.2!

Published in #Living on the edge