skip to content

In a composer.json, how do minimum-stability, prefer-stable and a stability flag like @beta decide whether Composer may install an unstable release?

level: middleimportance: should knowfreq 40%

answer

  1. dev < alpha < beta < RC < stable
  2. minimum-stability defaults to stable
  3. constraint@flag per package
  4. flags and settings are root-only
  5. prefer-stable picks stable when possible

basics

~20 s

minimum-stability, default stable, filters out less stable releases for all packages. A flag such as ^2.0@beta relaxes that for one package. prefer-stable true picks a stable release whenever one fits. All three are read only from the root composer.json.

solid answer

~40 s

Composer ranks stabilities `dev`, `alpha`, `beta`, `RC`, `stable`, read from a version's suffix, with branches counting as `dev`. `minimum-stability` sets the floor for **every** package and defaults to `stable`, so a beta is invisible unless you allow it. A **stability flag** relaxes the floor for **one** package: `"acme/sdk": "^2.0@beta"`, or `"acme/foo": "@dev"` with an empty constraint. `prefer-stable: true` does not change what is allowed; it makes the solver choose a stable release whenever one fits. Flags and both settings are **root-only**: if a dependency needs another package's `dev-main`, your root must list that package with `@dev` too. The safer pattern is a stable floor with per-package flags; `minimum-stability: dev` plus `prefer-stable: true` works but quietly admits a dev version wherever no stable one fits.

code

json · 8 lines
json
{
    "minimum-stability": "stable",
    "require": {
        "acme/sdk": "^2.0@beta",
        "acme/client": "dev-main",
        "acme/data": "@dev"
    }
}

go deeper

for a junior

Remember that Composer installs only stable releases by default and that @dev or @beta after a constraint allows one package to be less stable.

for a middle

Explain the stability order, that minimum-stability filters while prefer-stable only chooses, and that flags and settings are read only from the root.

for a senior

Diagnose the transitive dev-main failure by adding an @dev flag in the root, and argue for a stable floor with explicit flags over a global dev floor in production apps.

for a principal

Decide when a team may depend on pre-releases at all, and make each exception visible and time-boxed rather than hidden behind a global setting.

## Stabilities as Composer sees them Every version Composer knows has a **stability**, taken from its suffix: - `1.1-BETA` is `beta`, `1.1-RC1` is `RC`, `1.1` (no suffix) is `stable`. - Branches are always `dev`: a version-like branch `1.x` becomes `1.x-dev`, and any other branch such as `main` becomes `dev-main`. The order, from least to most stable, is **`dev`, `alpha`, `beta`, `RC`, `stable`**. Three separate knobs decide which of these the solver may pick. ## `minimum-stability`: a floor for every package `minimum-stability` is a **root-only** property of `composer.json`. It **defaults to `stable`**. Every version of every package that is less stable than the floor is removed from consideration before the solver runs. With the default, a project cannot get `2.0-beta.1` of anything, no matter how the constraint is written, unless it relaxes the rule somewhere. ## Stability flags: an exception for one package A **stability flag** is written after the constraint with `@`, in `require` or `require-dev` of the root package: ```json { "require": { "acme/sdk": "^2.0@beta", "acme/fixtures": "@dev" } } ``` - `^2.0@beta` means: the range `^2.0`, and for this package accept `beta`, `RC` and `stable` releases. - `@dev` with an empty constraint means: any version of this package, including dev branches. Flags are **root-only**. That produces the classic surprise: your project requires a package whose own `composer.json` asks for `"acme/data": "dev-main"`. The flag implied by `dev-main` counts only in the root, so resolution fails until you **also** list `"acme/data": "@dev"` in your own `require`. Composer's schema documentation shows exactly this case. ## `prefer-stable`: a preference, not a filter `prefer-stable: true` (also root-only, off unless set) changes the **choice**, not the **allowed set**. When both a stable and an unstable release satisfy the constraints, Composer takes the stable one. If only a dev version or only alphas exist for a package, they are still selected, provided `minimum-stability` allows them. The CLI can also pass `--prefer-stable` for a single `update` or `require`. | Setting | Scope | Default | Effect | |---|---|---|---| | `minimum-stability` | all packages | `stable` | removes less stable versions | | `@beta`, `@dev`, … flag | one package | none | lowers the floor for that package | | `prefer-stable` | all packages | off | picks stable when both fit | ## The two common configurations 1. **Stable floor, targeted flags** (`minimum-stability` left at `stable`, a flag on the one package you need early). Everything else stays stable; the exception is visible in review and disappears when you remove the flag after the stable release ships. 2. **`"minimum-stability": "dev"` with `"prefer-stable": true`**. Popular in project skeletons, it lets any package fall back to a dev version when no stable release satisfies the constraints. The risk is silence: a constraint that stops matching any stable release makes Composer pick a dev branch instead of failing, and the lock file records it without anyone deciding. Interviewers usually want to hear why the first is safer for production applications. ## Reading the failure When the floor removes the only matching versions, Composer's problem report says so explicitly: the root `composer.json` requires the package with your constraint, it **found** candidate versions, but they **do not match your minimum-stability**. That wording is the tell. The fix is almost never to lower the floor globally; it is one of: - add a stability flag to that one package in the root, such as `^2.0@beta`; - if the unstable version is demanded by a dependency, add that package to your root `require` with `@dev` or the matching flag; - or wait for the stable release and keep the constraint as it is. After the stable release ships, remove the flag. Leaving `@beta` in place is harmless for resolution, because stable still satisfies it, but it stops documenting anything useful. ## Pinning a branch to a commit Root `require` entries may also append a commit to a dev version, such as `dev-main#2eb0c09`. Composer's documentation calls this a temporary workaround with severe limits: the solver never sees the reference, the package's own metadata is read from the branch's current state, and a dist install may ignore the pin. Use a tagged release instead as soon as one exists. ## What this does not cover How the selected versions are frozen into `composer.lock`, and how `update` differs from `install`, belong to the lock and install workflow. Stability here only decides which versions are even candidates.

  • A dependency's composer.json sets minimum-stability to dev. Does that let your project install its dev dependencies?
    No. `minimum-stability` is root-only, so a dependency's setting is ignored. Only your root `composer.json` decides the floor, and any dev package the dependency needs must be allowed by your floor or by a stability flag you write in your own `require`.
  • With minimum-stability set to beta and prefer-stable true, a package has 3.0.0 and 3.1.0-beta1, and you require ^3.0. Which is installed?
    3.0.0. Both versions are allowed, because beta meets the floor, but `prefer-stable` makes the solver choose the stable release when one satisfies the constraint. Without `prefer-stable`, Composer would take the highest allowed version, 3.1.0-beta1.

minimum-stability is the bouncer's dress code for the whole club; a stability flag is a name on the guest list that lets one person in wearing trainers; prefer-stable is the host seating the well-dressed guest first when both are at the door.

saying these in an interview costs you the question

  • prefer-stable true blocks unstable releases from being installed
  • A dependency's minimum-stability setting applies to your project
  • minimum-stability defaults to dev in Composer
  • A stability flag like @beta changes the floor for every package
  • Stability flags work anywhere, including inside a dependency's composer.json