AngularJS and Symfony2 harmony

Symfony2’s a good server-side framework. Angular’s a good front-end framework. What happens if you want to use them together?

It’s certainly possible, but if you want them in one project there are two complications: templates and routes.

My project

I’ve been trying to find a clean combination of Angular & Symfony for an open source project. Right now it’s largely Angular and doesn’t even need a database. But I wanted Symfony involved for a couple of reasons.

Firstly it’s a handy way to get access to Assetic‘s snazzy asset management. For some projects you might be better with a separate client app and Symfony server, but you’d lose this benefit and have to maintain two codebases.

Secondly we keep options open if we want to quickly add more admin management or a server API later.

However this combination inside one project does introduce a couple of interesting problems. Let’s fix them!

Software

I’ll be using AngularJS 1.3 with angular-ui-router, Symfony 2.6, and configuration that should work with Apache 2.2 or 2.4.

Templates

I’ve mostly followed the Symfony2 convention for an outer layout template, which lets us use Twig with Assetic magic in the usual way.

But AngularJS needs templates to be useful too. These are public-facing static template files, not Symfony templates, so let’s keep them in /web/partials.

Having done this, we can use angular-ui-router in the usual way. When we want to configure a template with its $stateprovider, we’ll give it a state definition with a property like:

  ...
  templateUrl: '/partials/home.html'
  ...

Cool. But Twig interpolates stuff using {{ ... }}. And AngularJS interpolates stuff using {{ ... }}.

Fortunately this is an easy fix on the Angular side. In your app’s config(), inject the service $interpolateProvider and then write, for example:

$interpolateProvider.startSymbol('{[').endSymbol(']}');

This will get you set up using {[ ... ]} for AngularJS interpolation instead of the usual syntax. You can choose any symbols that won’t clash with something else.

Routes

The more confusing problem for me was getting Apache, Symfony and AngularJS to play nicely together with respect to routing.

I settled on letting Apache’s mod_rewrite send most initial page requests through to Symfony’s index file as if they were all loading the home page – but not static files.

This lets Symfony pick up requests for all static files still, including generated & concatenated ones from Assetic. Meanwhile, other routes can have their values passed via a # in the URL. This lets Apache & Symfony ignore them while Angular can pick them up.

When combined with AngularJS’s HTML5Mode, the # is transparently removed again, so for most users the URLs look just like any other.

Apache setup

How do we know what to rewrite? I found some discussion that suggested letting Apache check for real files & directories with e.g. RewriteCond %{REQUEST_FILENAME}.

The trouble here is that Apache doesn’t really know about Symfony routes. To work with both frameworks I need matching that’s based only on a pattern and not on the filesystem.

What I ended up with was a check for a dot in the request. It’s crude but it works!

All requests that have no dot-extension (.js, .css, etc.) go to AngularJS via that index route, while the dotty ones goes through Symfony.

The virtual host

Here’s what I ended up with in my production environment. The surrounding …’s can follow normal Symfony conventions.

<VirtualHost *:80>
  ...
  RewriteEngine on
  RewriteRule ^/([^.]+)$ /#!/$1 [R,NE]
  ...
</VirtualHost>

N.B. in development you can do the same, but you probably want to look for /app_dev.php/ at the start, not just /.

What was that?

Firstly we match the start of the request after the hostname, with ^, and match the initial /.

Next we want to capture (with ()) anything that’s not a dot. As many not-dots as it takes. (But at least one, so the / route won’t match.) We then keep going to the end of the whole URL – $ is where the buck stops.

Because we insist on looking to the end, any dots will break the pattern. We’re adopting the convention that only static resources will have dots – because these don’t match the pattern these will pass through unimpeded as with a normal Symfony app. Requests like .../bundles/.../asdf123.js are left alone.

So what happens to our matches? We redirect them all to our Symfony index file. Note that the redirect path uses #! – this means the web server won’t see the end bit, it’s just used by the browser. As far as Symfony’s concerned all these requests are going to the default / route.

Finally we append $1 – this is the bracket value we matched in the pattern. So now AngularJS has the URL piece it needs to do its own routing work.

We also need two special flags saying how the rule should work:

[R] (Redirect) tells Apache this should be a browser redirect and not an internal one – this defaults to a temporary 302. This is essential! Without an HTTP redirect none of this would work.

[NE] (No Escape) leaves special characters alone – also vital. Without it the added # causes a crazy infinite redirect loop, as Apache morphs it to an ever-expanding list of %23s. We need Apache to keep its nose out of our hash, so browsers can just request / from the server and pass the # bit to AngularJS.

Angular HTML5 routes

If we want to have URLs appear without a # in modern browsers, we can do this too in the normal way.

Our AngularJS app needs a config() block with $locationProvider injected. Then we do:

$locationProvider.html5Mode(true).hashPrefix('!');

Easy!

Symfony routing

I mentioned that Symfony would see everything as going to /. But it does still need to route those correctly.

Aside from some bundle placeholders, the project’s only route is a single catch-all set up with an annotation. So routing.yml has:

app:
    resource: @MyMainBundle/Controller/
    type:     annotation

And my only controller method uses one annotation:

/**
 * @Route("/", name="Home")
 * @Template()
 */

Keeping Symfony in the loop

What if we wanted a Symfony admin panel involved here too?

Well, luckily mod_rewrite and regular expressions are really flexible. We could easily add a new rule which limits the times we do those crazy #! redirects. We could add one that excludes URLs starting, say, /admin.

We’d be free to use regular Symfony routing with that prefix, while letting AngularJS transparently pick up any dynamic routing without it. Perfect!

Another way?

I hope this is one of the simplest ways to make this work while mostly respecting both framework’s conventions. But is there a better way? Would love to hear your comments.

Leave a Reply

Your email address will not be published. Required fields are marked *