A full analysis on a large codebase takes long enough that nobody runs it before committing, so every finding arrives from CI after the context has been lost.
#!/usr/bin/env bash
# .git/hooks/pre-commit
set -euo pipefail
files=$(git diff --cached --name-only --diff-filter=ACM | grep '.php$' || true)
[ -z "$files" ] && exit 0
echo "$files" | xargs -r vendor/bin/phpstan analyse --no-progress --level=5
The || true after grep matters because grep exits non-zero when nothing matches, and under set -e that aborts a commit touching no PHP. Analysing files in isolation loses some cross-file inference, so the hook is a fast approximation and CI remains the authority — which is the right division. Keeping it under two seconds is what stops people reaching for --no-verify, and that threshold is real.