Skip to content

rijverse/phpvm

Repository files navigation

phpvm

phpvm

A small PHP version manager for Linux. TUI in the terminal, optional system tray app, per-shell switching, 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.

Bash Python Linux License

phpvm GUI window

One-click switching with SAPI, xdebug, FPM, and EOL badges.

What it does

  • 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 (or composer.json's require.php) drives a per-project version. Walks up the tree like nvm does.
  • A cd-hook for bash / zsh / fish that runs phpvm --auto so the right PHP is loaded by the time the prompt comes back.
  • Per-shell switching: phpvm shell 8.2 pins a version for the current terminal only via a php shim on PATH. Two terminals can run two PHP versions at once, no sudo.
  • phpvm install <ver> adds a new PHP version straight from the upstream repo (OndΕ™ej SurΓ½'s PPA on Ubuntu, deb.sury.org on Debian) without hand-running apt install.
  • 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.

Installing

One-liner (no local clone needed):

curl -fsSL https://raw.githubusercontent.com/rijverse/phpvm/main/install.sh | sudo bash

Pin a specific tag or branch by adding PHPVM_REF to that command, e.g. ... | sudo PHPVM_REF=v2.6.1 bash.

Or clone and run it interactively:

git clone https://github.com/rijverse/phpvm.git
cd phpvm && sudo bash install.sh

It's interactive even through the curl | sudo bash pipe: pick CLI, GUI, or both, then yes/no on the shell hook and the passwordless-sudo rule. With no terminal at all (headless CI, nohup) it falls back to non-interactive defaults.

New terminals pick up the shell hook automatically. Already-open ones don't. Run source ~/.bashrc (or your shell's rc) in those, or phpvm shell will tell you to reload until you do.

To remove it, see Uninstalling below.

Upgrading

phpvm --self-update

Pulls the latest from the repo URL captured at install time and re-runs the installer in --upgrade mode: same paths, same CLI/GUI choice, no re-prompt for sudoers or the hook. If you installed from a tarball (no recorded URL), pass one explicitly, optionally with a tag or branch:

phpvm --self-update https://github.com/rijverse/phpvm.git v2.2.0

What you need

  • Linux with update-alternatives. Tested on Ubuntu 20.04 / 22.04 / 24.04 in CI, and 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.

CLI

Keyboard-driven picker right where you live. ↑/↓ to move, Enter to pin the current shell, g for a system-wide switch, p to pin the project, q to bail.

phpvm TUI

Command What it does
phpvm Opens the TUI
phpvm --list Lists installed PHP versions
phpvm --list --paths Same list with the absolute binary path next to each version (handy for IDE setup)
phpvm --list --json Emits [{version,path,active}, ...] for scripts and IDE tooling
phpvm which 8.2 Prints the absolute path (e.g. /usr/bin/php8.2). Matches nvm which / pyenv which
phpvm --current Shows the effective version plus the shell / project / global breakdown
phpvm shell 8.2 Switches this terminal only, no sudo (see Per-shell switching)
phpvm shell --unset Drops the per-shell pin
phpvm local 8.2 Pins the project: writes .php-version, no sudo
phpvm global 8.2 Switches the system default via update-alternatives (sudo)
phpvm install 8.3 Installs PHP 8.3 from OndΕ™ej SurΓ½'s repo (see Installing PHP versions)
phpvm --auto Reads .php-version / composer.json and switches
phpvm --auto --print [dir] Prints the resolved project PHP version without switching
phpvm --enable-hook [shell] Adds the shell hook + shim 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, shim, GUI, project
phpvm --help Everything else

--set is kept as an alias for global, and --set-project for local, so old muscle memory and scripts keep working. Vim users get k/j too.

Installing PHP versions

phpvm install drives the upstream PHP repos so you don't have to add a PPA and apt install by hand:

phpvm install 8.3            # cli + common + fpm
phpvm install 8.3 --minimal  # cli + common, no fpm
phpvm install 8.3 --with curl,mbstring,xml   # add extensions
phpvm install 8.3 --use      # install, then switch to it
phpvm install latest         # highest version the repo offers
phpvm install 8.3 --print    # dry-run: show the repo + packages, touch nothing

It picks the repo from your distro:

  • Ubuntu (and derivatives like Mint, Pop!_OS): OndΕ™ej SurΓ½'s PPA, ppa:ondrej/php.
  • Debian: the deb.sury.org repo, keyring under /etc/apt/keyrings/ and a [signed-by=...] source list pinned to your release.

Versions are X.Y (or latest), and patch levels like 8.2.13 are rejected. apt runs under a normal sudo password prompt. install never touches the passwordless sudoers rule, which stays scoped to update-alternatives --set. After installing it offers to switch (or pass --use / --yes for non-interactive runs).

Other distros aren't supported: install PHP with your own package manager and phpvm will pick it up via update-alternatives.

Per-shell switching

phpvm per-shell switching: two terminals running PHP 8.2 and 8.3 at the same time, no sudo

Two terminals, two PHP versions, no sudo.

The everyday path is automatic, with no command to run. With the shell hook enabled (the installer default), each cd reads the project's .php-version (or composer.json's require.php) and points php at the matching version, for that terminal only, no sudo. Leave the project and it falls back to the global default. Each terminal is independent:

