Objective: set up your projects more easily

For the last decade I worked on several more or less complex projects. I often had problems with their installation. Sometimes, the project was not documented or the existing documentation was not up to date. I had to run commands but I did not understand all of them. When I got errors it was hard to understand what happened. That was not always simple.

During my last projects, I used makefile to provide an abstraction to simplify their installation and hide complexity. It let me install projects with a single command and provided a minimal set of targets which made my daily work easier.

How does makefile work?

A makefile is a set of “rules” that looks like:

target: prerequisites prerequisites
        recipe
        recipe
        ...

A target is the name of a file (or a folder) that is generated by make. It could also be the name of an action to perform but you need to declare it as PHONY.

A prerequisite are the files (or folders) needed to create the target, you can see them as target dependencies.

A recipe are all actions executed when the target is run. Caution: you need to indent all recipes using a “real” tab character otherwise you will get errors.

.env:
     cp .env.dist .env

If the .env file does not exist, the recipe will be carried out, but if it does exist the recipe won’t be executed #magic.

.PHONY: cache
cache: .env
      bin/console c:c

The cache target does not target a file. Declaring this target as PHONY allows having a file named cache in the same directory as the Makefile.

Note: If the .env file does not exist the .env target will be performed before the cache target.

Let’s take an example

For instance, a project orchestrated by docker-compose having:

  • a front application written in JS using React framework,
  • a back application written in PHP using Symfony framework,
  • a PostgreSQL database managed by Doctrine DBAL and Doctrine migration

Caution: I will only speak about projects setup for dev purposes. I won’t talk about making docker images ready for production.

Do you speak French ? Tired of the same old CRUD applications, struggling with your framework, or feeling the pressure of production releases? It's time to take your career to the next level.

Discover the power of Hexagonal Architecture and DDD to build robust and sustainable Symfony applications. Join me and kickstart your journey toward mastering advanced development techniques.

Let’s start installing the project dependencies. The following targets install project dependencies if they have not been downloaded yet. They can guess if they are outdated to upgrade them thanks to target prerequisites. Another interesting thing is that nothing will be done if dependencies are already installed and up to date.

# Front
web/yarn.lock: web/package.json
  docker-compose run --rm node yarn install

web/node_modules: web/yarn.lock
  docker-compose run --rm node yarn install --frozen-lockfile
  docker-compose run --rm node yarn check --integrity

# back
api/composer.lock: api/composer.json
  docker-compose run --rm fpm composer update

api/vendor: api/composer.lock
  docker-compose run --rm fpm composer install

Then, we need to “up” all docker-compose services: the web server, the PHP process manager, the datatable, and the node to run the front application in development mode.

.PHONY: up
up:
  docker-compose up -d
docker-compose ps                                              
         Name                        Command               State                                     Ports                                   
---------------------------------------------------------------------------------------------------------------------------------------------                                                     
my-maps-node              docker-entrypoint.sh yarn  ...   Up                                                                                
my-maps-web               nginx -g daemon off;             Up      80/tcp                                                                    
my-maps_database_1        docker-entrypoint.sh postgres    Up      5432/tcp                                                                  
my-maps_fpm_1             php-fpm -F                       Up                                                                           

The last thing to do is to create the database schema using Doctrine migration.

.PHONY: db-migration
db-migration: api/vendor
  docker-compose run --rm fpm bin/console doctrine:migrations:migrate --no-interaction

Tip: To ease my daily work, I like introducing other targets like db target that resets database quickly or fixture to load some data fixtures.

.PHONY: db
db: api/vendor
  docker-compose run --rm fpm bin/console doctrine:database:drop --force --no-interaction
  docker-compose run --rm fpm bin/console doctrine:database:create --no-interaction

.PHONY: fixtures
fixtures: api/vendor
  docker-compose run --rm fpm bin/console project:fixtures:load

Now, we have all atomic targets to set up all parts of the application. Another interesting thing with make, is that it can run several targets within a single command make up api/vendor web/node_modules.This is pretty useful to create scenarios. For instance, to set up and run the project I only need to run the following command:

make up api/vendor web/node_modules db db-migration fixtures

But it works with everything:

make db db-migration fixtures
make api/vendor web/node_modules

To make your day-to-day basis, you can introduce targets that run those scenarios.

# It builds and runs the application
.PHONY: app-dev
app-dev: up api/vendor web/node_modules db db-migration fixtures

# It resets the database
.PHONY: db-reset
db-reset: db db-migration fixtures

# It resets the database
.PHONY: dependencies
dependencies: api/vendor web/node_modules

Tip: I advise you to introduce the minimum set of targets into the makefile. Keep it simple! Don’t forget that everyone uses it! To let developers have their custom targets you may want to include custom makefiles using include. Don’t forget to add an entry into .ignore to avoid committing those files. Now, developers can create their own makefile with their personal targets.

One last word

Makefile is only a solution, this is not THE solution. The important thing to keep in mind is that you should be able to easily run your project to reduce the developer’s mental load and let them focus on the right thing. It will improve the day-to-day basis and make newcomers’ onboarding easier.

Thanks to my proofreader @LaureBrosseau.