Aller plus loin avec Symfony et Docker

Après avoir développé une application Symfony avec Docker : Créer une image docker avec Symfony, nous aller essayer d’en apprendre d’avantage en utilisant une base de données PostgreSQL.

Rappel

Nous allons poursuivre notre aventure en nous servant du code écrit lors de la dernière séance : https://github.com/gdarquie/symfony-docker.

Objectif

Notre objectif tient en peu de mots : lier notre application Symfony à une BD postgreSQL et faire fonctionner le tout avec docker-compose.

Ajouter doctrine au projet

Pour utiliser postgres, nous devons ajoutons quelques composants.

Nous installons les paquets depuis notre local, avec l’instance de composer se trouvant sur notre machine.

Pour installer Doctrine, nous devons récupérer les deux paquets suivants:

composer require symfony/orm-pack
composer require symfony/maker-bundle --dev

Pour toutes infos complémentaires sur cette procédure, je vous recommande la doc Symfony : https://symfony.com/doc/current/doctrine.html

Ajouter l’image docker de la BD

Nous devons ajouter une nouvelle image docker pour la base de données PostgreSQL. Nous utilisons directement une image générique: postgres:10-alpine, ce qui fait que nous n’avons rien à ajouter dans notre dossier /docker, il nous suffit d’appeler l’image dans docker-compose.yml où nous ajoutons le code suivant:

db:
container_name: db
image: postgres:10-alpine
ports:
- "15432:5432"

D’ailleurs, petite parenthèse, il est recommandé d’utiliser le plus possible les images officielles qui sont maintenues par la communauté.

Nous associons au port 15432 postgres sur notre machine pour éviter toute confusion avec notre éventuelle installation locale.

Voici le code complet de docker-compose.yml:

version: '3'
services:
app:
build:
context: .
dockerfile: docker/Dockerfile
depends_on:
- "db"
image: symfony
ports:
- 81:80
volumes:
- .:/srv/app/
db:
container_name: db
image: postgres:10-alpine
ports:
- "15432:5432"

Vous remarquerez que j’ai ajouté au service “app”, la proprité “depends_on” qui nous permet de lier le service symfony à “db”.

Tout le reste demeure inchangé.

Ajout des variables d’environnement

Nous allons maintenant ajouter les variables d’environnement qui écraseront celles des fichiers .env.

Nous commençons par DATABASE_URL qui permettra à notre application Symfony d‘accéder à la base Postgres.

environment:
DATABASE_URL: "pgsql://postgres:password@db:5432/symfony"

DATABASE_URL est une sorte de combinaison de toutes les variables permettant d’accéder à la base :

  • “postgres” = le nom de l’user psql
  • “password” = le mot de passe associé à l’user psql
  • db:5342 = l’host et le port utilisé par psql
  • “symfony” = le nom de la base de données que nous utilisons.

Adminer

Afin de pouvoir facilement interagir avec la BD sur docker, nous ajoutons un nouveau Adminer qui est une sorte de PHP My Admin utilisable avec Postgres.

adminer:
image: adminer
restart: always
ports:
- 18080:8080

Là encore, nous avons choisi un port local différent du port par défaut pour éviter tout conflit éventuel.

docker-compose.yml final

Le code final est alors :

version: '3'
services:
app:
build:
context: .
dockerfile: docker/Dockerfile
depends_on:
- db
image: symfony
ports:
- 81:80
volumes:
- .:/srv/app/
environment:
DATABASE_URL: "pgsql://postgres:password@db:5432/symfony"
db:
container_name: db
image: postgres:10-alpine
ports:
- 15432:5432
adminer:
image: adminer
restart: always
ports:
- 18080:8080

Modifier l’image docker Symfony

Afin de prendre en compte la BD, nous modifions l’image du dossier docker qui permet de construire l’application Symfony à partir d’une image officielle PHP.

Nous rétrogradons en version 7.2 pour nous faciliter la tâche et installons plusieurs paquets nécessaires (j’espère qu’ils le sont tous, avis aux amat.eur/tric.es pour en écarter les inutiles).

FROM php:7.2-alpine

RUN apk add --no-cache \
curl \
gnupg \
git \
imagemagick \
icu-dev \
postgresql-dev \
zlib-dev \
netcat-openbsd

