skip to content

Manifest & Constraints

composer.json declares what a project needs: require vs require-dev, caret and tilde constraints, stability flags, and php and ext-* platform packages. Interviewers ask how ^1.2 differs from ~1.2.

on this pageshow

explore

questions

5

In a composer.json, what is the difference between require and require-dev, and when are require-dev packages not installed?

level: juniorimportance: must knowfreq 70%

answer

  1. runtime code vs tooling
  2. root-only section
  3. a dependency's require-dev is ignored
  4. composer require --dev
  5. Class not found only in production

basics

~20 s

require lists packages the code needs at runtime, installed everywhere and passed on to the package's consumers. require-dev lists test and tooling packages; it is root-only, so a dependency's dev list is never installed, and --no-dev builds skip it.

solid answer

~50 s

`require` is the list of packages your code cannot run without: they are installed in every environment, and when your package is itself a dependency of someone else, its `require` entries become their transitive requirements. `require-dev` is for what you need only to *develop* the package: PHPUnit or Pest, PHPStan, a coding-standard fixer, fixtures. It is **root-only** — Composer reads it only from the project you run Composer in, so a library's `require-dev` is never installed into the projects that use it. In the root project, dev packages are installed by default and are left out when you run an install or update with `--no-dev`, which is what a production build does. You add to it with `composer require --dev vendor/package`. The classic bug is a class your `src/` code uses that was put in `require-dev`: everything passes locally and in CI, then production fails with a class-not-found error.

code

bash · 3 lines
bash
composer require monolog/monolog
composer require --dev phpunit/phpunit phpstan/phpstan
composer remove --dev phpstan/phpstan

go deeper

for a junior

Know which section a test framework goes in and which one a runtime library goes in, and that composer require --dev adds to require-dev.

for a middle

Explain that require-dev is root-only, so a library's dev packages never reach its consumers, and that --no-dev builds leave them out.

for a senior

Show how a runtime class misplaced in require-dev slips past every dev and CI environment, and add a --no-dev boot check to the pipeline to catch it.

for a principal

Treat the require list of a published library as part of its public contract: every entry constrains every consumer's solver, so keep it minimal and justify each one.

