Makefile Magic: Supercharging PHP Quality Tools
Or: How I Learned to Stop Worrying and Love Make
- The 15-Minute Test Suite of Doom
- Why Make Instead of Composer Scripts?
- The Docker-Make-PHP Trinity
- Modular Makefile Architecture (Like the Pros Do It)
- Quality Tools Integration – Done Right!
- The Ultimate Quality Check
- Performance Isn’t Accidental
- Migrating from Phing/Ant to Make
- CI/CD Integration
- Pro Tips from the Trenches
- Reality Check
- The Complete Setup
- Conclusion
The 15-Minute Test Suite of Doom
Picture this: You make a tiny change, want to run your tests quickly, and…
time for coffee. Or two. Your test suite runs longer than your last sprint
retrospective.
I had exactly this problem on my last project: PHPUnit, PHPStan Level 11,
PHP-CS-Fixer, Psalm, and three other tools. Running sequentially. 15 minutes per
run. That’s 120 coffees per week – even too much for me.
The solution? Makefiles. Yes, those ancient things from the 70s. But trust me,
they’ve still got it.
Why Make Instead of Composer Scripts?
Before you say “But we have composer test!” – let me show you what Composer
scripts CAN’T do:
{
"scripts": {
"test": ["@phpunit", "@phpstan", "@phpcs", "@psalm"]
}
}
This runs nicely one after another. Sequential. While your machine with 16 cores
sits bored in the corner.
With Make? Parallel execution, baby!
.PHONY: test
test: phpunit phpstan phpcs psalm
@echo "✅ All tests passed!"
# These run in PARALLEL!
phpunit phpstan phpcs psalm:
@echo "🚀 Running $@..."
@vendor/bin/$@ $(ARGS)
The Docker-Make-PHP Trinity
But we’re going a step further. Docker + Make = Consistent environments
everywhere.
# docker.mk - Modular FTW!
DOCKER_PHP = docker run --rm -v $(PWD):/app -w /app php:8.3-cli
DOCKER_COMPOSE = docker compose
.PHONY: docker-test
docker-test:
$(DOCKER_COMPOSE) run --rm php make test
.PHONY: docker-shell
docker-shell:
$(DOCKER_COMPOSE) run --rm php bash
Your docker-compose.yml:
version: "3.8"
services:
php:
build:
context: .
dockerfile: docker/php/Dockerfile
volumes:
- .:/app
- composer-cache:/root/.composer
environment:
- XDEBUG_MODE=coverage
volumes:
composer-cache:
Modular Makefile Architecture (Like the Pros Do It)
I learned this from Symfony – they know what they’re doing:
# Makefile
include make/*.mk
.DEFAULT_GOAL := help
.PHONY: help
help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
Then structure your Make files:
make/
├── docker.mk # Docker commands
├── quality.mk # Quality tools
├── test.mk # Testing
└── build.mk # Build processes
Quality Tools Integration – Done Right!
PHPUnit with Paratest (Parallel Testing on Steroids)
# make/test.mk
PHPUNIT = vendor/bin/phpunit
PARATEST = vendor/bin/paratest
.PHONY: test-unit
test-unit: ## Run unit tests in parallel
@echo "🧪 Running unit tests..."
@$(PARATEST) -p$(shell nproc) --colors
.PHONY: test-coverage
test-coverage: ## Generate coverage report
@XDEBUG_MODE=coverage $(PHPUNIT) --coverage-html=coverage
PHPStan – Level 11 or Go Home!
# make/quality.mk
PHPSTAN = vendor/bin/phpstan
PHPSTAN_LEVEL ?= max
.PHONY: phpstan
phpstan: ## Run PHPStan analysis
@echo "🔍 Analyzing code with PHPStan level $(PHPSTAN_LEVEL)..."
@$(PHPSTAN) analyse -l $(PHPSTAN_LEVEL) --memory-limit=2G
.PHONY: phpstan-baseline
phpstan-baseline: ## Generate PHPStan baseline
@$(PHPSTAN) analyse -l $(PHPSTAN_LEVEL) --generate-baseline
PHP-CS-Fixer with Pre-Commit Hook
CS_FIXER = vendor/bin/php-cs-fixer
.PHONY: cs-fix
cs-fix: ## Fix coding standards
@echo "🎨 Fixing code style..."
@$(CS_FIXER) fix --diff --verbose
.PHONY: cs-check
cs-check: ## Check coding standards
@$(CS_FIXER) fix --dry-run --diff
.PHONY: install-hooks
install-hooks: ## Install git hooks
@echo "🪝 Installing pre-commit hooks..."
@cp scripts/pre-commit .git/hooks/
@chmod +x .git/hooks/pre-commit
The pre-commit hook (scripts/pre-commit
):
#!/bin/bash
make cs-fix
git add -u
Psalm for Type Nerds
PSALM = vendor/bin/psalm
.PHONY: psalm
psalm: ## Run Psalm type analysis
@echo "⛪ Running Psalm..."
@$(PSALM) --show-info=false
.PHONY: psalm-baseline
psalm-baseline: ## Update Psalm baseline
@$(PSALM) --set-baseline=psalm-baseline.xml
The Ultimate Quality Check
Now here’s the kicker: Everything in parallel!
# make/quality.mk
.PHONY: quality
quality: ## Run all quality checks in parallel
@echo "🚀 Running all quality checks..."
@$(MAKE) -j$(shell nproc) \
phpstan \
psalm \
cs-check \
phpcpd \
phpmd
@echo "✅ Quality checks passed!"
.PHONY: quality-fix
quality-fix: ## Fix what can be fixed
@$(MAKE) cs-fix
@$(MAKE) phpstan-baseline
@$(MAKE) psalm-baseline
Performance Isn’t Accidental
Let’s talk numbers. Real-world example from a Symfony project with 100k LOC:
.PHONY: benchmark
benchmark: ## Compare sequential vs parallel execution
@echo "📊 Sequential execution:"
@time make quality-sequential
@echo "\n📊 Parallel execution:"
@time make quality
# Results:
# Sequential: 14m 32s
# Parallel: 3m 48s
# Improvement: 73.8% 🎉
Migrating from Phing/Ant to Make
If you’re still using Phing (my condolences), here’s the migration path:
<!-- build.xml (OLD) -->
<target name="test">
<phingcall target="phpunit"/>
<phingcall target="phpstan"/>
</target>
Becomes:
# Makefile (NEW)
.PHONY: test
test: phpunit phpstan
# Bonus: You can temporarily wrap Phing
.PHONY: phing-%
phing-%:
@phing $*
CI/CD Integration
For GitLab CI:
# .gitlab-ci.yml
quality:
stage: test
script:
- make quality
parallel:
matrix:
- TOOL: [phpstan, psalm, phpcs]
script:
- make $TOOL
GitHub Actions:
# .github/workflows/quality.yml
jobs:
quality:
runs-on: ubuntu-latest
strategy:
matrix:
tool: [phpstan, psalm, phpcs]
steps:
- uses: actions/checkout@v3
- run: make $
Pro Tips from the Trenches
1. Use Baselines for Progressive Improvement
.PHONY: baseline-all
baseline-all: ## Create all baselines
@$(MAKE) phpstan-baseline
@$(MAKE) psalm-baseline
@echo "📈 Baselines created. Now improve incrementally!"
2. Smart Caching
# Cache Composer dependencies
vendor: composer.json composer.lock
@composer install --no-interaction
@touch vendor
# Targets depending on vendor
phpstan psalm phpcs: vendor
3. Docker Layer Caching
# docker/php/Dockerfile
FROM php:8.3-cli AS base
RUN apt-get update && apt-get install -y git unzip
FROM base AS composer
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
FROM composer AS deps
WORKDIR /app
COPY composer.* ./
RUN composer install --no-scripts --no-autoloader
FROM deps AS app
COPY . .
RUN composer dump-autoload --optimize
Reality Check
Sure, Make isn’t always the answer. If you only have PHPUnit and nothing else –
stick with Composer scripts. But once you have more than three tools or your
test suite runs longer than your daily standup, it’s time for Make.
The learning curve? Well, Make syntax is… special. Tabs instead of spaces,
anyone? But the documentation is good, and you don’t need to become a Make ninja
right away.
The Complete Setup
Here’s the final Makefile for copy-paste heroes:
# Makefile
-include .env
export
# Tools
COMPOSER = composer
PHP = php
DOCKER = docker
DOCKER_COMPOSE = docker compose
# Directories
VENDOR_DIR = vendor
COVERAGE_DIR = coverage
# Include modular makefiles
-include make/*.mk
.DEFAULT_GOAL := help
# Auto-help from comments
.PHONY: help
help: ## Show this help
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
##@ Testing
.PHONY: test
test: test-unit test-integration ## Run all tests
.PHONY: test-unit
test-unit: $(VENDOR_DIR) ## Run unit tests
@$(PHP) $(VENDOR_DIR)/bin/paratest -p$$(nproc)
##@ Quality
.PHONY: quality
quality: ## Run all quality checks in parallel
@$(MAKE) -j$$(nproc) phpstan psalm cs-check
.PHONY: quality-fix
quality-fix: cs-fix ## Fix code style issues
##@ Docker
.PHONY: up
up: ## Start containers
@$(DOCKER_COMPOSE) up -d
.PHONY: down
down: ## Stop containers
@$(DOCKER_COMPOSE) down
##@ Utilities
.PHONY: clean
clean: ## Clean build artifacts
@rm -rf $(COVERAGE_DIR) var/cache var/log
# Dependencies
$(VENDOR_DIR): composer.json composer.lock
@$(COMPOSER) install
@touch $(VENDOR_DIR)
Conclusion
Makefiles for PHP? Sounds weird at first, but works incredibly well. Especially
combined with Docker, you get a setup that runs the same everywhere and
dramatically reduces your build times.
My team looked at me funny at first too. “Make? Isn’t that from 1976?” Yes, and
so is Vim. Yet everyone uses it. After the first demo where the test suite ran
in 3 instead of 15 minutes, everyone was convinced.
So give it a try! Worst case, you learned something. Best case, you save an hour
of waiting time daily. That’s 5 hours per week for more meaningful things. Like
writing more tests. Or drinking coffee. Your choice.
Want even more performance? Check out my next post about Mutation Testing with
Infection – when your tests test the tests. Mind = blown.
Further Reading: