Skip to content

🔍 检测器 Detector

跨操作系统定位系统中已安装的 Composer 可执行文件,是「先探测、再调用」工作流的第一步。

包路径:github.com/scagogogo/composer-skills/pkg/detector

Composer 是一个 PHP 可执行文件,可能以 composer(可执行二进制)或 composer.phar(PHP 归档)两种形态存在于系统的不同位置:PATH、/usr/local/bin、Homebrew 目录、COMPOSER_HOME、Windows 的 %APPDATA%\Composer 等。detector 包把「在哪儿能找到它」这件事收敛成一个跨平台的 Detector 类型,按固定优先级逐个探测,命中即返回。

核心能力清单

🔍

跨 OS 探测

同一份 API 在 darwin / linux / windows 上行为一致,平台差异通过 build tag 自动切换。

🛤️

固定优先级

COMPOSER_PATH → COMPOSER_HOME → 常见路径 → which/where → composer.phar。

📋

详细结果

DetectVerbose 返回路径 + 命中方式 + 是否 .phar,便于诊断。

轻量判断

IsInstalled 一行代码判断是否已装,无需关心路径。

类型与函数

Detector

检测器主体,内部只持有「候选路径列表」一个状态。

go
type Detector struct {
    possiblePaths []string
}

NewDetector() 创建时会调用 defaultPossiblePaths(),根据当前操作系统注入平台特定的常见路径(详见下文「平台差异」)。

DetectionResult

DetectVerbose 返回的详细结果,告诉你 Composer 是「怎么被找到的」。

go
type DetectionResult struct {
    Path   string // 检测到的 Composer 可执行文件路径
    Method string // 命中方式:env:COMPOSER_PATH / env:COMPOSER_HOME/vendor/bin / default_path / which/where
    IsPhar bool   // 是否为 .phar 文件
}

检测顺序

DetectDetectVerbose 按以下顺序逐级探测,命中即返回(短路):

顺序探测源Method 值说明
1️⃣环境变量 COMPOSER_PATHenv:COMPOSER_PATH用户显式指定的二进制路径,最高优先
2️⃣环境变量 COMPOSER_HOMEenv:COMPOSER_HOME/vendor/bin拼接 $COMPOSER_HOME/vendor/bin/composer
3️⃣possiblePaths 候选路径default_path平台常见路径 + ./composer + ./composer.phar
4️⃣which / where 命令which/where让系统 PATH 自己解析 composer
5️⃣composer.phar 兜底Detectcomposer.phar / ./composer.phar / ~/composer.phar(非 Windows)

为什么要分 Detect 和 DetectVerbose

Detect 只返回路径字符串,适合直接喂给 composer.New()Options.ExecutablePathDetectVerbose 额外返回命中方式与 .phar 标记,适合做诊断报告、日志或决定后续用 php composer.phar 还是直接调用 composer

方法签名

NewDetector

创建检测器,候选路径已按当前 OS 预填。

go
func NewDetector() *Detector

SetPossiblePaths

整体替换候选路径列表(覆盖默认平台路径)。

go
func (d *Detector) SetPossiblePaths(paths []string)

AddPossiblePath

在已有候选路径后追加一条(不覆盖)。

go
func (d *Detector) AddPossiblePath(path string)

Detect

检测并返回 Composer 可执行文件路径。

go
func (d *Detector) Detect() (string, error)

未找到时返回 ErrExecutableNotFound未找到composer可执行文件)。

DetectVerbose

检测并返回详细结果(路径 + 命中方式 + 是否 phar)。

go
func (d *Detector) DetectVerbose() (*DetectionResult, error)

DetectVerbose 不含 phar 兜底

DetectVerbose 不包含 Detect 第 5 步的 composer.phar 文件兜底逻辑,只覆盖前四步。需要兼容散落 phar 的场景时请用 Detect

IsInstalled

go
func (d *Detector) IsInstalled() bool

等价于 _, err := d.Detect(); return err == nil

平台差异