cd ~/work/legacy-app   # .php-version says 7.4  -> php is 7.4 here
cd ~/work/new-api      # composer wants ^8.2    -> php is 8.2 here
cd ~                   # no project             -> back to the global default

phpvm shell is the manual override for the current terminal, for when you want a version that differs from the project:

phpvm shell 8.2     # force this terminal to 8.2, whatever the project says
phpvm shell --unset # drop the override, back to the project / global default

Both paths use the same mechanism as rbenv, pyenv, and asdf: a tiny php shim on your PATH execs the matching /usr/bin/phpX.Y. It needs the shell hook enabled (installer default, or phpvm --enable-hook).

phpvm --current shows the three layers the shim checks, highest priority first, and uses the first one that's set:

  1. shell: your manual phpvm shell override (PHPVM_SHELL_VERSION). not pinned just means the layer below is in charge.
  2. project: the auto version from .php-version / composer.json, set by the cd-hook on every cd.
  3. global: the system default from update-alternatives (/usr/bin/php). What cron, systemd, other users, and PHP-FPM see, since none of them load your shell hook. Set it with phpvm global <v> (sudo, still aliased as phpvm --set).

To pin a version for the whole project (not just this terminal), phpvm local <v> writes .php-version. Aliased as phpvm --set-project.

Per-project PHP

echo "8.1" > .php-version   # or: phpvm local 8.1

phpvm walks up the tree for .php-version, falling back to composer.json's require.php. Caret, tilde, ranges, and | unions all resolve to the highest installed version that fits.

Shell hook (auto-switch on cd)

The easy way:

phpvm --enable-hook            # detects $SHELL
phpvm --enable-hook zsh        # or name it
phpvm --disable-hook           # undo, rc backed up
If you'd rather edit your rc yourself

System install lives under /etc/phpvm, and 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.fish

The GUI

A tray indicator sits in your panel showing the system-wide (global) PHP. A tray app isn't attached to a terminal, so it works at the global level: clicking a version runs the same switch as phpvm global. A terminal pinned with phpvm shell can sit above it, so the tray and a given shell may legitimately differ.

phpvm tray indicator showing PHP 8.4 active

Click it and you get a menu for a one-click global switch:

phpvm tray menu

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-fpm for that version is running, and a red marker if it's EOL. Each row gets Switch and Restart FPM buttons, plus 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.

FPM restart tries passwordless sudo first, and if that fails it pops the polkit auth dialog (pkexec). Either works, and nothing else is needed.

Using with your IDE

phpvm shell switches your terminal, but a GUI IDE doesn't read shell env vars. It pins its interpreter in its own settings at startup. phpvm gives you three primitives so any IDE or tool can wire in:

phpvm which 8.2          # /usr/bin/php8.2  (single path, parseable)
phpvm --list --paths     # table: version + absolute path, active marker
phpvm --list --json      # [{"version":"8.2","path":"/usr/bin/php8.2","active":true}, ...]

phpvm --list --json is the contract for tooling: stable schema (version, path, active), one entry per installed PHP, dependency-free (no jq required to produce it, only to filter).

See docs/IDE.md for per-IDE setup (PhpStorm, VS Code, Sublime/Zed/Helix, NetBeans) and CI recipes.

About sudo

Only the global switch needs sudo. phpvm global (and its --set alias) moves the system-wide /usr/bin/php symlink via sudo update-alternatives --set php .... Per-shell (phpvm shell) and per-project (phpvm local) switching touch only your own environment, so they never ask for a password.

For the global switch, the installer offers to drop a sudoers rule so you don't get a prompt:

# /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, phpvm global just asks for a password the normal way (and labels the prompt so you know who's asking). The GUI, which is global by nature, tries passwordless sudo first, then falls back to the polkit dialog.

