Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Composer

Basics

Composer manifests are composer.json, with dependencies under require (production) and require-dev (development):

{
  "require": {
    "symfony/console": "^7.0",
    "monolog/monolog": "~3.5"
  }
}

Platform packages (php, ext-*, lib-*) are filtered out — they name a PHP runtime or extension, not a Packagist package, and have no registry entry to resolve. Every remaining entry is resolved against Packagist’s metadata API (repo.packagist.org/p2/{vendor}/{package}.json), with hover showing the latest version, license, and (if the maintainer flagged the package) an “abandoned” notice. Completion queries Packagist’s packagist.org/search.json endpoint. Version constraints use Composer’s own syntax — caret (^7.0, compatible up to the next major), tilde (~3.5, compatible up to the next minor), exact pins, wildcards, hyphenated ranges (1.0 - 2.0, equivalent to >=1.0.0 <2.1.0), and OR-separated alternatives (1.0 || 2.0, or the equivalent single-pipe 1.0 | 2.0) — compared with the stability-aware ordering described below. When a composer.lock exists alongside the manifest, it is read to resolve each dependency’s actual in-use (installed) version, shown alongside the declared constraint.

Version Comparison

compare_versions previously discarded everything after the first non-digit character in a dot-separated segment, so a prerelease/qualifier version compared as equal to its stable counterpart. Composer now applies proper stability-aware ordering: stability precedence dev < alpha < beta < RC < stable, applied both to requirement-satisfaction comparison and to “latest version” selection. select_latest_matching/get_latest_matching exclude alpha/beta/RC releases by default (mirroring Composer’s minimum-stability: stable default) unless overridden; a wildcard/existence-check requirement still resolves a prerelease-only package instead of reporting no version found. The effective stability floor is now manifest- and dependency-aware: a per-dependency @stability flag (^1.0@beta) or a directly-pinned prerelease version overrides the manifest’s own composer.json minimum-stability field, which in turn overrides the stable default — reflected consistently across diagnostics, hover, completion, and code actions, via a shared SelectionContext rather than a diagnostics-only path. Editing minimum-stability in an already-open document forces a full re-fetch, so the other surfaces don’t keep showing a stale “latest” behind the new stability floor. Separator-less and dot/underscore-separated prerelease suffixes (1.0.0RC1, 2.6.3.alpha) classify consistently regardless of v/V prefix. A minimum-stability value that isn’t one of dev/alpha/beta/RC/stable (or isn’t a string at all) now surfaces a warning diagnostic pointing at the offending value; selection still falls back to stable.

Composer’s update code actions and completion also preserve the requirement’s own v-prefix style instead of forcing the raw Packagist tag’s prefix onto an unprefixed requirement (or vice versa).

See Dart for the equivalent fix in that ecosystem, which shares the same underlying compare_versions bug class and was corrected in the same change.

Non-Registry Dependency Sources

A require entry bound to a non-vcs-heuristic repositories entry is classified as non-registry instead of defaulting to Packagist: a package-type repository (matched by its embedded package.name), an artifact-type repository, and an only/exclude wildcard-filtered vcs/path entry (Composer’s * glob syntax, not just exact names). A top-level {"packagist.org": false} entry disables the default registry outright. A dependency resolved this way is never sent to Packagist, drops its public-registry hover link, and is excluded from OSV vulnerability scanning against the public package name (resolves #1202).

A bare vcs/path/artifact repository entry with no only filter has no static per-package name binding in composer.json itself (Composer tries every declared repository, in order, for any required package) — an earlier vendor-substring URL heuristic covered this case but produced false positives that silently disabled OSV scanning for unrelated public packages sharing a GitHub org with a private repository’s URL (e.g. one vcs entry for github.com/acme/internal incorrectly reclassifying an unrelated public acme/-scoped package too). The heuristic was removed rather than fixed. Instead, when a bare repository of this kind is declared, an ancestor composer.lock (once composer install has run) is cross-checked for the affected dependency’s own recorded source.type, and only "path" is trusted as a non-registry signal — never "git", since composer.lock records a "git" source for essentially every ordinary Packagist-resolved package too (Packagist itself mirrors GitHub/GitLab/Bitbucket-hosted packages), so it cannot distinguish a genuinely private package from an ordinary public one (resolves #1212).

Known limitation: the vcs-repository case from #1202’s original report (a private git-hosted package, no lockfile equivalent to Path’s unambiguous signal) remains an accepted gap, as does a lockless manifest (no composer.lock present) and the artifact repository type (Composer’s lock records an artifact-sourced package under dist, not source, so this repository kind can never trigger the override).

Deprecation & Abandoned Packages

Composer’s abandoned field powers two cross-ecosystem features rather than a Composer-specific one — see Package Deprecation Diagnostics for the diagnostic and the Composer-only “Replace with X” code action, and Yanked Version Diagnostic for how abandoned also feeds the yanked-version check (restricted to exact-pin requirements).

Licensing

Composer’s license arrives for free in its hot-path Packagist registry response, so it is covered by both License Hover and the License Policy Diagnostic with no dedicated pre-fetch.