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