Symfony Vue JS Integration in under 10 mins

Integrating a good front end library like Vue js has become a de facto standard these days. This tutorial aims at integrating Vue js with Symfony backend. Symfony Vue Js integration is one of the best combinations for a trendy App, whether it be a SPA or a traditional Web App.

Requirementssymfony vue js

  • Symfony 3.4 and above
  • Vue JS  2.5 and above
  • FOSRestBundle (optional, but useful when you want to develop a SPA)

Setup

The easiest way to setup an already working Symfony Vue Js Integration is to clone this repository and then follow the remaining tutorial to understand how it has actually been done. Or you could integrate it yourself from scratch.

1. Install Symfony

You can install symfony via composer

composer create-project symfony/website-skeleton my_project

2. Install Vue JS

To install Vue JS, first Cd into your project directory (i.e. cd my_project). Ensure that you have node.js and yarn package manager installed. There are 2 ways of installing all Vue js dependencies. The first one is to update your package.json as shown below:

// package.json
{
    "devDependencies": {
        "@symfony/webpack-encore": "^0.20.1",
        "jquery": "^3.3.1",
        "less": "^3.7.1",
        "less-loader": "^4.1.0",
        "vue": "^2.5.16",
        "vue-loader": "^15.2.4",
        "vue-template-compiler": "^2.5.16"
    },
    "license": "UNLICENSED",
    "private": true,
    "scripts": {
        "dev-server": "encore dev-server",
        "dev": "encore dev",
        "watch": "encore dev --watch",
        "build": "encore production"
    },
    "dependencies": {
        "vue-router": "^3.0.1"
    }
}

and then execute npm install in your terminal.
An alternate way to install the same is using yarn package manager. It can be done by executing the following in your terminal.

yarn add vue vue-loader vue-template-compiler @symfony/webpack-encore --dev
yarn add vue-router
// if you wish to use less
yarn add less less-loader --dev

3. Development Server

You could install a local development server to see the results. Usually, a dev server is installed by default with Symfony, however if you wish to install/reinstall it, execute in your terminal

// install dev server
composer require server --dev
// to start the server
php bin/console server:start
// to stop the server
php bin/console server:stop

If you started the server, you could navigate to http://localhost:8000/ and should have the usual Welcome to Symfony page ensuring that you have setup the project correctly.

4. Update Controller and View

We will be using Vue-router to handle the routing of our App. Hence, we need to define a controller that can catch all routes. But we want to use symfony backend to serve as a REST API as well. So how do we do that? Well, the trick here is to define a route that gets matched conditionally. Lets see how…
We will update the DefaultController located at /src/Controller/DefaultController.php as follows:

// /src/Controller/DefaultController.php
<?php 
namespace App\Controller; 
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method; 
use Symfony\Component\HttpFoundation\JsonResponse; 
use Symfony\Component\HttpFoundation\Request; 
use Symfony\Component\Routing\Annotation\Route; 
use Symfony\Bundle\FrameworkBundle\Controller\Controller; 
class DefaultController extends Controller 
{ 
    /** 
    * @Route("/", name="homepage") 
    * @Route("/{route}", name="vue_pages", requirements={"route"="^(?!.*api).+"}) 
    * @Method("GET") 
    */ 
    public function indexAction(Request $request) { return $this->render('default/index.html.twig', [
            'base_dir' => realpath($this->getParameter('kernel.project_dir')).DIRECTORY_SEPARATOR,
            'controller_name' => 'DefaultController',
        ]);
    }
    /**
     * @Route("/api/colors", name="colors_route")
     */
    public function colorsAction()
    {
        return  new JsonResponse(array('colors' => ['red', 'green','blue', 'yellow'], "success" => true));
    }
}