RUN docker-php-ext-configure pgsql -with-pgsql=/usr/include/postgresql/
RUN
docker-php-ext-install \
bcmath \
intl \
mbstring \
pcntl \
pdo_pgsql \
pgsql \
shmop \
zip

COPY . /srv/app

WORKDIR /srv/app

CMD php -S 0.0.0.0:80 public/index.php

Avec “RUN app…”, nous téléchargeons les paquets, puis avec “Run docker-php-ext-configure”, nous créons un utilisateur postresql (je n’en suis pas sûr du tout) et enfin avec docker-php-ext-installons plusieurs extensions suivantes pour psql.

docker-php-ext-configure et docker-php-ext-install sont des helpers de l’image PHP de docker qui facilitent l’installation d’extensions https://docs.docker.com/samples/library/php/#how-to-install-more-php-extensions

Nous ajoutons un workdir qui nous permet de définir le contexte à partir duquel nous lançons toutes nos opérations depuis docker, à savoir /srv/app . De cette manière nous n’avons plus besoin de préciser /srv/app/public/index.php.

A noter également que nous copions désormais notre contenu local dans /srv/app et non plus simplement /app pour nous rapprocher de ce qui semble être une bonne pratique.

Configuration de doctrine

Pour connecter notre application Symfony à une base de données postgres, nous devons la configurer, c’est l’ORM Doctrine qui fait le lien entre notre code PHP et notre BD.

Nous ajoutons la conf suivante à /config/packages/doctrine.yaml :

doctrine:
dbal:
# configure these for your database server
url: '%env(DATABASE_URL)%'

Et dans le fichier .env (même si cette variable sera finalement écrasée par celle indiquée dans le docker-compose, elle nous sera utile pour certaines générations lancées depuis app) :

DATABASE_URL="pgsql://postgres:password@db:15432/symfony"

Ce qui nous donne le code final suivant :

parameters:
# Adds a fallback DATABASE_URL if the env var is not set.
# This allows you to run cache:warmup even if your
# environment variables are not available yet.
# You should not need to change this value.
env(DATABASE_URL): ''

doctrine:
dbal:
# configure these for your database server
url: '%env(DATABASE_URL)%'
orm:
auto_generate_proxy_classes: true
naming_strategy: doctrine.orm.naming_strategy.underscore
auto_mapping: true
mappings:
App:
is_bundle: false
type: annotation
dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity'
alias: App

Relancer et recréer les images

Nous ajoutons les lignes suivantes à notre Makefile :

down:
docker-compose down

up:
docker-compose up -d

reload: down up

“down” (contrairement à stop) détruit les containers et images qui ont été préalablement construites par docker-compose (voir documentation).

“reload” nous permet de reconstruire dans la volée ces images. Nous l’utilisons ici pour réinitialiser nos images et containers avec notre code.

Nous nous retrouvons face à une application Symfony connectable à une base de données Postgres mais pas encore connectée, la nuance est importante. Nous n’avons pour le moment, ni la base de données, ni le code permettant de faire appel à elle.

Création de l’entité Article

Nous devons d’abord créer une entité Symfony, une classe PHP liée par l’ORM Doctrine à notre BD.

Nous installons le bundle Maker depuis notre environnement local:

composer require symfony/maker-bundle --dev

Nous utilisons les commandes symfony:

php bin/console make:entity

Si jamais vous êtes curieu.se/x n’hésitez pas à consulter la documentation Symfony : https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html

Nous créons une entité Article.php avec un “title” (string 255) et une “description” (string 500) de façon à ce que le code corresponde à :

<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
*
@ORM\Entity(repositoryClass="App\Repository\ArticleRepository")
*/
class Article
{
/**
*
@ORM\Id()
*
@ORM\GeneratedValue()
*
@ORM\Column(type="integer")
*/
private $id;
    /**
*
@ORM\Column(type="string", length=255)
*/
private $title;
    /**
*
@ORM\Column(type="string", length=500, nullable=true)
*/
private $description;
    public function getId(): ?int
{
return $this->id;
}
    public function getTitle(): ?string
{
return $this->title;
}
    public function setTitle(string $title): self
{
$this->title = $title;
        return $this;
}
    public function getDescription(): ?string
{
return $this->description;
}
    public function setDescription(?string $description): self
{
$this->description = $description;
        return $this;
}
}

Ajout d’une page /articles

Une fois l’entité ajoutée, nous allons écrire le code du contrôleur qui appellera notre page, voici le code:

/**
*
@Route("articles", methods={"GET","HEAD"}, name="articles")
*/
public function getArticles()
{
$articles = $this->getDoctrine()->getRepository(Article::class)->findAll();
    return $this->render('articles.html.twig', array(
"articles" => $articles
));
}

Nous récupérons avec la variable $articles tous les articles se trouvant dans la BD et nous les retournons en paramètre de la vue articles.html.twig que nous allons justement créer.

Nous ajoutons cette vue dans le dossier /template avec le code :

<!doctype html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Symfony avec Docker</title>
</head>
<body>
    {% for item in articles %}
<h1>{{ item.title }}</h1>
<p>{{ item.description }}</p>
{% endfor %}
</body>
</html>

Nous créons une simple page HTML, dans le body, nous utilisons un foreach pour afficher le titre et la description de chaque article présents dans la BD.

Création de la base de données

Et justement, d’articles nous n’en avons guère ; nous n’avons même pas encore créé de BD.

Pour y remédier, nous allons nous servir de la commande Symfony “php bin/console doctrine:database:create”.

Pour lancer cette commande depuis le contexte du shell du service app, nous utiliserons “docker-compose exec app sh -c”.

Nous ajoutons le tout dans notre Makefile.

create-db:
docker-compose exec app sh -c 'php bin/console doctrine:database:create'

Et nous lançons “make create-db”, ça y est la base est crée. Elle est vide, mais elle existe.

Exécution des commandes avec docker-compose

Si nous avions voulu, nous aurions aussi pu lancer cette commande après nous être connecté au shell d’app.

Pour lancer des commandes dans le contexte de notre docker depuis un service, il faut exécuter la commande docker-compose exec <nom de l’app> sh.

Nous en profitons pour ajouter un raccourci pour l’app Symfony dans le Makefile :

bash-app:
docker-compose exec app sh

Pour en savoir plus, je vous invite à consulter l’article suivant :

Pour tester une autre façon de faire, je vous propose de lancer la commande “php bin/console doctrine:generate:database”, qui permet de mettre à jour notre BD en fonction de notre Mapping doctrine, non pas en une fois comme on pourrait le faire en ajoutant ce bout de code à notre Makefile :


generate-db:
docker-compose exec app sh -c 'php bin/console doctrine:schema:update --force'

Mais en nous connectant d’abord “manuellement” à notre console:

make bash-app

Puis en lançant depuis le shell, depuis le contexte docker, la commande :

php bin/console doctrine:schema:update --force

Avec cette dernière opération, Doctrine a construit pour nous la table article en fonction des annotations que nous avons écrites dans Article.php.

Notre BD existe et elle comporte une table article, toutefois elle est toujours vide, nous allons maintenant la remplir.

Création d’un l’article

Nous utiliserons Adminer — que nous avons installer plus tôt avec notre docker-compose — pour ajouter un article.

Pour utiliser Adminer, il faut que notre user postgres dispose d’un mot de passe.

Si c’est déjà le cas, passer directement cette étape.

Ajout d’un mot de passe à l’utilisateur postgres

Nous nous connectons au shell de notre BD avec make bash-db:

bash-db:
docker-compose exec db sh

Puis à la console PostgreSQL :

psql -U postgres

Nous y écrivons :

alter user postgres password 'password';

Ca y est, notre user postgres a maintenant un mot de passe, nous avons choisi “password”. Pas besoin de changer notre .env ou la variable DATABASE_URL de doctrine-compose.yml car nous avions déjà anticipé ce mot de passe.

Ajout de l’article avec Adminier

Nous nous rendons à présent à l’adresse : 0.0.0.0:18080.

Nous nous connectons en choisissant comme système “postgreSQL”, comme serveur “db”, comme utilisateur “postgres”, comme mot de passe “password” et comme Base de données “symfony”.

Ensuite nous cliquons sur requête SQL et ajoutons la requête :

INSERT INTO article (id, title, description) VALUES (1, 'Exemple de titre', 'Exemple de contenu');

Voilà, notre article est créé.

Le résultat

En nous rendant sur la page http://127.0.0.1:81/articles, nous pouvons découvrir ce “magnifique” résultat :

Rendu final : affichage des articles

Félicitations, vous avez réussi à combiner docker, docker-compose, Symfony, Doctrine et PostgreSQL :-)

Code final

Le code est disponible ici : https://github.com/gdarquie/postgres-docker