## Two maps with the same syntax Both `require` and `require-dev` are **package links**: JSON objects that map a package name to a **version constraint**. Composer reads them with the same parser, so `^3.0`, `~1.2`, `1.0.*` and platform packages like `php` or `ext-mbstring` work in either. What differs is **who gets the packages** and **when**. ```json { "require": { "php": "^8.3", "monolog/monolog": "^3.0" }, "require-dev": { "phpunit/phpunit": "^13.0", "phpstan/phpstan": "^2.1" } } ``` ## What `require` means `require` is the list of packages the code needs **to run**. Composer's schema says the package will not be installed unless those requirements can be met. Two consequences follow: - In your own project, every `require` entry is installed in every environment: laptops, CI and production. - When your package is a **library** that someone else installs, its `require` entries become **their** transitive requirements. The solver must find versions that satisfy your constraints and theirs together, which is why a library's `require` should be as small and as loosely constrained as its code allows. ## What `require-dev` means `require-dev` is for packages needed only to **develop or test** the package: a test framework, a static analyser, a coding-standard fixer, a debugging helper, test fixtures. Two rules govern it: 1. **It is root-only.** Composer honours `require-dev` only in the **root package**, the `composer.json` of the project you run Composer in. A library's `require-dev` is ignored completely when that library is installed as someone's dependency. That is the point: consumers of your library do not download your test framework. 2. **It is installed by default in the root project.** A plain `composer install` or `composer update` installs dev requirements too. Passing `--no-dev` leaves them out; that is what a production or container build normally does. | | `require` | `require-dev` | |---|---|---| | Installed for the root project by default | yes | yes | | Installed with `--no-dev` | yes | no | | Installed when the package is someone's dependency | yes | never (root-only) | | Typical contents | frameworks, clients, `php`, `ext-*` | test framework, static analysis, fixers | | Added with | `composer require vendor/pkg` | `composer require --dev vendor/pkg` | ## The two mistakes interviewers probe **A runtime class in `require-dev`.** Suppose `src/` uses a Markdown parser that was added with `--dev` by accident. Every developer machine and every CI job installs dev packages, so all tests pass. The production image is built with `--no-dev`, the parser is missing, and the first request that touches it dies with `Class "..." not found`. The fix is to move the package to `require`; the lesson is that `require-dev` must contain nothing that production code paths load. **A dev tool in `require`.** Putting PHPUnit or PHPStan in `require` does not break anything locally, but it ships the tool and its whole dependency tree to production, and — for a library — forces it on every consumer, where its own constraints can now conflict with theirs. It also widens the attack surface of the deployed code. ## Adding, moving and removing entries - `composer require vendor/pkg` adds to `require`; `composer require --dev vendor/pkg` adds to `require-dev`. If you name no constraint, Composer picks one from the newest stable release, typically a caret constraint such as `^3.0`. - `composer remove --dev vendor/pkg` removes it from `require-dev`. - Moving a package between sections: run `composer require vendor/pkg` without `--dev` for a package that sits in `require-dev` (or the reverse). Composer warns that the entry will move to the other key and, in an interactive terminal, asks for confirmation first. ## A quick placement test When deciding where a new package goes, ask three questions in order: 1. **Does any file under `src/` (or config the application loads in production) reference it?** Then it belongs in `require`, full stop. 2. **Is it only used by tests, static analysis, code generation you run by hand, or local debugging?** Then it belongs in `require-dev`. 3. **Is this a library?** Then double-check every `require` entry: each one is imposed on every consumer, while `require-dev` costs them nothing. A useful CI safeguard is a job that installs with `--no-dev` and then boots the application or runs a smoke script. It turns the misplaced-package bug from a production incident into a failed build. ## Where the rest lives Class autoloading for test code (`autoload-dev`) is a separate key handled by Composer's autoloader generation. How `install`, `update`, `--no-dev` and the lock file interact during a deploy belongs to the lock-file and install workflow. This question is only about which map a package belongs in, and who receives it.

  • You maintain a library whose tests need a fake HTTP server package. Where does it go, and do your users download it?
    It goes in `require-dev`. Because `require-dev` is root-only, Composer reads it only when your library is the root project, for example when you clone it and run its tests. Projects that install your library never see that entry, so they never download the fake server or its dependencies.
  • A deploy fails with a class-not-found error that no test ever caught. How do you check whether the Composer sections are the cause?
    Find which package provides the class and look it up in `composer.json`. If it sits under `require-dev` while `src/` uses it, the `--no-dev` production build left it out. Move it to `require` by running `composer require vendor/pkg` without `--dev`; Composer warns that it is moving the entry from `require-dev`. Then add a CI job that installs with `--no-dev` and boots the application so the gap shows before a deploy.

saying these in an interview costs you the question

  • require-dev packages are never installed on a developer machine
  • A library's require-dev is installed into every project that uses it
  • Putting PHPUnit in require is harmless because nobody runs it in production
  • require-dev uses a different constraint syntax from require
  • Composer warns you when src code uses a package that is only in require-dev
open as a page

In a composer.json constraint, how does ^1.2 differ from ~1.2, and how do ^1.2.3, ~1.2.3 and ^0.3 behave?

level: middleimportance: must knowfreq 65%

basics

~10 s

In Composer, ^1.2 and ~1.2 are the same range, >=1.2.0 <2.0.0. They differ with three parts: ~1.2.3 means >=1.2.3 <1.3.0 while ^1.2.3 means >=1.2.3 <2.0.0. For 0.x, ^0.3 means >=0.3.0 <0.4.0.

open as a page

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%

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.

open as a page

Your team runs PHP 8.5 locally but production runs 8.3; how do composer.json's php and ext-* requirements and config.platform keep composer update from locking incompatible packages?

level: seniorimportance: should knowfreq 38%

basics

~20 s

php and ext-* in require are platform packages: Composer installs nothing, only checks the running PHP. Setting config.platform.php to production's version, such as 8.3.12, makes composer update resolve as if on that PHP, so nothing needing 8.4 gets locked.

open as a page

In a Composer library's composer.json, how do you declare support for both major versions 1 and 2 of a dependency, and what mistakes break consumers?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Write an OR constraint such as "acme/serializer": "^1.8 || ^2.0", with each lower bound the oldest release you test. Avoid >=1.8, which also accepts 3.0, and exact pins, which leave consumers' solvers no room; test the lowest and highest resolutions in CI.

open as a page