As seen, we define the route such that indexAction() will be executed for every route excepting the ones beginning with /api. This way all routes that do not begin with /api/* can be handled via the Vue Router. And the routes beginning with /api/* will be handled by Symfony’s router. Using this, we could very well develop a REST Api in Symfony using the Route scheme /api/Foo/, /api/bar/ and so on.
Note: At this point, if the Symfony debug toolbar seems broken, its totally normal. This is because the DefaultController interferes with the routes which were supposed to be handled by the debug toolbar’s controller. So, you could either disable the debug toolbar from your dev config or else you could update your DefaultController’s route to make it stop interfering. If you choose to update the route, you could set route requirements as shown: requirements={"route"="^(?!.*api|_wdt|_profiler).+"}. You might wanna checkout another recommended way to handle Symfony-Vuejs routing.

Also, in the above controller, we used the view default/index.html.twig which does not exist yet. So lets define this view.

Create the Views

You could begin by emptying your /templates/ directory if it isn’t empty already. We will the define a base view that includes our app’s assets. These assets(css/js files) don’t exist yet, but we will be generating them shortly via Webpack.

// /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/css/style.css') }}">
            <link rel="stylesheet" href="{{ asset('build/js/app.css') }}">
        {% endblock %}
    </head>
    <body>
        {% block body %}{% endblock %}

        {% block javascripts %}
            <script src="{{ asset('build/js/app.js') }}"></script>
        {% endblock %}
    </body>
</html>

We will now define our default view (index.html.twig). This is where we will be initializing our Vue App. So we create a div with an id=”vueApp” that we will reference later.

// /templates/default/index.html.twig
{% extends 'base.html.twig' %}

{% block title %}Hello {{ controller_name }}!{% endblock %}

{% block body %}
<style>
    .example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
    .example-wrapper code { background: #F5F5F5; padding: 2px 6px; }
</style>

<div class="example-wrapper">
    <div id="vueApp"></div>
</div>
{% endblock %}

That’s it. Now the Symfony part of the App is setup. Moving on to setup Vue js as the frontend

Setup Vue Js Front End

We use Symfony’s “Webpack Encore” to generate webpack’s configuration. Webpack Encore simplifies many of Webpacks configurations that newbies would definitely appreciate.
Update your webpack.config.js as follows:

1. Update Webpack Config

// ./webpack.config.js
var Encore = require('@symfony/webpack-encore');
const { VueLoaderPlugin } = require('vue-loader');

Encore
    // the project directory where compiled assets will be stored
    .setOutputPath('public/build/')
    // the public path used by the web server to access the previous directory
    .setPublicPath('/build')
    .cleanupOutputBeforeBuild()                     // empties the outputPath dir before each build
    .enableSourceMaps(!Encore.isProduction())       // enables source maps for Dev
    // .enableVersioning(Encore.isProduction())     // uncomment to create hashed filenames (e.g. app.abc123.css)

    // define the assets of the project
    .addEntry('js/app', './assets/js/index.js')
    .addStyleEntry('css/style', './assets/css/custom.less')
    .enableLessLoader()
    .addLoader({
        test: /\.vue$/,
        loader: 'vue-loader'
    })
    .addPlugin(new VueLoaderPlugin())

    // $/jQuery as a global variable
    .autoProvidejQuery()
    .addAliases({
        vue: 'vue/dist/vue.js'
      })
;

module.exports = Encore.getWebpackConfig();

2. Create Vue.js App Entry point

Webpack encore expects an entry point to your app. As per our configuration above, we have set it to be ./assets/js/index.js. So lets bootstrap our Vue app in this file.

// ./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 }
})

We bootstrapped the App with a component named “app“, so lets quickly define this component:

// ./assets/js/app.vue
<template>
    <div class="container">
        <nav class="navbar is-transparent">
            <div class="navbar-brand">
                <h1>Getting started with Symfony + VueJS</h1>
            </div>
            <div id="vue-menu" class="navbar-menu">
                <div class="navbar-start">
                    <router-link tag='div' to="/" class="navbar-item">Home</router-link>
                    <router-link tag='div' to="hello" class="navbar-item">Hello</router-link>
                    <router-link tag='div' to="notfound" class="navbar-item">Not Found URL</router-link>
                </div>
            </div>
        </nav>
        <div>
            <router-view></router-view>
        </div>
    </div>
</template>

<script>
    export default {
        name: "app"
    }
</script>

<style>
    .container .navbar-item {
        cursor: pointer;
        text-decoration: underline;
    }
    h1 {
        color:blue;
    }
</style>

While bootstrapping, we also provided a router to the app(in ./assets/js/index.js), but haven’t defined it.

3. Create Vue JS Router

We now define a new Vuejs Router to handle the internal routing.

// ./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: '*',
            name: 'notfound',
            component: notfound
        }
    ]
})

We have defined 3 routes and each route is handled by a separate component. So we have a component home.vue for / route, component hello.vue for /hello route and component notFound.vue for the third route which is a wildcard that catches any route which wasn’t matched earlier.

4. Define Vue Components

Lets define the 3 components with some basic markup

// ./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>

5. Build Vue App

Now that all the configurations, components and routes have been configured and defined, all that remains, is to build the assets. Execute yarn run encore dev --watch in your terminal to build assets via webpack encore.
Doing this should populate your public/build/ directory with the necessary built assets. And we have already included them in the base template (base.html.twig).

6. Symfony Vue Integration Output

Now you could head over to http://localhost:8000/ and check your Vue App fired up and running. You can click on the hello, not found urls and see the Vue Router in action which is responsible to update the views. Vue router also catches any wildcard routes as expected, for example: http://localhost:8000/contact/us will show the notfound route view.
However, if you head over to any route beginning with api/, Symfonys router kicks in and handles the response. For example, head over to http://localhost:8000/api/colors that returns a dummy JSON response.

The entire code is available on github. Feel free to mingle around with it.


Going Further

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. Hope that helps 🙂


References
https://www.cloudways.com/blog/symfony-vuejs-app/
https://developer.okta.com/blog/2018/06/14/php-crud-app-symfony-vue
https://medium.com/@rebolon/symfony-is-not-dead-thanks-to-vuejs-99cdf75f57b
https://blog.eleven-labs.com/fr/ssr-symfony-vue/
https://www.youtube.com/watch?v=UjejTX5FOG8
https://nehalist.io/directly-injecting-data-to-vue-apps-with-symfony-twig/
https://blog.elao.com/fr/dev/comment-integrer-vue-js-application-symfony/