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
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 !