Skip to content

🪞 Package Mirror

In 10 minutes, use the Packagist API client to fetch package indices, list packages, and statistics to build a lightweight "mirror view". Pure Go, no PHP required.

Why a Mirror View?

When building internal development platforms or offline mirrors, you often need "what packages exist on Packagist, what versions each has, download counts". Hand-writing HTTP + JSON parsing is verbose. The Packagist API client wraps these into methods that directly return structs.

Data Source Methods

PurposeMethodReturn
Site-wide statisticsc.GetStatistics()*StatisticsResponse (Totals.Packages/Versions/Downloads)
All package namesc.ListPackages()*PackageListResponse
List by vendorc.ListPackagesByVendor(vendor)*PackageListResponse
List by typec.ListPackagesByType(type)*PackageListResponse
Popular packagesc.ListPopularPackages(perPage)*PopularPackagesResponse
Package detailsc.GetPackage(name)*ComposerPackageInfo

Complete Runnable Example

go
package main

import (
	"encoding/json"
	"fmt"
	"os"
	"time"

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

type MirrorSnapshot struct {
	CapturedAt     time.Time          `json:"captured_at"`
	TotalPackages  int                `json:"total_packages"`
	TotalVersions  int                `json:"total_versions"`
	TotalDownloads int64              `json:"total_downloads"`
	VendorPackages map[string]int     `json:"vendor_packages"`
	PopularTop     []string           `json:"popular_top"`
}

func main() {
	c := client.NewComposerClient(60 * time.Second)
	snap := MirrorSnapshot{CapturedAt: time.Now(), VendorPackages: map[string]int{}}

	// 1️⃣ Site-wide statistics
	if stats, err := c.GetStatistics(); err == nil {
		snap.TotalPackages = stats.Totals.Packages
		snap.TotalVersions = stats.Totals.Versions
		snap.TotalDownloads = stats.Totals.Downloads
		fmt.Printf("📊 Packages: %d | Versions: %d | Downloads: %d\n",
			snap.TotalPackages, snap.TotalVersions, snap.TotalDownloads)
	}

	// 2️⃣ Count packages by vendor
	for _, vendor := range []string{"symfony", "laravel", "guzzlehttp", "psr"} {
		if list, err := c.ListPackagesByVendor(vendor); err == nil {
			snap.VendorPackages[vendor] = len(list.PackageNames)
			fmt.Printf("🏷️  %s/*: %d packages\n", vendor, len(list.PackageNames))
		}
	}

	// 3️⃣ Popular packages Top N
	if popular, err := c.ListPopularPackages(10); err == nil {
		for _, p := range popular.Packages {
			snap.PopularTop = append(snap.PopularTop, p.Name)
		}
		fmt.Printf("🌟 Top %d popular packages: %v\n", len(snap.PopularTop), snap.PopularTop)
	}

	// 4️⃣ Write snapshot to disk
	b, _ := json.MarshalIndent(snap, "", "  ")
	os.WriteFile("mirror-snapshot.json", b, 0644)
	fmt.Println("\n✅ Snapshot written to mirror-snapshot.json")
}

Expected Output

📊 Packages: 390000 | Versions: 3200000 | Downloads: 9876543210
🏷️  symfony/*: 156 packages
🏷️  laravel/*: 24 packages
🏷️  guzzlehttp/*: 12 packages
🏷️  psr/*: 18 packages
🌟 Top 10 popular packages: [psr/log monolog/monolog symfony/console ...]

✅ Snapshot written to mirror-snapshot.json

Advanced: Incremental Sync & Field Pruning

  • c.GetPackageChanges(ctx, since) returns packages changed since timestamp since, suitable for scheduled incremental mirror sync.
  • c.ListPackagesWithData(fields) only fetches specified fields (e.g., name,downloads), reducing payload size.
go
// Incremental sync: only process packages changed in the past hour
changes, _ := c.GetPackageChanges(context.Background(), time.Now().Add(-time.Hour).Unix())
fmt.Printf("Packages changed in last hour: %d\n", len(changes.Actions))

// Slim list: only package names and download counts
slim, _ := c.ListPackagesWithData([]string{"name", "downloads"})

Mirror vs Full Clone

This tutorial is a "view-level mirror" — fetching indices and statistics for dashboards/search. If you want to build an offline mirror that can actually be used for composer install, use 🏭 Satis Private Repository Builder.

Pagination & Payload Size

ListPackages() returns the full package name list, which can be tens of MB. In production, recommend fetching by vendor/type, or using ListPackagesWithData to limit fields.

Next Steps

Released under the MIT License