How to set up PHP autoformatting in Zed using Pint and PHP CS Fixer original
I only switched to Zed last week (you can see my full setup on my uses page), so I'm still learning the ropes. One thing I ran into is that its external formatter configuration is global. You configure one formatter command for PHP, and that's what gets used in every project you open.
The problem is that not all of my projects use the same formatter. Some use Pint, some use PHP-CS-Fixer directly. My Zed config originally pointed to ./vendor/bin/pint, which meant it silently did nothing in projects that don't have Pint installed.
Let me walk you through how I solved this.
The wrapper script
Zed's external formatter pipes your buffer content to stdin and expects formatted output on stdout. Tools like pint and php-cs-fixer don't work that way, they modify files in place. So you need a wrapper script to bridge the two.
The solution is a small bash script that bridges the gap. I used AI to help me build it. I put it at ~/bin/php-format.
#!/bin/bash
FILE="$1"
GLOBAL_PINT="$HOME/.composer/vendor/bin/pint"
find_project_root() {
local dir="$1"
while [ "$dir" != "/" ]; do
if [ -f "$dir/composer.json" ]; then
echo "$dir"
return
fi
dir="$(dirname "$dir")"
done
}
PROJECT_ROOT=$(find_project_root "$(dirname "$FILE")")
TEMP=$(mktemp /tmp/php-format.XXXXXX.php)
cat > "$TEMP"
if [ -n "$PROJECT_ROOT" ] && [ -f "$PROJECT_ROOT/vendor/bin/pint" ]; then
"$PROJECT_ROOT/vendor/bin/pint" "$TEMP" > /dev/null 2>&1
elif [ -n "$PROJECT_ROOT" ] && [ -f "$PROJECT_ROOT/vendor/bin/php-cs-fixer" ]; then
cd "$PROJECT_ROOT"
./vendor/bin/php-cs-fixer fix --allow-risky=yes "$TEMP" > /dev/null 2>&1
else
"$GLOBAL_PINT" "$TEMP" > /dev/null 2>&1
fi
cat "$TEMP"
rm -f "$TEMP"
The script walks up from the buffer path to find the project root (by looking for composer.json), writes stdin to a temp file, runs the right formatter, and outputs the result. It tries project-local Pint first, then PHP-CS-Fixer, and falls back to a globally installed Pint for projects without a formatter.
Don't forget to make it executable:
chmod +x ~/bin/php-format
You'll also need Pint installed globally for the fallback to work:
composer global require laravel/pint
The Zed configuration
In your Zed settings (~/.config/zed/settings.json), configure PHP to use the wrapper script as its formatter:
{ "languages": { "PHP": { "formatter": { "external": { "command": "/path/to/your/home/bin/php-format", "arguments": ["{buffer_path}"] } } } } }
Replace /path/to/your/home with your actual home directory. Unfortunately, Zed doesn't expand ~ in the command path.
With format_on_save set to "on" in your Zed settings, this runs automatically every time you save a PHP file. You can also trigger it manually with whatever keybinding you've set for editor::Format (I use Cmd+Alt+L).
Getting the files from my dotfiles
I keep both the wrapper script and my Zed configuration in my dotfiles repo on GitHub. You can find the php-format script in the bin directory and my full Zed settings under config/zed. Feel free to grab them and adjust to your own setup.
In closing
It's a simple script, but it solves an annoying problem. Now I can open any PHP project in Zed and formatting just works, regardless of whether the project uses Pint or PHP-CS-Fixer.