Démarrer avec Docker, Symfony et Vue.js

Démarrer avec Docker, Symfony et Vue.js


Dans cet article nous allons vous montrer comment mettre en place une application web avec symfony et Vue.js dans un environnement docker. À la fin de cet article vous aurez un projet prêt au développement. Vous pouvez également retrouver le projet sur le github d’Eleven-labs sur ce dépôt eleven-labs/docker-symfony-vue

ENVIRONNEMENT : Docker

Pour l'environnement nous allons nous baser sur le projet de Maxence POUTORD disponible sur son GitHub auquel nous allons apporter quelques modifications. Dans un premier temps nous changeons de base de données pour passer sur PostgreSQL. Pour ce faire nous modifions le fichier docker-compose.yml se trouvant à la racine de notre projet :

# ... postgres: image: postgres:9.6 ports: - ${POSTGRES_PORT}:5432 environment: POSTGRES_DB: ${POSTGRES_DB} POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} php: build: docker/php7-fpm env_file: ./.env volumes: - ${SYMFONY_APP_PATH}:/var/www/symfony links: - postgres # ...

Ensuite nous ajoutons à notre stack Node JS pour Vue.js ainsi que Redis pour la gestion des sessions. Toujours dans le fichier docker-compose.yml :

# … redis: image: redis:3.2.10 node: build: docker/node volumes: - ${SYMFONY_APP_PATH}:/var/www/symfony command: bash -c "yarn && yarn dev"

Puis nous créons le Dockerfile pour Node JS dans le répertoire docker/node :

FROM node:8 RUN apt-get update && \ apt-get install -y \ curl \ apt-transport-https RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list RUN apt-get update && apt-get install yarn WORKDIR /var/www/symfony

Enfin nous modifions le Dockerfile de PHP qui se trouve dans le répertoire docker/php7-fpm pour installer la librairie cliente de PostgreSQL libpq-dev ainsi que l’extension pdo_pgsql pour PHP :

# ... RUN apt-get update && \ apt-get install -y \ git \ unzip \ libpq-dev # RUN docker-php-ext-install pdo pdo_pgsql # ...

Dans le même fichier, nous en profitons aussi pour supprimer l’alias de la commande pour Symfony 2 RUN echo 'alias sf="php app/console"' >> ~/.bashrc

Et nous mettons à jour nos variables du fichier .env.dist se trouvant à la racine du projet :

# PATH DIR SYMFONY_APP_PATH=./ LOGS_DIR=./docker/logs # DATABASE POSTGRES_DB= POSTGRES_USER= POSTGRES_PASSWORD= POSTGRES_PORT= # PORT WEB WEB_PORT= ELK_PORT= # SYMFONY SECRET= #SMTP SMTP_USER= SMTP_PASSWORD= SMTP_HOST= SMTP_TRANSPORT= #REDIS REDIS_DNS=

BACKEND : Symfony

Maintenant que notre environnement est prêt, nous installons Symfony, je vous invite à suivre le tutoriel officiel sur le site de symfony. Nous allons personnaliser Symfony pour notre projet et pour ce faire nous supprimons l'appel à trois scripts exécutés lors du composer install ou du composer update, qui se trouvent dans le fichier composer.json à la racine du projet, et qui sont :

  • installRequirementsFile
  • prepareDeploymentTarget,
  • buildParameters.

Ce qui nous donne :

// … { "deploy-scripts": [ "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap", "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache", "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installAssets" ], "symfony-scripts": [ "@deploy-scripts" ], "post-install-cmd": ["@symfony-scripts"], "post-update-cmd": ["@symfony-scripts"] }, // ...

Nous pouvons donc supprimer les lignes de contrôle d’accès du fichier app_dev.php qui se trouvent dans web/ (ATTENTION: ce fichier ne devra plus se trouver dans un environnement de production) :

<?php // ... if (isset($_SERVER['HTTP_CLIENT_IP']) || isset($_SERVER['HTTP_X_FORWARDED_FOR']) || !(in_array(@$_SERVER['REMOTE_ADDR'], ['127.0.0.1', '::1']) || PHP_SAPI === 'cli-server') ) { header('HTTP/1.0 403 Forbidden'); exit('You are not allowed to access this file. Check '.basename(__FILE__).' for more information.'); } // ...

Nous pouvons supprimer les fichiers web/config.php et app/config/parameters.yml.dist, et éditer le fichier app/config/parameters.yml comme ceci :