getPlatformSpecificPaths() 通过 build tag 在编译期选择实现:

🍎 Darwin(macOS)

go
//go:build darwin
func getPlatformSpecificPaths() []string {
    return []string{
        "/usr/local/bin/composer",
        "/usr/bin/composer",
        "/opt/homebrew/bin/composer",                 // Apple Silicon Homebrew
        filepath.Join(os.Getenv("HOME"), ".composer/vendor/bin/composer"),
        filepath.Join(os.Getenv("HOME"), "composer.phar"),
    }
}

🐧 Unix(Linux 等,!windows && !darwin

go
//go:build !windows && !darwin
func getPlatformSpecificPaths() []string {
    return []string{
        "/usr/local/bin/composer",
        "/usr/bin/composer",
        filepath.Join(os.Getenv("HOME"), ".composer/vendor/bin/composer"),
        filepath.Join(os.Getenv("HOME"), "composer.phar"),
    }
}

🪟 Windows

go
//go:build windows
func getPlatformSpecificPaths() []string {
    return []string{
        filepath.Join(os.Getenv("APPDATA"), "Composer", "composer.phar"),
        filepath.Join(os.Getenv("ProgramFiles"), "Composer", "composer.phar"),
        filepath.Join(os.Getenv("ProgramFiles(x86)"), "Composer", "composer.phar"),
        "composer.phar",
        "composer.bat",
        "composer",
    }
}

三个平台实现都会再叠加 ./composer./composer.phar 两条当前目录通用路径(由 defaultPossiblePaths 统一追加)。

可执行性判断

isExecutable 在 Unix 系通过 info.Mode().Perm() & 0111 检查执行位;在 Windows 上无法直接判定执行位,只要是普通文件即视为可执行。.phar 文件用 fileExists 判断存在性即可。

快速示例

基础用法:拿到路径直接用

go
package main

import (
    "fmt"
    "log"

    "github.com/scagogogo/composer-skills/pkg/detector"
)

func main() {
    d := detector.NewDetector()

    path, err := d.Detect()
    if err != nil {
        log.Fatalf("未检测到 Composer: %v", err)
    }
    fmt.Println("Composer 路径:", path)
}

详细诊断:看命中了哪一级

go
package main

import (
    "fmt"
    "log"

    "github.com/scagogogo/composer-skills/pkg/detector"
)

func main() {
    d := detector.NewDetector()

    result, err := d.DetectVerbose()
    if err != nil {
        log.Fatalf("未检测到 Composer: %v", err)
    }
    fmt.Printf("路径: %s\n命中方式: %s\n是否 phar: %v\n",
        result.Path, result.Method, result.IsPhar)
}

自定义候选路径

go
package main

import (
    "fmt"

    "github.com/scagogogo/composer-skills/pkg/detector"
)

func main() {
    d := detector.NewDetector()

    // 整体替换为只探测企业内部路径
    d.SetPossiblePaths([]string{
        "/opt/company/bin/composer",
        "/usr/local/bin/composer",
    })

    // 或在默认路径基础上追加一条
    d.AddPossiblePath("/data/composer/composer.phar")

    if d.IsInstalled() {
        path, _ := d.Detect()
        fmt.Println("找到:", path)
    }
}

进阶

  • 与 Installer 配合Detect 返回 ErrExecutableNotFound 时,可转而调用 pkg/installerEnsureComposerInstalled 自动安装。
  • 与 Composer 配合:拿到路径后,把它传给 composer.Options.ExecutablePath,避免 pkg/composer 内部再次 LookPath
  • COMPOSER_PATH 的语义:它仅作用于 detector,并非 Composer 原生环境变量;用来锁定「用哪一个 composer」非常方便,尤其适合多版本共存(Composer 1 / 2 / preview)的机器。
  • 并发安全Detector 未加锁,SetPossiblePaths / AddPossiblePath 会修改内部切片;多 goroutine 共享同一实例时应在外部同步,或每个 goroutine 各自 NewDetector()

基于 MIT 许可证发布