C'est l'une de mes découvertes préférées de ma nouvelle carrière de développeur web, l'utilisation du fichier Makefile pour effectuer rapidement des commandes Docker ou pour la console Symfony (ou autre).

Si vous avez touché un peu à Docker ou à la console Symfony, vous savez qu'il existe pas mal de commande que vous pouvez utiliser pour faire différentes actions. Cela peut aller à lancer votre environnement de développement Docker, effacer le cache de Symfony, effectuer vos migrations, lancer vos tests, etc...

La première solution quand on commencer à avoir un peu l'habitude avec les commandes sous Linux, c'est de créer des alias de commandes (alias cc="php bin/console c:c") pour les effectuer.

L'avantage c'est que c'est rapide à mettre en place, le gros inconvénient c'est que cela n'est valable que sur la machine où vous avez créé l'alias et donc que vous devez dupliquer à chaque fois les changements entre les différentes machines quand vous souhaitez faire des modifications dans vos alias. De même, ces alias ne seront pas disponibles dans vos conteneurs Docker (sauf si vous les ajouter dans le Dockerfile de votre projet, mais ça peut demander de build un conteneur juste pour ajouter ces alias).

Et c'est là qu'il existe une autre solution pour palier à cela, qui pourra fonctionner sur quasiment toutes les machines (modulo une chose) ainsi que dans les conteneurs Docker. Bien sûr comme le titre l'indique, il s'agit de créer un fichier Makefile (sans extension) dans votre projet.

Ce fichier est à lancer avec le programme make sous Linux (donc il faut l'installer auparavant (sudo apt install make par exemple) mais c'est un package qui existe dans les repos de base de toutes les distributions). Par contre pour tout dire, je ne sais pas si c'est utilisable sur Windows hors WSL.

Dans le contexte de votre projet, le fichier Makefile servira de référence pour tous les alias que vous souhaitez utiliser sur votre projet. Sachez également que généralement vous pouvez également utiliser ces commandes make dans vos CI (Continuous Integration, soit GitHub Actions ou GitLab CI par exemple)

Agencer le Makefile

Voici la partie qui vous intéresse le plus, faire un Makefile est très simple. Donc comme indiquer créer un fichier Makefile à la racine de votre projet, être sûr d'avoir la commande make installé sur votre distribution ou dans votre conteneur Docker. Si ce n'est pas le cas dans ce dernier cas, il suffit d'ajouter cela à votre Dockerfile :

RUN set -xe \
    && apk add --no-cache --virtual \
    make

Maintenant commençons à rédiger le Makefile. La première étape est de savoir si le conteneur Docker de notre projet est en train de tourner pour savoir si on doit exécuter les commandes dans le conteneur ou non.

containerName = "symfony-php"
isContainerRunning := $(shell docker ps | grep $(containerName) > /dev/null 2>&1 && echo 1)

Cela va vérifier si le conteneur nommé symfony-php est en cours d'exécution ou non, isContainerRunning renverra 1 ou 0 quand on l'appelera ensuite.

user := $(shell id -u)
group := $(shell id -g)
Pour stocker l'UID et le GID de l'utilisateur qui va exécuter une commande

Ensuite on stock l'UID et le GID de l'utilisateur qui exécutera les commandes (pour éviter au maximum les problèmes de permissions que peuvent engendrer certaines commandes sur la création des fichiers).

Après je définis tous les mots clés que je vais utiliser pour les différentes commandes :

DOCKER :=
DOCKER_COMPOSE := USER_ID=$(user) GROUP_ID=$(group) docker-compose
DOCKER_TEST := APP_ENV=test

CONSOLE := $(DOCKER) php bin/console
CONSOLE_MEMORY := $(DOCKER) php -d memory_limit=256M
CONSOLE_TEST := $(DOCKER_TEST) php bin/console
COMPOSER = $(DOCKER) composer

