Skip to content

📦 Managing PHP Dependencies from Go

In 12 minutes, use the Composer CLI SDK to replace hand-written exec.Command for Install / Require / Update / Remove, and get structured dependency trees and outdated information. Requires PHP 7.4+ and Composer 2.0+ installed locally.

Why Not Use exec.Command?

Bare composer binary calls require manual argument assembly, multi-line text parsing, and exit code handling. The Composer CLI SDK wraps these into typed methods, returning error or structs, and supports Mock injection for testing.

Step 1: Create Composer Instance

composer.New(composer.DefaultOptions()) auto-detects local Composer; AutoInstall is enabled by default, auto-installing if not found.

go
comp, err := composer.New(composer.DefaultOptions())
if err != nil {
    log.Fatal(err)
}
comp.SetWorkingDir("/path/to/php/project") // Point to directory containing composer.json

Step 2: Install Existing Dependencies

Install(noDev, optimize) corresponds to composer install. The first parameter controls whether to exclude dev dependencies, the second controls autoloader optimization.

Step 3: Add / Remove / Upgrade

  • RequirePackage(name, version, dev) — Add dependency and write to composer.json.
  • Update(packages, noDev) — Upgrade specified packages (empty slice means full update).
  • Remove(name, dev) — Remove dependency.

Step 4: Get Structured Results

  • ShowDependencyTree(name) — Returns dependency tree text for a package.
  • GetOutdatedInfo() — Returns *OutdatedResult, containing current version, latest version, and upgrade type for each outdated package.

Complete Runnable Example

go
package main

import (
	"fmt"
	"log"

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

func main() {
	comp, err := composer.New(composer.DefaultOptions())
	if err != nil {
		log.Fatal(err)
	}
	comp.SetWorkingDir("/tmp/my-php-project")

	// 1️⃣ Install dependencies from composer.lock (no dev, optimize autoloader)
	if err := comp.Install(false, true); err != nil {
		log.Printf("install: %v", err)
	}

	// 2️⃣ Add monolog ^3.0
	if err := comp.RequirePackage("monolog/monolog", "^3.0", false); err != nil {
		log.Fatalf("require: %v", err)
	}

	// 3️⃣ Upgrade monolog (empty slice means only update listed packages)
	if err := comp.Update([]string{"monolog/monolog"}, false); err != nil {
		log.Printf("update: %v", err)
	}

	// 4️⃣ View dependency tree
	tree, err := comp.ShowDependencyTree("monolog/monolog")
	if err != nil {
		log.Printf("tree: %v", err)
	} else {
		fmt.Println("🌳 Dependency tree:")
		fmt.Println(tree)
	}

	// 5️⃣ Structured outdated packages list
	outdated, err := comp.GetOutdatedInfo()
	if err != nil {
		log.Printf("outdated: %v", err)
	} else {
		fmt.Printf("⏳ Outdated packages: %d\n", len(outdated.Installed))
		for _, p := range outdated.Installed {
			fmt.Printf("   - %s: %s%s (%s)\n",
				p.Name, p.Installed, p.Latest, p.LatestStatus)
		}
	}

	// 6️⃣ Remove monolog
	if err := comp.Remove("monolog/monolog", false); err != nil {
		log.Printf("remove: %v", err)
	}
}

Expected Output

🌳 Dependency tree:
monolog/monolog 3.5.0
├── psr/log 3.0.0
└── ...
⏳ Outdated packages: 2
   - monolog/monolog: 3.5.0 → 3.7.1 (semver-safe-update)
   - psr/log: 3.0.0 → 3.0.2 (semver-safe-update)

Advanced: WithOptions & Multi-Package Operations

Use *WithOptions variants when passing extra CLI parameters:

go
// Install with prefer-dist and skip scripts
comp.InstallWithOptions(map[string]string{
    "prefer-dist": "",
    "no-scripts":  "",
})

// Require multiple packages at once
comp.RequireMultiple(map[string]string{
    "monolog/monolog": "^3.0",
    "symfony/console": "^7.0",
}, false)

Dry Run Before Real Changes

InstallDryRun(), UpdateDryRun(packages), RequireDryRun(name, version) only print without writing to disk, suitable for CI rehearsal.

Working Directory Matters

Forgetting SetWorkingDir will execute composer in the current Go process directory, likely reporting "composer.json not found". Always point to the real PHP project root directory.

Next Steps

Released under the MIT License