Makefile Magic: PHP Quality Tools auf Steroiden
Oder: Wie ich aufhörte, Composer Scripts zu lieben und die Macht von Make
entdeckte
- Die 15-Minuten-Test-Suite des Grauens
- Warum Make und nicht Composer Scripts?
- Die Docker-Make-PHP Trinity
- Modulare Makefile-Architektur (wie die Großen es machen)
- Quality Tools Integration – aber richtig!
- Der ultimative Quality-Check
- Performance ist kein Zufall
- Migration von Phing/Ant zu Make
- CI/CD Integration
- Pro-Tipps aus der Praxis
- Realitäts-Check
- Das komplette Setup
- Fazit
Die 15-Minuten-Test-Suite des Grauens
Kennste das? Du machst 'ne kleine Änderung, willst nur schnell die Tests laufen
lassen und… Zeit für 'nen Kaffee. Oder zwei. Deine Test-Suite läuft länger als
die letzte Sprint-Retro.
Bei meinem letzten Projekt hatte ich genau das Problem: PHPUnit, PHPStan Level
11, PHP-CS-Fixer, Psalm, und noch drei andere Tools. Sequenziell ausgeführt. 15
Minuten pro Run. Das sind 120 Kaffees pro Woche – selbst für mich zu viel.
Die Lösung? Makefiles. Ja, diese uralten Dinger aus den 70ern. Aber glaub mir,
die haben’s echt drauf.
Warum Make und nicht Composer Scripts?
Bevor du jetzt sagst “Aber wir haben doch composer test!” – lass mich dir
zeigen, was Composer Scripts NICHT können:
{
"scripts": {
"test": ["@phpunit", "@phpstan", "@phpcs", "@psalm"]
}
}
Das läuft schön der Reihe nach. Einer nach dem anderen. Während dein Rechner mit
16 Cores gelangweilt in der Ecke steht.
Mit Make? Parallel execution, Baby!
.PHONY: test
test: phpunit phpstan phpcs psalm
@echo "✅ All tests passed!"
# Diese laufen PARALLEL!
phpunit phpstan phpcs psalm:
@echo "🚀 Running $@..."
@vendor/bin/$@ $(ARGS)
Die Docker-Make-PHP Trinity
Aber wir gehen noch einen Schritt weiter. Docker + Make = Konsistente
Environments überall.
# 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
Dein 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:
Modulare Makefile-Architektur (wie die Großen es machen)
Ich hab mir das von Symfony abgeschaut – die wissen, was sie tun:
# 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}'
Dann strukturierst du deine Make-Files:
make/
├── docker.mk # Docker commands
├── quality.mk # Quality tools
├── test.mk # Testing
└── build.mk # Build processes
Quality Tools Integration – aber richtig!
PHPUnit mit Paratest (Parallel Testing auf Crack)
# 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 oder 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 mit 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
Der Pre-Commit Hook (scripts/pre-commit
):
#!/bin/bash
make cs-fix
git add -u
Psalm für 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
Der ultimative Quality-Check
Jetzt kommt’s: Alles 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 ist kein Zufall
Lass uns über Zahlen reden. Real-world Beispiel aus einem Symfony-Projekt mit
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% 🎉
Migration von Phing/Ant zu Make
Falls du noch mit Phing rumhängst (mein Beileid), hier die Migration:
<!-- build.xml (OLD) -->
<target name="test">
<phingcall target="phpunit"/>
<phingcall target="phpstan"/>
</target>
Wird zu:
# Makefile (NEW)
.PHONY: test
test: phpunit phpstan
# Bonus: Du kannst Phing temporary wrappen
.PHONY: phing-%
phing-%:
@phing $*
CI/CD Integration
Für 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-Tipps aus der Praxis
1. Nutze Baselines für progressive Verbesserung
.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
Realitäts-Check
Klar, Make ist nicht immer die Antwort. Wenn du nur PHPUnit hast und sonst nix –
bleib bei Composer Scripts. Aber sobald du mehr als drei Tools hast oder deine
Test-Suite länger als dein Daily läuft, wird’s Zeit für Make.
Die Lernkurve? Naja, Make-Syntax ist… speziell. Tabs statt Spaces, anyone?
Aber die Doku ist gut, und du musst ja nicht gleich ein Make-Ninja werden.
Das komplette Setup
Hier das finale Makefile für Copy-Paste-Helden:
# 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)
Fazit
Makefiles für PHP? Klingt erstmal weird, funktioniert aber mega gut. Besonders
in Kombination mit Docker kriegst du ein Setup, das überall gleich läuft und
deine Build-Zeiten dramatisch reduziert.
Mein Team hat anfangs auch komisch geguckt. “Make? Ist das nicht von 1976?” Ja,
und Vim auch. Trotzdem benutzen’s alle. Nach der ersten Demo, wo die Test-Suite
in 3 statt 15 Minuten durchlief, waren alle überzeugt.
Also, probier’s aus! Worst case hast du was gelernt. Best case sparst du dir
täglich eine Stunde Wartezeit. Und das sind 5 Stunden pro Woche für sinnvollere
Dinge. Wie zum Beispiel mehr Tests schreiben. Oder Kaffee trinken. Deine Wahl.
Du willst noch mehr Performance? Check meinen nächsten Post über Mutation
Testing mit Infection – wenn deine Tests die Tests testen. Mind = blown.
Weiterführende Links: