Image created with resources from Freepik (image, image, image)

Our journey from Symfony to Vue — Part 2

Mario Sanz
Jeff Tech
Published in
8 min readDec 19, 2018

--

In part 1 of this article, we detailed why we decided to move all the Mr Jeff web apps from Symfony to a Javascript frontend stack, why we chose Vue, and how we planned to tackle the migration using Encore. In this new chapter, first we’ll see how we installed and configured Encore to fit our needs, then created the simplest Vue app possible to test that everything worked, and finally how we successfully rewrote one of our Symfony views as a Vue app running inside our website.

The real journey starts here — The ocean unfolds before us, full of winds and storms and krakens. We’ll need brave sailors, come sail with us!

Unfurling the sails: Setting up Encore

The official Encore docs are quite good, with step by step code examples. We made some small changes in order to adapt the configuration to our project structure, but in general, the code should be very similar in all projects.

As always, first step is installing things. These are all the packages we’ll need for now:

$ yarn add --dev @symfony/webpack-encore
$ yarn add --dev vue vue-loader@^14 vue-template-compiler
$ yarn add --dev sass-loader node-sass

Then, we create a webpack.config.js in the project root and specify the Webpack config we want for our project. Encore gives us nice one-liners for many of the common Webpack config chunks, making it a breeze to set up most of the workflow. So, let’s start simple:

var Encore = require('@symfony/webpack-encore');Encore
.setOutputPath('web/build/')
.setPublicPath('/build')
.addEntry('app', './vue/src/main.js') .cleanupOutputBeforeBuild()
.enableSourceMaps(!Encore.isProduction())
.enableVersioning(Encore.isProduction())
.enableVueLoader()
.enableSassLoader();
module.exports = Encore.getWebpackConfig();

Easy enough — for now, all we did was copy-pasting from a few sections of the Encore docs. We only adjusted the path for our entry point, since we wanted to name our project folder vue instead of assets. By the way, that vue folder is where all our Vue code will live, and we put it in the top level of the Symfony project, but you can put it wherever you prefer.

That entry point basically means ‘Hey Encore, go to that file, import and process everything you find there, package it into a file called app, and throw it into the output directory we just set’. If we want to have multiple apps (which we’ll do later), we just have to add more entry points here. Nice!

Apart from that, the rest of the file reads very easy — we’re just asking Webpack to clean our output folder before every new build, telling it to enable source maps and asset versioning, and enabling the Vue and Sass loaders we installed earlier. Ah, what a joy is to be able to just drop these few lines instead of spending hours fighting with nightmare-inducing Webpack config objects! 🙌

Weathering the storm: Our first Vue app

Let’s keep going. Next step is creating that main.js entry point we just pointed to: we have to import the Vue library and a Vue component, and then mount the component on an HTML element. It should look like this:

import Vue from 'vue'
import App from './components/App'
new Vue({
el: '#app',
template: '<App/>',
components: { App },
});

Of course, we also need that App component, so let’s create a components folder and an App.vue file inside it. Our first Vue component!

<template>
<h1>Look ma! A Vue component!</h1>
</template>
<script>
</script>
<style lang="scss">
</style>

As a last thing, since we enabled asset versioning, we need to point Symfony to a manifest file that Webpack will create for us. So we open config.yml and add this line:

framework:
assets:
json_manifest_path: '%kernel.project_dir%/web/build/manifest.json'

(That last line should be all in one line, it just doesn’t fit in Medium’s width. Check reference here.)

So that should do it! Let’s tell Encore to process all this and generate our app, with this command:

$ yarn encore production

If a few new files showed up in the web/build folder, yay it worked! But wait, we’re not seeing our app yet… 🤔 Ah, of course, we have to go to our template and tell where it should render. We’ll use a fresh new template, but you can choose any element in any template:

{% extends '::layout.html.twig' %}{% block title %}Test page{% endblock %}{% block stylesheets %}
<link rel="stylesheet" href="{{ asset('build/app.css') }}"/>
{% endblock %}
{% block content %}
<div id="app"></div>
{% endblock %}
{% block javascripts %}
<script src="{{ asset('build/app.js') }}"></script>
{% endblock %}

The empty <div> has the same ID we chose in the main.js file, and those app.css and app.js files are the ones generated by Encore when we ran the build command.

Refresh, and BOOM! If the contents of our component are on the screen, then everything went fine. Our template rendered an #app element, and loaded a CSS file, which is empty since we didn’t add any styles yet, and a JS file, which runs all the necessary code for creating our app and putting it in the #app element, just as we specified earlier.

The kraken emerges: Our first REAL Vue app

So, now that our setup is working, let’s build a real world example. In our project, the first place where we wanted to introduce Vue is in the User Settings screen — it’s a simple one, but it required major changes, so sounded like the perfect test case for starting the transition. So, instead of messing with the already messy Twig templates and jQuery scripts, we’ll scratch the whole thing and replace it with a shiny new Vue app.

In other words, what we’re going to do is to create a second Vue app, called SettingsPage. It will live in one of our Twig templates, but all the layout will be generated by Vue at render time. Let’s do it step by step, replicating the process we just did for our test component.

As before, the first step is creating a new entry point in our Webpack config, which will take care of our new app.

