Routing with Symfony and Vuejs Router

One of the initial problems you might come across while integrating Symfony and VueJs is probably Routes. How do you handle this Symfony Vuejs routing? How to let Symfony forward the request to Vue Router? Lets quickly get to answering these questions.

symfony vuejs routing

Forwarding Requests from Symfony to the Vue Router

To allow the Vue Router to take control, we need to forward incoming requests from Symfony to the Vue Router. To do this, we will have to configure Symfony Routing.

There are 2 ways to configure Symfony Routing:
– Annotations
– Including routes in the config. (xyz.yaml)

For the purpose of the tutorial, we will be using Annotations. But if you are including routes in your yaml config, check section “How to let Symfony router handle external Bundle routes instead of the Vue Router” at the end of the article.


Routing Via Annotations:

We update our controller DefaultController to handle matching routes and forward the response to a twig template (base.html.twig)

// Controller/DefaultController.php
// use ...;

class DefaultController extends Controller
{
    /**
     * @Route("/", name="home_FE")
     * @Route("/{route}", name="vue_pages", requirements={"route"="^.+"})
     */
    public function indexAction(Request $request)
    {

        return $this->render('default/index.html.twig', [
            'controller_name' => 'DefaultController',
        ]);
    }
}

The routing pattern used above "route"="^.+" matches every possible route and returns the response via the Twig template. This template is shown below. It imports the necessary Vue Assets (stylesheets/javascript files) which allows for the Vue Router to handle the routing.

// /templates/base.html.twig
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>{% block title %}Welcome!{% endblock %}</title>
        {% block stylesheets %}
            <link rel="stylesheet" href="{{ asset('build/js/app.css') }}">
        {% endblock %}
    </head>
    <body>
        {% block body %}
               <div id="vueApp"></div>
        {% endblock %}

        {% block javascripts %}
            {# includes all built assets of VueJs/VueRouter/... #}
            <script src="{{ asset('build/js/app.js') }}"></script>
        {% endblock %}
    </body>
</html>

As seen above, we initialize the Vue app in the body block and include the built assets in the stylesheets/javascript blocks. If you have already setup your Vue App, you may skip the following part.


Lets quickly setup an example Vue app to demonstrate the working of the Vue router. So, to begin with, ensure you have installed all the necessary Symfony-VueJS dependencies. If you run into any issues integrating Symfony with VueJs, here’s a painless tutorial to guide you along.

Assuming, you have all necessary dependencies met, lets create the App entry point.

// ./assets/js/index.js
import Vue from 'vue';
import VueRouter from 'vue-router';
 
// app specific
import router from './router/';
import app from './app';
 
Vue.use(VueRouter);
 
// bootstrap the app
let demo = new Vue({
    el: '#vueApp',
    router,
    template: '<app/>',
    components: { app }
})

Now, we will create some example components with some basic markup/styling

// ./assets/js/components/hello.vue
<template>
    <div id="vueApp">
        <div id="container">
            <div id="welcome">
                <h1>Hello Page</h1>
            </div>
            <div id="status">
                <p>
                    You are now in  <b>Hello vue route</b>
                </p>
            </div>
        </div>
    </div>
</template>
  
<script>
    export default {
        name: "hello"
    }
</script>
  
<style scoped lang="less">
    #welcome h1 {
        color: green;
    }
</style>
  
/*************************************************/
// ./assets/js/components/home.vue
<template>
    <div id="vueApp">
        <div id="container">
            <div id="welcome">
                <h1>Home Page</h1>
            </div>
            <div id="status">
                <p>
                    You are now in <b>Home vue route</b>
                </p>
            </div>
        </div>
    </div>
</template>
  
<script>
    export default {
        name: "home"
    }
</script>
  
<style scoped>
    #status p {
        color: green;
    }
</style>
/************************************************/
// ./assets/js/components/notFound.vue
<template>
    <section class="hero is-danger">
        <div class="hero-body">
            <div class="container">
                <h1 class="title">
                    404 Page not found
                </h1>
                <p class="subtitle">
                    its just another vue route - you can change the url above to any thing
                </p>
            </div>
        </div>
    </section>
</template>
  
<script>
    export default {
        name: "notFound"
    }
</script>
  
<style scoped lang="less">
    .title {
        color: #0000F0;
    }
    .hero-body {
        background-color: gray;
        padding: 15px;
        .title {
            color: white;
        }
    }
</style>

As we have setup our Vue App and a few example components, we will proceed to configure the Vue Router.


We now create and configure the Vue Router to handle 3 routes as seen below –

// ./assets/js/router/index.js
import Router from 'vue-router'
 
// components
import home from '../components/home'
import hello from '../components/hello'
import notfound from '../components/notFound'
 