parameters: # Database parameters database_host: postgres database_port: '%env(POSTGRES_PORT)%' database_name: '%env(POSTGRES_DB)%' database_user: '%env(POSTGRES_USER)%' database_password: '%env(POSTGRES_PASSWORD)%' # Mailer parameters mailer_transport: '%env(SMTP_TRANSPORT)%' mailer_host: '%env(SMTP_HOST)%' mailer_user: '%env(SMTP_USER)%' mailer_password: '%env(SMTP_PASSWORD)%' # Secret secret: '%env(SECRET)%' # Redis parameters redis_dsn: '%env(REDIS_DNS)%' redis_options: ~ session_ttl: 86400

Notre projet est installé et personnalisé, il ne reste plus qu'à installer quelques bundles :

  • friendsofsymfony/rest-bundle, pour la mise en place rapide d’une API REST
  • jms/serializer-bundle, pour faciliter la sérialisation et désérialisation des données
  • predis/predis et snc/redis-bundle, pour la communication avec redis et la gestion des sessions
  • (optionnel) doctrine/doctrine-fixtures-bundle, pour générer des données

Avec cette commande, nous les installons docker-compose exec -T --user www-data php composer require friendsofsymfony/rest-bundle jms/serializer-bundle predis/predis snc/redis-bundle doctrine/doctrine-fixtures-bundle

Nous les référençons dans app/AppKernel.php :

<?php // ... class AppKernel extends Kernel { public function registerBundles() { $bundles = [ // ... new FOS\RestBundle\FOSRestBundle(), new JMS\SerializerBundle\JMSSerializerBundle(), new Snc\RedisBundle\SncRedisBundle(), new AppBundle\AppBundle(), ]; if (in_array($this->getEnvironment(), ['dev', 'test'], true)) { // ... $bundles[] = new Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle(); // .. } // ... }

Et nous les configurons :

# SerializerBundle Configuration jms_serializer: metadata: auto_detection: true # FOSRestBundle Configuration fos_rest: body_converter: enabled: true validate: true serializer: serialize_null: true param_fetcher_listener: true routing_loader: default_format: json include_format: false view: view_response_listener: true format_listener: rules: - { path: '^/api', priorities: ['json'], fallback_format: 'json' } - { path: '^/', stop: true } # RedisBundle Configuration snc_redis: clients: session_client: type: predis logging: false alias: session_client dsn: %redis_dsn% options: %redis_options% session: client: session_client prefix: app_session_ ttl: '%session_ttl%'

Voilà qui est fait pour la partie docker et symfony. Nous allons maintenant passer à la partie Vue.js

FRONTEND : Vue.js

Si vous n’êtes pas familier avec Vue.js, vous pouvez visiter la page officielle du framework. Vous trouverez des tutoriels très bien faits et traduits en français.

Tout d’abord, initialisons notre gestionnaire de package :

symfony-vue $ yarn init yarn init v1.3.2 question name (symfony-vue): question version (1.0.0): question description: symfony <3 vue question entry point (index.js): question repository url: question author: Wilson question license (MIT): question private: success Saved package.json ✨ Done in 49.10s. symfony-vue $

Pour nous permettre d’utiliser ES6 tout en restant compatible, nous utilisons babel :

/* .babelrc */ { "presets": [ [ "env", { "targets": { "browsers": [ "last 2 versions", "Chrome >= 52", "FireFox >= 44", "Safari >= 7", "ie >= 10", "last 4 Edge versions", ], }, }, ], "stage-2", "vue", ], }

Et pour gérer nos différents bundles nous utilisons Webpack. Voici notre configuration :

/* app/config/webpack.config.js */ const path = require('path'); const ExtractTextPlugin = require('extract-text-webpack-plugin'); const webpack = require('webpack'); const autoprefixer = require('autoprefixer'); module.exports = { entry: { page1: './src/AppBundle/Resources/js/page1/entrypoint.js', page2: './src/AppBundle/Resources/js/page2/entrypoint.js', }, output: { path: path.resolve(__dirname, '../../src/AppBundle/Resources/public'), filename: 'js/[name].js', }, module: { rules: [ { test: /\.vue$/, loader: 'vue-loader', options: { scss: 'style!css!sass', }, }, { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/, }, { test: /\.s[a|c]ss$/, use: ExtractTextPlugin.extract({ fallback: 'style-loader', use: [ 'css-loader', 'postcss-loader', { loader: 'postcss-loader', options: { plugins: [ autoprefixer({ remove: false, browsers: [ 'last 2 versions', 'Chrome >= 52', 'FireFox >= 44', 'Safari >= 7', 'ie >= 10', 'last 4 Edge versions', ], }), ], }, }, 'sass-loader', ], }), }, { test: /\.(jpg|png|svg)$/, loader: 'file-loader', }, ], }, plugins: [ new ExtractTextPlugin({ filename: 'css/style.css', }), new webpack.LoaderOptionsPlugin({ options: { sassLoader: { includePaths: [path.resolve(__dirname, '../node_modules')] } }, }), new webpack.DefinePlugin({ 'process.env': { NODE_ENV: `'${process.env.NODE_ENV}'`, }, }), ], };

