During development, you probably take advantage of some extra command-line tools. In PHP world it could be a mess detector or program to check the code style. The framework you use also exposes some functionalities to clear cache, migrate database or generate documentation. All these commands are helpful but you need to look for them until you memorize the most useful ones.

Sometimes you need to perform a task, like project initialization or restoring a stable snapshot of the database. It’s rarely an atomic operation, so you need to execute a few commands in a specific order.

This is a place where a task runner comes to play. Take a look at how a program called make can help you organize common tasks in your project.

Why make?

Make is a tool to generate files from other files, e.g. executables from the source code. But it can also perform other operations like running other tools and commands.

Unlike gulp or grunt, make doesn’t need any special JavaScript’s runtime or plugins. If you can describe your task using a set of shell commands, you may use them as your task in make as well.

Install make

macOS

If you use a macOS, you may install make using homebrew:

$ brew install make

Debian, Ubuntu

If you use a Debian-like system, it’s likely that you already have make. If not, you can install it from the repository:

$ sudo apt install build-essential

Windows

I have no experience with make on a Windows machine, but according to this answer from StackOverflow, you can install make using chocolatey package manager.

> choco install make

Define your first task

To define your first task, create a file called Makefile with the following content. Please be aware, Makefile uses tabs as indentation.

init:
    @echo "Initialize project"
    @echo "End of initialization"
.PHONY: init

Then, run the task by executing make init. The output should be like this:

$ make init
Initialize project
End of initialization

The Makefile is a bag for your tasks. Each task (called target) consists of commands that can be executed through the shell.

Because it’s a file, you can store and version it together with your project files in the VCS. Any team member may define other tasks to build up a set of tools and procedures for the whole team.

Thanks to the @ symbol at the beginning command, make displays output without printing the executed command. Whether it is useful or not depends on your use-case.

The extra line containing .PHONY: init means, that task doesn’t produce any file. It would be problematic if you have a file with the same name as the make target.

Make Make more useful

What is in your Makefile depends on your needs. Think about the goal of each task. Don’t put everything you suppose you will need. You may consider adding a new task if it:

  • adds any value for you and your team
  • speeds up your work
  • simplifies any complex operation

Here are a few examples of tasks from my Makefile.

## Initialize the application
init:
    docker-compose exec app bash -c "composer install -n -o --dev"
    make init-db
.PHONY: init
    
## Go to the shell inside app container
jump:
    @docker-compose exec app bash
.PHONY: jump
    
## Fix code style issues using phpcbf
cbf:
    ./vendor/bin/phpcbf --extensions=php --standard=phpcs.xml -p src/ --warning-severity=9 --cache
.PHONY: cbf

## Show code style issues using phpcs
cs:
    ./vendor/bin/phpcs --extensions=php --standard=phpcs.xml -p src/ --warning-severity=9 --cache
.PHONY: cs

## Migrate database
migrate:
    @docker-compose exec app bash -c "php ./bin/console doctrine:migrations:migrate"
.PHONY: migrate
    
## Initialize database    
init-db:
    @docker-compose exec db mysql -u root -proot -e "DROP DATABASE IF EXISTS app_db" > /dev/null
    @docker-compose exec db mysql -u root -proot -e "CREATE DATABASE app_db CHARACTER SET utf8 COLLATE utf8_unicode_ci" > /dev/null
    @make migrate
.PHONY: init-db

Creating a good Makefile is more like a process rather than a single action. Trying to define everything in advance is unproductive and may lead to creating many tasks you will rarely use. Start with something small and improve in time.

Let Make document itself

Since it’s worth to have a distinct name of the task, the goal of task runner is to save your time and memory. Instead of creating tasks with long names, you can define another task that displays the description of the task alongside its name.

GREEN  := $(shell tput -Txterm setaf 2)
YELLOW := $(shell tput -Txterm setaf 3)
WHITE  := $(shell tput -Txterm setaf 7)
RESET  := $(shell tput -Txterm sgr0)

TARGET_MAX_CHAR_NUM=15

.DEFAULT_GOAL := help

## Show this help message
help:
    @echo ''
    @echo 'Usage:'
    @echo '  ${YELLOW}make${RESET} ${GREEN}<target>${RESET}'
    @echo ''
    @echo 'Targets:'
    @awk '/^[a-zA-Z\-\_0-9]+:/ { \
        helpMessage = match(lastLine, /^## (.*)/); \
        if (helpMessage) { \
            helpCommand = substr($$1, 0, index($$1, ":")); \
            sub(/:/, "", helpCommand); \
            helpMessage = substr(lastLine, RSTART + 3, RLENGTH); \
            printf "  ${YELLOW}%-$(TARGET_MAX_CHAR_NUM)s${RESET} ${GREEN}%s${RESET}\n", helpCommand, helpMessage; \
        } \
    } \
    { lastLine = $$0 }' $(MAKEFILE_LIST)

Here’s a link to the source code. You may find other implementations on GitHub or StackOverflow.

Using my Makefile, it produces output like this:

$ make

Usage:
  make <target>

Targets:
  help            Show this help message
  init            Initialize the application
  jump            Go to the shell inside app container
  cbf             Fix code style issues using phpcbf
  cs              Show code style issues using phpcs
  migrate         Migrate database
  init-db         Initialize database

Which looks like

iterm2 window that displays the help target's output
Self-documented Makefile – each task has the description

As you can see, the help message is displaying by default if you provide no arguments. It’s configured by setting .DEFAULT_GOAL := help.

If tasks' names are longer than 15 characters, you can adjust the TARGET_MAX_CHAR_NUM variable.

Summary

Make is not the only tool in this area. Aforementioned gulp and grunt are also good but they need some extra resources and plugins. If you already work on JavaScript project, you can consider using either. Even so, it’s more likely that you already use a tool like webpack to bundle your application.

Using a task runner is great to hide complex operations behind simple commands. However, it’s also worth to know, what specific task does under the hood. The goal is to save time, not to reduce the necessary knowledge. In case of any error, who will be able to fix a bug, if the last modification to Makefile has done one year ago by a person who isn’t in the project anymore?

Featured photo by sergey Svechnikov on Unsplash.