export default new Router({
    mode: 'history',
    routes: [
        {
            path: '/',
            name: 'homepage',
            component: home
        },
        {
            path: '/hello',
            name: 'Hello',
            component: hello
        },
        {
            path: '*',       // * => wildcard. Matches all other routes
            name: 'notfound',
            component: notfound
        }
    ]
})

If you run your application now, you will notice that the Vue Router effectively handles your routes. You can navigate to /, /hello, or /any/other/route and see that the home, hello or notfound Vue components will handle these routes.


Caveats with Symfony Vuejs routing

There are however certain caveats:

  • Symfony Web Profiler is broken.
  • You cannot use any external bundles (FOSUserBundle,etc..) since your DefaultController will intercept and handle the routes that were supposed to handled by the concerned Bundle’s controller.(FOSUserBundle’s controllers)
  • 4xx errors will be not be handled by Symfony.

Lets see how to go around these caveats…


How to fix the Symfony Web Profiler/Debug Toolbar?

The key here is to update the route matching requirement in the DefaultController to selectively handle routes. We update the routing requirements to ^(?!.*_wdt|_profiler).+ which means that this controller action will match all routes excepting the ones beginning with _wdt or _profiler. In default Symfony 3.x, 4.x installations, routes beginning with _wdt/_profiler are responsible to make the debug toolbar functional.

// Controller/DefaultController.php
class DefaultController extends Controller
{
    /**
     * @Route("/", name="home_FE")
     * @Route("/{route}", name="vue_pages", requirements={"route"="^(?!.*_wdt|_profiler).+"})
     */
    public function indexAction(Request $request)
    {
          // ...
    }
}

This ensures that our DefaultController will not interfere or try to handle routes that are supposed to be handled by Symfony’s WebProfilerBundle. Now when you refresh your app, you can see that the Debug toolbar becomes operational.


How to let Symfony router handle external Bundle’s routes instead of the Vue Router?

Symfony’s router matches the routing rules in the order in which they are loaded. Hence, the route loading order is very important. To see which routes are loaded first, you could debug the router in your console.

php bin/console debug:router  // lists routes by loaded order

To let Symfony router handle the concerned bundle’s routes instead of the Vue Router, all you need to do, is to ensure that the Bundle’s routes are imported first.
This can be done by updating your annotations.yaml config:

# config/routes/annotations.yaml 
fos_user:    # load FOSUserBundle's routes first
    resource: "@FOSUserBundle/Resources/config/routing/all.xml"
    prefix: /static/user   #optional
 
vue_controller: # load our routes later
    resource: ../../src/Controller/DefaultController.php
    type: annotation

Now, if you debug the router via php bin/console debug:router, you will see that the FOSUserBundle’s routes were loaded first. Hence these routes will be matched and executed in Symfony instead of being forwarded to the Vue Router.


Handling 4xx errors in Symfony

Unfortunately, there is no way to handling 4xx errors in Symfony since all unmatched routes are forwarded to the Vue Router. However, if you check the config/routes/dev/twig.yaml that is generated by default (seen below)

_errors:
    resource: '@TwigBundle/Resources/config/routing/errors.xml'
    prefix: /_error

You can see, that you can catch twig errors by either loading this route earlier or by updating the routing requirements to ^(?!.*_wdt|_profiler|_error).+ to disallow our DefaultController from handling these routes.


Routing for Single Page Applications (SPA)

You can create an API for Single Page Applications and make Symfony handle those routes simply by loading the routes to your api first.

# config/routes/annotations.yaml 

api_controllers:  # load api routes first
    resource: ../../src/Controller/api/
    type: annotation

fos_user:    # load external bundle routes
    resource: "@FOSUserBundle/Resources/config/routing/all.xml"
    prefix: /static/user   #optional
 
vue_controller: # load Vuejs routes last
    resource: ../../src/Controller/DefaultController.php
    type: annotation

And very simply the same Symfony backend can serve as an API to your awesome frontend VueJS App.


Generating Symfony route in a Vue Component

You can get a Symfony route in a Vue component simply by using the twig path('some_identifier') or url('some-identifier') in your twig template.

// /templates/base.html.twig
// ...
{% block javascripts %}
    <script>
    window.globalVars = {
        route1: "{{ path('blog_show', {'slug': 'my-blog-post'})|escape('js') }}",
        route2: "{{ url('blog_index')|escape('js') }}"
    }
    </script>
    <script src="{{ asset('build/js/app.js') }}"></script>
{% endblock %}
// ...

And then you have access to these global variables in your components.
Another way to do it is to use the FOSJsRoutingBundle
After configuring this Bundle, you can generate any route in your component as follows:

var url = Routing.generate('blog_show', {
    'slug': 'my-blog-post'
});

Going Further with VueJS

Once your project/app starts growing, you most certainly will see the need for managing the state efficiently. Vuex is tailored specifically for this very purpose and here’s our Vuex beginners tutorial for those interested.
That’s it for now folks!


References