Si vous comprenez, les mot-clé DOCKER permettra de lancer la commande direct sous docker (ou sur votre host si vous avez ce qu'il faut). D'ailleurs vous pouvez voir que les variables qu'on vient de créer peuvent être appelé via les caractères $().

DOCKER_COMPOSE sera utilisé pour les commandes docker-compose tout en ajoutant l'UID et le GID pour porté les permissions de l'utilisateur faisant la commande. Et ainsi de suite pour les autres mots-clé.

Ensuite il faut vérifier si le conteneur est en train de fonctionner pour savoir s'il faut exécuter les commandes dans celui-ci ou directement sur la machine host :

ifeq ($(isContainerRunning), 1)
	DOCKER := @docker exec -t -u $(user):$(group) $(containerName) php
	DOCKER_COMPOSE := USER_ID=$(user) GROUP_ID=$(group) docker-compose
	DOCKER_TEST := @docker exec -t -u $(user):$(group) $(containerName) APP_ENV=test php
endif

Maintenant il suffit de définir les commandes qu'on souhaite faire via make. Voici un premier exemple :

build-docker:
	$(DOCKER_COMPOSE) pull --ignore-pull-failures
	$(DOCKER_COMPOSE) build --no-cache

En faisant make build-docker, ça exécutera les deux commandes docker-compose pull --ignore-pull-failures et docker-compose build --no-cache consécutivement. Ce qui est plus facile à se rappeler et plus rapide à taper.

Même pour des commandes uniques ça peut être intéressant de faire une commande make comme par exemple :

database: ## Create database if no exists
	$(CONSOLE) doctrine:database:create --if-not-exists

Dans ce cas là make database va juste permettre de créer la base de données, ce qui peut être plus rapide que de faire un php bin/console doc:data:create --if-not-exists

Une autre chose possible est de chainer plusieurs commandes make existant dans le fichier, par exemple :

reset-database: drop-database database migrate load-fixtures ## Reset database with migration

Avec cet exemple, pour la commande make reset-database, cela va lancer consécutivement les commandes :

  • make drop-database
  • make database
  • make migrate
  • make load-fixtures

Ce qui va donc détruire la base de données, la recréer, faire les migrations puis charger les fixtures.

C'était l'essentiel concernant le Makefile, un outil qui a transformé ma manière de gérer mes stacks (parce que bien sûr vous pouvez l'adapter pour d'autres stacks comme Go par exemple, puisque ce n'est pas lié à un langage ou un framework).

Et pour fermer ce billet, je vous laisse le Makefile que j'utilise le plus régulièrement sur mes projets Symfony :

containerName = "symfony-php"
isContainerRunning := $(shell docker info > /dev/null 2>&1 && docker ps | grep $(containerName) > /dev/null 2>&1 && echo 1)
user := $(shell id -u)
group := $(shell id -g)

DOCKER := 
DOCKER_COMPOSE := USER_ID=$(user) GROUP_ID=$(group) docker-compose
DOCKER_TEST := APP_ENV=test 

CONSOLE := $(DOCKER) php
CONSOLE_MEMORY := $(DOCKER) php -d memory_limit=256M
CONSOLE_TEST := $(DOCKER_TEST) php
COMPOSER = $(DOCKER) composer

ifeq ($(isContainerRunning), 1)
	DOCKER := @docker exec -t -u $(user):$(group) $(containerName)
	DOCKER_COMPOSE := USER_ID=$(user) GROUP_ID=$(group) docker-compose
	DOCKER_TEST := @docker exec -t -u $(user):$(group) $(containerName) APP_ENV=test
endif

## —— App ————————————————————————————————————————————————————————————————
build-docker:
	$(DOCKER_COMPOSE) pull --ignore-pull-failures
	$(DOCKER_COMPOSE) build --no-cache

up:
	@echo "Launching containers from project $(COMPOSE_PROJECT_NAME)..."
	$(DOCKER_COMPOSE) up -d
	$(DOCKER_COMPOSE) ps

stop:
	@echo "Stopping containers from project $(COMPOSE_PROJECT_NAME)..."
	$(DOCKER_COMPOSE) stop
	$(DOCKER_COMPOSE) ps

prune:
	@docker-compose down --remove-orphans
	@docker-compose down --volumes
	@docker-compose rm -f

serve:
	$(CONSOLE) serve

install-project: install reset-database generate-jwt ## First installation for setup the project

update-project: install reset-database ## update the project after a checkout on another branch or to reset the state of the project

sync: update-project test-all ## Synchronize the project with the current branch, install composer dependencies, drop DB and run all migrations, fixtures and all test

## —— 🐝 The Symfony Makefile 🐝 ———————————————————————————————————
help: ## Outputs this help screen
	@grep -E '(^[a-zA-Z0-9_-]+:.*?## .*$$)|(^## )' Makefile | awk 'BEGIN {FS = ":.*?## "}{printf "\033[32m%-30s\033[0m %s\n", $$1, $$2}' | sed -e 's/\[32m##/[33m/'

## —— Composer 🧙‍♂️ ————————————————————————————————————————————————————————————
install: composer.lock ## Install vendors according to the current composer.lock file
	$(COMPOSER) install -n

update: composer.json ## Update vendors according to the composer.json file
	$(COMPOSER) update -w

## —— Symfony ————————————————————————————————————————————————————————————————
cc: ## Apply cache clear
	$(DOCKER) sh -c "rm -rf var/cache"
	$(CONSOLE) cache:clear
	$(DOCKER) sh -c "chmod -R 777 var/cache"

cc-test: ## Apply cache clear
	$(DOCKER) sh -c "rm -rf var/cache"
	$(CONSOLE_TEST) cache:clear
	$(DOCKER) sh -c "chmod -R 777 var/cache"

doctrine-validate:
	$(CONSOLE) doctrine:schema:validate --skip-sync $c

reset-database: drop-database database migrate load-fixtures ## Reset database with migration

database: ## Create database if no exists
	$(CONSOLE) migrate:status

drop-database: ## Drop the database
	$(CONSOLE) doctrine:database:drop --force --if-exists

migration: ## Apply doctrine migration
	$(CONSOLE) make:migration

migrate: ## Apply doctrine migrate
	$(CONSOLE) doctrine:migration:migrate -n --all-or-nothing

generate-jwt: ## Generate private and public keys
	$(CONSOLE) lexik:jwt:generate-keypair --overwrite -q $c

## —— Tests ✅ ————————————————————————————————————————————————————————————
test-database: ### load database schema
	$(CONSOLE_TEST) doctrine:database:drop --if-exists --force
	$(CONSOLE_TEST) doctrine:database:create --if-not-exists
	$(CONSOLE_TEST) doctrine:migration:migrate -n --all-or-nothing
        $(CONSOLE_TEST) doctrine:fixtures:load -n

pest:
	$(CONSOLE) ./vendor/bin/pest

test: phpunit.xml* ## Launch main functional and unit tests, stopped on failure
	$(CONSOLE) ./vendor/bin/pest --stop-on-failure $c

test-all: phpunit.xml* test-load-fixtures ## Launch main functional and unit tests
	$(DOCKER_TEST) ./vendor/bin/pest

test-report: phpunit.xml* test-load-fixtures ## Launch main functional and unit tests with report
	$(DOCKER_TEST) ./vendor/bin/pest --coverage-text --colors=never --log-junit report.xml $c

## —— Coding standards ✨ ——————————————————————————————————————————————————————
stan: ## Run PHPStan only
	$(CONSOLE) ./vendor/bin/phpstan analyse -l 9 src --no-progress -c phpstan.neon --memory-limit 256M

ecs: ## Run ECS only
	$(CONSOLE) ./vendor/bin/ecs check --memory-limit 256M

ecs-fix: ## Run php-cs-fixer and fix the code.
	$(CONSOLE) ./vendor/bin/ecs check --fix --memory-limit 256M

cs-fix: ## Run php-cs-fixer and fix the code.
	$(CONSOLE) ./vendor/bin/php-cs-fixer fix --allow-risky=yes

cs-dry: ## Dry php-cs-fixer and display code may to be change
	$(CONSOLE) ./vendor/bin/php-cs-fixer fix --dry-run --allow-risky=yes