phpdiscover is a zero-dependency Go package that finds all PHP binaries
installed on the current machine. Every candidate is collected, verified and
deduplicated.
package main
import (
"context"
"fmt"
"github.com/shyim/go-php-discover"
)
func main() {
phps := phpdiscover.Discover(context.Background())
for _, p := range phps {
fmt.Printf("PHP %s at %s (%s)\n", p.Version, p.Path, p.Source)
}
}Result helpers:
system := phpdiscover.Default(phps) // first $PATH binary, else newest
latest := phpdiscover.Latest(phps) // newest version
php82 := phpdiscover.FindVersion(phps, "8.2") // newest matching "8", "8.2" or "8.2.3"
phpdiscover.Sort(phps) // ascending by versionOptions:
phpdiscover.Discover(ctx,
phpdiscover.WithTimeout(2*time.Second), // per-binary probe timeout (default 5s)
phpdiscover.WithConcurrency(8), // parallel probes (default 4)
phpdiscover.WithLogger(log.Printf), // progress messages
phpdiscover.WithExtraDirs("/opt/my-php"), // scan extra directories
phpdiscover.WithSources(phpdiscover.SourcePath), // only use $PATH
phpdiscover.WithoutSource(phpdiscover.SourceNix), // skip one source
)Candidates come from four places:
$PATH— every directory in$PATHis scanned forphp,php8,php8.2,php8.2.3,php-cli, ... The first PHP found in$PATHis markedIsSystem.- Known hoster locations — well-known locations used by hosting
providers (
/opt/plesk/php/*/bin/php,/opt/alt/php*/usr/bin/php,/usr/local/phpfarm/..., MAMP, ...). The{major},{minor},{release}placeholders are turned into glob patterns so that every installed version is matched. - Well-known installation directories —
/usr/bin,/usr/local/bin, XAMPP, phpbrew, phpenv, Herd-lite, Homebrew (brew --cellar), Nix (builtins.storeDir), asdf-vm, Remi's RPM repo, and on macOS MAMP, MacPorts and Liip PHP; on Windows XAMPP, Cygwin, Chocolatey, WAMP, MAMP and Herd. - Extra directories — supplied via
WithExtraDirs.
Every candidate is verified by running php --version (with a timeout, in
parallel) and parsing the PHP X.Y.Z version from its output. Results are
deduplicated by the resolved (real) path, so symlinks like
/usr/local/bin/php -> ../Cellar/php/8.5.9/bin/php collapse into a single
entry. Sibling binaries of the same installation are attached when present:
p.FPMPath // php-fpm
p.CGIPath // php-cgi
p.PHPConfigPath // php-config
p.PHPizePath // phpize
p.PHPdbgPath // phpdbg$ go run ./cmd/phpdiscover
PHP 8.3.33
path: /opt/homebrew/Cellar/php@8.3/8.3.33/bin/php
source: homebrew
fpm: /opt/homebrew/Cellar/php@8.3/8.3.33/sbin/php-fpm
cgi: /opt/homebrew/Cellar/php@8.3/8.3.33/bin/php-cgi
config: /opt/homebrew/Cellar/php@8.3/8.3.33/bin/php-config
phpize: /opt/homebrew/Cellar/php@8.3/8.3.33/bin/phpize
phpdbg: /opt/homebrew/Cellar/php@8.3/8.3.33/bin/phpdbg
PHP 8.4.24
...
PHP 8.5.9 (system)
path: /opt/homebrew/Cellar/php/8.5.9/bin/php
via: /opt/homebrew/bin/php
source: PATH
...
- Returns all binaries instead of a single "best" one and works without
a running PHP (no
PHP_BINARY-relative lookups). - Zero external dependencies.
- Symlinks are resolved and deduplicated; sibling server binaries are attached to each version.
- No disk cache: every call does a fresh, fast discovery.
The project uses golangci-lint (v2) with all
linters enabled except five, each with a documented reason in
.golangci.yml:
wsl/gomodguard— deprecated, superseded by the enabledwsl_v5/gomodguard_v2forbidigo— its default rules forbidfmt.Print*, which the CLI legitimately usestestpackage— the unit tests exercise unexported helpersvarnamelen— idiomatic short names (p,v,i,ok) in small scopes
golangci-lint run ./... # lint everything
golangci-lint fmt # apply gofumpt + goimports
Note: wsl_v5 and nlreturn enforce an airy whitespace style (blank lines
between unrelated statements, before return/break/continue). Keep new
code consistent with it, or drop those linters from disable: if you dislike
the style.
The module lives at github.com/shyim/go-php-discover (private). If you later
publish it publicly or move it to another account, run:
go mod edit -module github.com/<owner>/go-php-discover
update the import path in cmd/phpdiscover/main.go and the depguard
allowlist in .golangci.yml, and adjust the CI badge URL at the top of this
README to match the new repository.
MIT, see LICENSE.