Nous avons aussi configuré Webpack pour utiliser SASS.

Nous pouvons ajouter les scripts suivants à notre package.json pour lancer et builder notre application :

/* package.json */ // ... "dev": "NODE_ENV=dev webpack --config ./app/config/webpack.conf.js --devtool source-map --debug --watch --display-error-details", "build": "NODE_ENV=production webpack --config ./app/config/webpack.conf.js --progress --colors --optimize-minimize", // ...

Comme vous pouvez le voir, nous avons deux entrypoints différents dans notre configuration Webpack. De ces deux entrypoints, Webpack va générer deux bundles. De cette façon, nous allons pouvoir intégrer des applications Vue.js à différentes pages Twig.

Pour cet exemple, nous allons créer un composant “message” que nous allons appeler dans deux pages différentes. Créons d’abord notre composant, qui prend en propriété “text” :

/* src/AppBundle/Resources/js/components/message/index.vue */ <template> <div class="message"> {{ text }} </div> </template> <script> export default { name: 'message', props: { text: { type: String, required: true } } } </script> <style lang="scss" scoped> </style>

Appelons-le dans notre page 1 :

/* src/AppBundle/Resources/js/page1/index.vue */ <template> <Message text="Hello page 1" /> </template> <script> import Message from '../components/message/index.vue'; export default { name: 'Page1Container', data() { return { }; }, components: { Message, }, }; </script>

Il faut ensuite créer notre application Vue.js :

/* src/AppBundle/Resources/js/page1/entrypoint.js */ import Vue from 'vue'; import Page1 from './index.vue'; export const vm = new Vue({ el: '#app1', components: { app: Page1, }, render: h => h('app'), });

Puis nous appelons notre application dans la page Twig :

/* src/AppBundle/Resources/views/App/index.html.twig */ {% extends '@App/App/layout.html.twig' %} {% block container %} <div id="app1"></div> <script type="text/javascript" src="{{ asset('bundles/app/js/page1.js') }}"></script> {% endblock %}

Nous faisons de même pour la page 2 :

/* src/AppBundle/Resources/js/page2/index.vue */ <template> <Message text="Hello page 2" /> </template> <script> import Message from '../components/message/index.vue'; export default { name: 'Page2Container', data() { return { }; }, components: { Message, }, }; </script>
/* src/AppBundle/Resources/js/page2/entrypoint.js */ import Vue from 'vue'; import Page2 from './index.vue'; export const vm = new Vue({ el: '#app2', components: { app: Page2, }, render: h => h('app'), });
/* src/AppBundle/Resources/views/App/page2.html.twig */ {% extends '@App/App/layout.html.twig' %} {% block container %} <div id="app2"></div> <script type="text/javascript" src="{{ asset('bundles/app/js/page2.js') }}"></script> {% endblock %}

COMMUNICATION

Parfois nous avons besoin d’envoyer des informations de Symfony vers Vue.js. Selon la taille de l’information et sa sensibilité, nous pouvons passer par une requête API, ou par un Data Layout.

Commençons par le Data Layout. Le Data Layout est un objet déclaré globalement, qui sera donc accessible par notre template Twig, et notre application Vue.js.

Nous allons d’abord définir un objet dataLayout au niveau le plus haut de nos templates Twig :

/* app/Resources/views/app.html.twig */ <!DOCTYPE html> <html lang="{{ app.request.locale }}"> <head> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta charset="UTF-8" /> <title>{% block title %}{% endblock %}</title> {% block stylesheets %}{% endblock %} <link rel="icon" type="image/x-icon" href="{{ asset('favicon.ico') }}" /> </head> <body> {% block data_layout %} <script type="text/javascript" id="dataLayout"> var dataLayout = {}; </script> {% endblock %} {% block body %}{% endblock %} {% block javascripts %}{% endblock %} </body> </html>

Veillez à bien respecter l’ordre d’appel des scripts : en premier le dataLayout et ensuite l’application Vue.js. Sinon vous n’aurez pas accès à l’objet global dataLayout, car il ne sera pas encore créé. Ensuite nous allons envoyer des données depuis Symfony, et les récupérer dans notre Twig :