var Encore = require('@symfony/webpack-encore');Encore
// ...
.addEntry('app', './vue/src/main.js')
.addEntry('pageSettings', './vue/src/pageSettings.js')
// ...

Now we need that pageSettings.js, which will import Vue and a PageSettings component and will create the app. It’s very similar to the one we used for testing:

import Vue from 'vue'
import PageSettings from './components/PageSettings'
new Vue({
el: '#page-settings',
template: '<PageSettings/>',
components: { PageSettings },
});

Of course, we also need that PageSettings.vue component, that will contain the actual layout and logic of the page. It could be something like this:

<template>
...
<form class=”form” @submit.prevent=”changePassword”>
<div class="form-group">
<label>Current password</label>
<input type="password" v-model="passwordCurrent">
</div>
<div class="form-group">
<label>New password</label>
<input type="password" v-model="passwordNew">
</div>
<div class="form-group">
<label>Repeat password</label>
<input type="password" v-model="passwordRepeat">
</div>
<button type="submit" :disabled="!isFormValid">Save</button
</form>
...
</template>
<script>
import UserService from ‘@/services/UserService’
export default {
computed: {
isFormValid () {
return ... // Just a bunch of form validation logic here
}
},
methods: {
changePassword () {
const data = {
currentPassword: this.passwordCurrent,
password: this.passwordNew
}
UserService.updatePassword(this.userId, data)
.then(res => {
// Handle OK response
})
.catch(err => {
// Handle error response
})
}
},
created () {
this.userId = sessionStorage.getItem(‘userId’)
},
data () {
return {
userId: null,
passwordCurrent: ‘’,
passwordNew: ‘’,
passwordRepeat: ‘’
}
}
}
</script>
<style lang="scss">
</style>

I know, that was a big chunk of code for an article, sorry about that. It’s actually a simplified version of our settings page component, but we thought showing something real would be more helpful than just another barebones component. For example, here you can see how we’re importing a service (just a JS file in some other folder) and using it to call our API with the values set in the form.

One interesting detail here: our API endpoint for changing the user password requires us to pass it the user ID, but since our view isn’t rendered by Symfony anymore, we don’t have access to that. So the question is, how can we pass some initial data from Symfony to our Vue apps? There are several ways to do it, but for this use case we went with one of the simplest ones — using the browser storage. As you can see, in the created() hook (all about Vue hooks here) we are reading the user ID from the session storage. We’ll see how we stored it there in the next chunk of code.

Lastly, our new app needs a place to live. We’ll go to our settings.html.twig template, scratch most of the Twig and jQuery code there, and replace it with the references to the CSS and JS files that Encore will create for us. It should look something like this:

{% extends '::base_layout.html.twig' %}
{% block title %}Settings{% endblock %}
{% block stylesheets %}
<link rel="stylesheet" href="{{ asset('build/pageSettings.css') }}"/>
{% endblock %}
{% block content %}
<div id="page-settings"></div>
{% endblock %}
{% block javascripts %}
<script type="text/javascript">
sessionStorage.setItem('userId', '{{ user.id }}');
</script>
<script src="{{ asset('build/pageSettings.js') }}"></script>
{% endblock %}

As noted earlier, here we’re accessing the data from our controller for saving the user ID to the session storage, of course before running the javascript for our new app, so the ID is already there when the app starts to run.

The rest of the template is exactly the same as the test one we did before, so if we run our build command again:

$ yarn encore production

Boom! If we now visit the route for our settings page, our shiny new Vue app completely replaced the old view!

Too many giant tentacles: A basic setup is usually not enough

This setup worked fine for the first few views we wanted to replace, and served well to our purpose of testing if Vue was the tool we needed for the migration we wanted. However, as it always happens, basic setups start to show their limitations as the project starts to grow, and you start to need more tools if you want to keep your sanity.

As mentioned, this approach of creating one independent app for each view worked fine with the couple of views we used for our pilot: the very simple Settings page we showed here, and a new feature that only needed a list, an item detail section, and a few API calls. The pilot went great and the decision of Vuefying everything got green light, but we also detected a few signs that the current setup could be improved. Do we need to maintain a Symfony controller, a template, and an entry point for every Vue app we create, if all of them are almost identical? How can we add Webpack configurations that Encore doesn’t include? What if several of our views need to communicate or receive the same initial data? How can we use our Symfony env variables in Vue if they only exist on the server side? Can we use the hot reloading capabilities that Webpack offers? And if that hot reloading local server creates its own localhost, how can we access it if we’re working in Docker containers?

So, before starting to migrate more views, we took a bit of time to do some research and answer all those questions. And as it turned out, the answers to most of them were simpler than we thought… But we’ll leave that for the next chapter.

Yes, we’ll need to deal with some more config files before being able to fully focus on migrating our apps. But fear not, the roughest part of the journey is over — from here, nothing but calm waters and blue skies ahead. Ahoy!

Update: Part 3 is out now!

Stay tuned for the rest of the story! We will detail how we added the vue-router once the number of Vue apps started to grow, and how we tweaked our Encore config to improve the development workflow and to be able to work with Docker, Babel plugins and other tools. The treasure island awaits, stay with us!

--

--