A small PHP version switcher for Linux. TUI in the terminal, optional system tray app, and a cd-hook that picks the right PHP for the project you just stepped into.
If you've been juggling update-alternatives --set php by hand every time you switch between a Laravel 9 app on 8.1 and a fresh Symfony repo on 8.3, this is for you.
- Interactive TUI version picker, arrow keys, Enter, done.
- A tray icon (and a separate GTK window if you'd rather not live in the panel) with per-version badges: which SAPIs are available, whether xdebug is loaded, whether FPM is running, whether the version is EOL.
.php-version(orcomposer.json'srequire.php) drives a per-project version. Walks up the tree likenvmdoes.- A
cd-hook for bash / zsh / fish that runsphpvm --autoso the right PHP is loaded by the time the prompt comes back. - An installer that asks the obvious questions (CLI? GUI? wire up the shell hook? passwordless sudo?) and an uninstaller that backs up your shell rc before touching it.
Under the hood it's just update-alternatives --set php. Nothing exotic. The whole point is that you stop typing that command.
One-liner (no local clone needed — the installer bootstraps itself by fetching the repo into a temp dir, then cleans up):
curl -fsSL https://raw.githubusercontent.com/rijoanul-shanto/phpvm/main/install.sh | sudo bashOr clone and run it interactively:
git clone https://github.com/rijoanul-shanto/phpvm.git
cd phpvm && sudo bash install.shThe installer is interactive even under curl … | sudo bash — it reads prompts directly from /dev/tty so the pipe doesn't swallow them. Pick CLI, GUI, or both, then say yes/no to the shell hook and the sudoers rule. Falls back to non-interactive defaults only when there is genuinely no controlling terminal (headless CI, nohup, etc.).
Pin a specific tag or branch:
curl -fsSL https://raw.githubusercontent.com/rijoanul-shanto/phpvm/main/install.sh | sudo PHPVM_REF=v2.3.2 bashTo remove it — see Uninstalling below.
phpvm --self-updateThat pulls the latest from the repo URL captured at install time and re-runs the installer in --upgrade mode, same install paths, same CLI/GUI choice, doesn't re-prompt for sudoers or the shell hook.
If you installed from a tarball (no recorded URL), you can pass one explicitly, optionally with a tag or branch:
phpvm --self-update https://github.com/rijoanul-shanto/phpvm.git
phpvm --self-update https://github.com/rijoanul-shanto/phpvm.git v2.2.0- Linux with
update-alternatives. Tested on Ubuntu 20.04 / 22.04 / 24.04 in CI; Debian 11+ and Ubuntu derivatives (Mint, Pop!_OS, Zorin, elementary) on the equivalent releases should work too. - Bash 4.3+ (uses
local -n). - For the GUI:
python3-gi, GTK3, AppIndicator3. The install command is in the GUI section below.
Keyboard-driven picker right where you live. ↑/↓ to move, Enter to switch, p to pin as the project version, q to bail.
| Command | What it does |
|---|---|
phpvm |
Opens the TUI |
phpvm --list |
Lists installed PHP versions |
phpvm --current |
Prints whichever one is active |
phpvm --set 8.2 |
Switches globally to 8.2 |
phpvm --auto |
Reads .php-version / composer.json and switches |
phpvm --auto --print [dir] |
Prints the resolved project PHP version without switching |
phpvm --set-project 8.2 |
Writes .php-version here |
phpvm --enable-hook [shell] |
Adds the auto-switch hook to your rc |
phpvm --disable-hook [shell] |
Removes it (rc is backed up first) |
phpvm --window |
Launches the GTK picker window, then frees the terminal |
phpvm-gui |
Tray applet (see The GUI) |
phpvm-gui --window |
Standalone GTK picker window, no tray |
phpvm --self-update |
Re-runs the installer against the latest commit |
phpvm --doctor |
Full diagnostic — CLI install, PHP runtimes, FPM, sudoers, shell hook, GUI, project |
phpvm --help |
Everything else |
Vim users get k/j too.
Two shapes, same binary.
sudo apt install python3-gi gir1.2-gtk-3.0 gir1.2-ayatana-appindicator3-0.1
phpvm-gui # tray applet
phpvm-gui --window # detached GTK picker window, no tray
phpvm --window # same window, launched from the shell (terminal freed)The window view shows each version with:
- which SAPIs are available (
cli,fpm,apache2) - whether xdebug is enabled
- whether
php-fpmfor that version is running - a red marker if it's EOL
Each row gets buttons for Switch and Restart FPM. There's also a project auto-detect button and a folder picker for one-off switches. Hover a row and the tooltip tells you which php.ini it would load.
About FPM restart: it tries passwordless sudo first, and if that fails it pops the polkit auth dialog (pkexec). Either works; nothing else needed.
echo "8.1" > .php-version
# or
phpvm --set-project 8.1phpvm walks up the directory tree looking for .php-version. If there isn't one, it reads require.php from composer.json and picks the highest installed version that satisfies the constraint. Caret, tilde, ranges, | unions — all the constraint syntaxes Composer accepts.
The easy way:
phpvm --enable-hook # detects $SHELL
phpvm --enable-hook zsh # or name it
phpvm --disable-hook # undo, rc backed upIf you'd rather edit your rc yourself
System install lives under /etc/phpvm; user install lives under ~/.phpvm. Source whichever exists:
# bash
source /etc/phpvm/php-auto.bash # or ~/.phpvm/php-auto.bash
# zsh
source /etc/phpvm/php-auto.zsh # or ~/.phpvm/php-auto.zsh
# fish
source /etc/phpvm/php-auto.fish # or ~/.phpvm/php-auto.fishEvery switch ends up running sudo update-alternatives --set php …
By default that means a password prompt. The installer offers to drop a sudoers rule so you don't get one:
# /etc/sudoers.d/phpvm
username ALL=(ALL) NOPASSWD: /usr/bin/update-alternatives --set php /usr/bin/php[0-9].[0-9]
The glob is intentionally narrow, it matches php8.2 but not phpunit or php-config.
If you skip the sudoers rule, the CLI just asks for a password the normal way (and labels the prompt so you know who's asking). The GUI tries passwordless sudo first, then falls back to the polkit dialog.
If phpvm reports no versions installed
You probably haven't registered them with update-alternatives yet:
sudo update-alternatives --install /usr/bin/php php /usr/bin/php8.3 83
sudo update-alternatives --install /usr/bin/php php /usr/bin/php8.2 82
sudo update-alternatives --install /usr/bin/php php /usr/bin/php8.1 81The number at the end is the priority; higher wins when nothing is explicitly selected.
Project layout
phpvm/
├── phpvm.sh CLI + TUI
├── phpvm-gui.py tray + window GUI
├── shell/
│ ├── php-auto.bash
│ ├── php-auto.zsh
│ └── php-auto.fish
├── install.sh
└── uninstall.sh
One-liner (no local clone needed):
curl -fsSL https://raw.githubusercontent.com/rijoanul-shanto/phpvm/main/uninstall.sh | sudo bashOr from a local clone:
sudo bash uninstall.shWhat it removes:
phpvmandphpvm-guibinaries from both/usr/local/binand~/.local/bin- Hook directory (
/etc/phpvmor~/.phpvm) - Sudoers rule (
/etc/sudoers.d/phpvm) - Desktop entry and autostart file
- Icon from the hicolor theme (and refreshes the icon cache)
- The
source …/php-auto.*lines from~/.bashrc,~/.zshrc, and~/.config/fish/config.fish
Shell RCs are backed up as <file>.phpvm-backup before any edits. Running under sudo also cleans the invoking user's home, not just root's.
- Install PHP for you. You still need
apt install php8.2 php8.2-fpm …(or Ondřej Surý's PPA). phpvm only switches between what's already on disk. - Work on distros without
update-alternatives. Arch, Fedora, RHEL, openSUSE out of scope. Patches welcome if you want to add a backend. - Touch your web server config. Apache/Nginx still point at whatever socket or module you wired up. FPM restart is per-version and only knows
systemctl restart phpX.Y-fpmstyle unit names. - Pin a specific patch version. Everything is
X.Y. If you need8.2.13exactly, this is the wrong tool. - Cross-shell auto-switching inside a single session. Open a new shell after switching to pick up the change inside that shell's
$PATHresolved binary cache. - Pop the polkit dialog without a desktop session. Headless boxes get the regular
sudopassword prompt instead.
Patches welcome. See CONTRIBUTING.md. Two ground rules: no runtime dependencies beyond what's already there, and shellcheck clean.