/* src/AppBundle/Controller/appController.php */ // ... public function indexAction(): Response { return $this->render('@App/App/index.html.twig', [ 'message'=>'hello !' ]); } // ...
/* src/AppBundle/Resources/views/App/index.html.twig */ // ... {% block data_layout %} {{ parent() }} <script type="text/javascript"> dataLayout.message = "{{ message }}"; </script> {% endblock %} // ...

Il faut maintenant récupérer le message depuis Vue.js. Modifions notre index.vue de la page 1 :

/* src/AppBundle/Resources/js/page1/index.vue */ <template> <Message :text="message" /> </template> // ... data() { return { message: '', }; // ... mounted() { this.$set(this, 'message', dataLayout.message); }, }; </script>

Plus classiquement, nous pouvons récupérer les informations depuis un appel API. Créons une route “/hello/:astronaut” :

/* src/AppBundle/Controller/apiController.php */ /** * Class ApiController * @package AppBundle\Controller */ class ApiController extends FOSRestController { /** * @Rest\View() * @Rest\Get("hello/{astronaut}", defaults={"astronaut" = null}) * * @param string $astronaut * * @return string */ public function getHelloAction(string $astronaut = null) { return isset($astronaut) ? "Hello $astronaut" : "Hello Astronaut"; } }

Et modifions notre page2/index.vue :

/* page2/index.vue */ <template> <Message :text="message" /> </template> // ... data() { return { message: '', }; // ... mounted() { fetch('/api/hello/wilson') .then(response => response.json) .then(({ message }) => this.$set(this, 'message', message)) }, }; </script>

Et voilà, vous pouvez maintenant faire communiquer votre application Symfony avec Vue.js

EXTRA : UN SCRIPT POUR SE SIMPLIFIER LA VIE

Voici un petit extra pour se simplifier la vie. Comme vous avez pu le voir, pour l’installation de bundle, nous devions écrire une commande assez longue donc pour ne pas la réécrire entièrement, je vous propose de créer un script dédié à notre projet. Nous définissons dans un premier temps la fonction d'entrée et la fonction d’information sur l'usage comme ceci :

#!/bin/bash # ... usage () { echo "usage: bin/docker COMMAND [ARGUMENTS] init Initialize the project start Start project stop Stop project bash Use bash inside the app container exec Executes a command inside the app container destroy Remove all the project Docker containers with their volumes console Use the Symfony console composer Use Composer inside the app container test Run test project inside the app container " } main () { declare CMD=$1 if [ -z $1 ]; then usage exit 0 fi if [[ ! $1 =~ ^init|start|stop|bash|destroy|console|composer|exec|tests$ ]]; then echo "$1 is not a supported command" exit 1 fi $@ } main $@

Ensuite nous implémentons nos fonctions. Je vais prendre uniquement l’exemple de composer, mais vous pouvez retrouver l'intégralité du script ici.

#!/bin/bash # … # run Composer inside the app container composer () { declare ARGS=$@ docker-compose exec -T --user www-data php composer $ARGS } # …

Et voilà le tour est joué ! Maintenant au lieu d’écrire docker-compose exec -T --user www-data php compose [repository/bundleName], nous écrirons bin/app compose [repository/bundleName]

EN CONCLUSION

Vous disposez d’un projet configuré pour utiliser la puissance de Symfony et la simplicité de Vue.js. N’hésitez pas à nous poser des questions ou à nous laisser un commentaire !

Auteur(s)

Nicolas Grévin

Nicolas Grévin

Ingénieur DevOps SRE spécialisé en conteneurisation, Kubernetes, CI/CD, cloud, Infrastructure as Code et outillage. Engagé dans la Green IT et développeur passionné.

Voir le profil
Jean-François Moreno

Jean-François Moreno

Développeur Javascript

Voir le profil

Vous souhaitez en savoir plus sur le sujet ?
Organisons un échange !

Notre équipe d'experts répond à toutes vos questions.

Nous contacter

Découvrez nos autres contenus dans le même thème

À la découverte de l'Anchor positioning API

La nouvelle Anchor positioning API en CSS

L'Anchor positioning API est arrivée en CSS depuis quelques mois. Expérimentale et uniquement disponible à ce jour pour les navigateurs basés sur Chromium, elle est tout de même très intéressante pour lier des éléments entre eux et répondre en CSS à des problématiques qui ne pouvaient se résoudre qu'en JavaScript.