What phpvm --doctor looks like

--doctor walks every subsystem so you can spot what's wrong without grepping logs. Sample output on a healthy install:

phpvm --doctor  v2.6.1
  user=alice  shell=bash  pwd=/home/alice/work/api

β–Έ CLI install
  βœ“ phpvm at /usr/local/bin/phpvm  (v2.6.1)
  βœ“ bash 5.1.16(1)-release

β–Έ PHP runtimes
  βœ“ update-alternatives: /usr/bin/update-alternatives
  βœ“ 3 PHP runtime(s) registered
    php8.2 β†’ 8.2.31
    php8.3 β†’ 8.3.31
    php8.4 β†’ 8.4.21
  βœ“ Active: php8.2  (/usr/bin/php8.2)
  βœ“ composer: Composer version 2.7.2

β–Έ PHP-FPM
  βœ“ php8.2-fpm.service  active=active  enabled=enabled
  βœ“ php8.3-fpm.service  active=active  enabled=enabled

β–Έ Sudo (auto-switch)
  βœ“ Sudoers rule at /etc/sudoers.d/phpvm
  βœ“ sudo -n update-alternatives works (passwordless)

β–Έ Shell hook (auto-switch on cd)
  βœ“ Hook dir /etc/phpvm
  βœ“ bash hook sourced in ~/.bashrc

β–Έ Per-shell switching (shim)
  βœ“ Shim at /etc/phpvm/shims/php
  βœ“ Shim dir on PATH

β–Έ GUI / tray (optional)
  βœ“ phpvm-gui at /usr/local/bin/phpvm-gui
  βœ“ python3-gi importable, AppIndicator3 present

β–Έ Project (cwd)
  βœ“ .php-version says 8.3  β†’ php8.3 resolved

It walks eight subsystems (CLI install, PHP runtimes, FPM, sudo, hook, shim, GUI, and the current project) and tallies the result. Each row is βœ“ (ok), ! (warn), βœ— (fail), or - (skipped, e.g. GUI not installed).

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 81

The number at the end is the priority, where 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
β”‚   └── shim-php         php resolver, installed to <hook dir>/shims/php
β”œβ”€β”€ install.sh
└── uninstall.sh

Uninstalling

One-liner (no local clone needed):

curl -fsSL https://raw.githubusercontent.com/rijverse/phpvm/main/uninstall.sh | sudo bash

Or from a local clone:

sudo bash uninstall.sh

What it removes:

  • phpvm and phpvm-gui binaries from both /usr/local/bin and ~/.local/bin
  • Hook directory (/etc/phpvm or ~/.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.

Current limits

A few things phpvm doesn't handle yet. Some are on the Roadmap, some are out of scope for now.

  • Per-shell pins are shell-only: phpvm shell lives in your interactive shell's environment, so it's invisible to cron, systemd, other users, non-interactive scripts, and the GUI. Those all follow the global default, which is what phpvm global is for.
  • Distros without update-alternatives: Arch, Fedora, RHEL, openSUSE aren't supported. Adding a backend is welcome as a contribution.
  • Web server config: Apache/Nginx still point at whatever socket or module you wired up. FPM restart is per-version and assumes systemctl restart phpX.Y-fpm style unit names.
  • Patch-level pinning: everything is X.Y. If you need 8.2.13 exactly, you'll want a different tool.
  • Polkit without a desktop session: headless boxes fall back to the regular sudo password prompt instead.

Roadmap

What's next, roughly in priority order. See ROADMAP.md for the detailed specs. Open an issue if you want to push one up the stack or claim one.

  • Extension manager: phpvm ext install xdebug redis imagick per version, with the matching php<ver>-<ext> packages and ini wiring. None of the existing PHP version managers do this well.
  • phpvm exec <ver> <cmd>: run a one-off in a specific version without switching, like nvm exec. Handy for CI and quick sanity checks.
  • Shell completion: bash/zsh/fish completion for shell, global, install, etc. so phpvm global <TAB> lists installed versions.
  • phpvm install --lts alias: track the moving LTS target without remembering version numbers. Needs a maintained EOL table.

Already shipped: phpvm install (v2.4.0) and per-shell switching (v2.5.0).

Contributing

Patches welcome. See CONTRIBUTING.md. Two ground rules: no runtime dependencies beyond what's already there, and shellcheck clean.

License

MIT

About

🐘 A fast PHP version manager for Linux featuring an interactive TUI, a system tray GUI, and automatic directory based switching.

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors