composer.json Explained: Every Field for PHP Package Management

Last updated:

composer.json is PHP's manifest file — the direct parallel to package.json for npm. It tells Composer which packages a project requires, the autoload rules for your own source code (PSR-4 is the modern standard), custom scripts to run on lifecycle events, and the metadata used when publishing to Packagist. Dozens of fields are valid against the schema, but in practice name, require, and autoload are the three you will actually use daily.

Need to validate a composer.json file? Paste it into Jsonic's JSON Validator — it pinpoints syntax errors with line and column numbers before Composer ever sees them.

Validate composer.json

Required fields: name and version (sort of)

Only name is unconditionally required. version is required only when you publish a package without a tagged Git repository — Packagist reads versions from your Git tags, so most libraries omit the field entirely. Application projects (those with type: projectthat you never publish) technically don't need nameeither, but Composer warns if it's missing.

{
  "name": "acme/my-app",
  "description": "Short package description shown on Packagist",
  "type": "project",
  "license": "MIT",
  "keywords": ["api", "rest", "json"],
  "homepage": "https://github.com/acme/my-app",
  "authors": [
    {
      "name": "Jane Doe",
      "email": "jane@example.com",
      "role": "Developer"
    }
  ],
  "support": {
    "issues": "https://github.com/acme/my-app/issues",
    "source": "https://github.com/acme/my-app"
  }
}

name rules: vendor/package format, lowercase, alphanumeric plus hyphens/underscores/dots. The vendor segment must exist; pick a vendor namespace that matches your organization or GitHub user.

type values worth knowing:

  • library — default. A reusable package installed to vendor/.
  • project — an application (Laravel, Symfony, Drupal). Used by composer create-project as a template.
  • composer-plugin — a plugin that extends Composer itself. Must declare a plugin class in extra.
  • metapackage — empty package that only pulls in other packages via require. No files installed.

license uses SPDX identifiers (MIT, Apache-2.0, GPL-3.0-or-later). Use an array for dual licensing: ["MIT", "Apache-2.0"].

require and require-dev: PHP dependency types

Composer has two dependency buckets. require is for everything your package needs at runtime; require-dev is for development-only tools. Unlike npm there are no peerDependencies or optionalDependencies concepts — Composer expresses similar ideas through provide, conflict, and suggest at the top level.

FieldInstalled whenVisible to consumers?Typical use
requireAlways — including --no-dev production installsYes — transitively pulled inPHP itself, framework, runtime libraries
require-devOnly on composer install in your own repoNoPHPUnit, PHPStan, Psalm, code style fixers
suggestNever auto-installed — just a hint shown after installYes — message displayedOptional integrations (ext-redis for caching)
conflictComposer refuses to install if matchedYesBlock known-broken combinations
provideDeclares a virtual package this one fulfillsYesPSR implementations (psr/log-implementation)
{
  "require": {
    "php": "^8.2",
    "ext-mbstring": "*",
    "ext-json": "*",
    "guzzlehttp/guzzle": "^7.8",
    "monolog/monolog": "^3.5",
    "symfony/console": "^7.0"
  },
  "require-dev": {
    "phpunit/phpunit": "^10.5",
    "phpstan/phpstan": "^1.10",
    "friendsofphp/php-cs-fixer": "^3.50"
  },
  "suggest": {
    "ext-redis": "Required for the Redis cache adapter"
  },
  "conflict": {
    "doctrine/dbal": "<3.0"
  }
}

Version constraint syntax (full reference at getcomposer.org/doc/articles/versions):

  • ^7.8 — caret. Allows changes that don't break compat: >=7.8.0 <8.0.0. The default and most common.
  • ~7.8 — tilde. Allows last-segment changes: ~7.8 = >=7.8 <8.0; ~7.8.2 = >=7.8.2 <7.9.
  • 7.8.* — wildcard. >=7.8.0 <7.9.0.
  • >=7.8 <8.0 — explicit range. Useful for compound constraints.
  • dev-main — track a Git branch directly. Append #hash to pin a commit.
  • * — any version. Reserved for platform packages like ext-json.

Platform packages — names starting with php, hhvm,ext-*, or lib-*are virtual. Composer doesn't install them; it checks them against the running PHP. Always declare the PHP version and any required extensions explicitly.

autoload: PSR-4, PSR-0, classmap, files

autoloadtells Composer how to find your package's classes at runtime. After any change to autoload rules, run composer dump-autoload to regenerate vendor/autoload.php. There are four supported mechanisms, listed in order of preference.

MechanismUse whenPerformance
psr-4New code. Namespace maps 1:1 to directory.Fast — direct file lookup
psr-0Legacy code from before 2014. Deprecated.Slightly slower; underscores in class name become slashes
classmapNon-namespaced code, generated stubs, or pre-existing libraries that don't follow any PSRFastest at runtime, but scans directories at dump-autoload time
filesProcedural helpers loaded on every request (global functions, polyfills)Always included — use sparingly
{
  "autoload": {
    "psr-4": {
      "App\\": "src/",
      "App\\Domain\\": "domain/"
    },
    "classmap": ["database/seeders", "database/factories"],
    "files": ["src/helpers.php"]
  },
  "autoload-dev": {
    "psr-4": {
      "Tests\\": "tests/"
    }
  }
}

PSR-4 example — with {"App\\": "src/"} the class App\Service\UserService resolves to src/Service/UserService.php. The namespace prefix is stripped, the remainder converted from \ to /, and .php appended.

classmap example — Composer scans the listed directories at dump-autoload time and writes a giant $classmap array mapping every class name to its file path. Used by Laravel for its non-namespaced database/seeders directory.

files example — every file listed runs on require 'vendor/autoload.php'. Symfony Polyfill packages use this to register polyfill functions before any class loads. Don't put class definitions here.

autoload-devis the dev-only mirror — PHPUnit test namespaces typically go here so they aren't loaded in production. The structure mirrors autoload exactly.

Always run composer dump-autoload --optimize (or -o) in production deploys. It converts PSR-4 rules to a classmap on disk — same fast runtime lookup, no filesystem stats. The default non-optimized autoloader does an is_file check for every class load.

scripts: pre/post hooks and custom commands

scriptshooks into Composer's lifecycle and defines custom commands callable with composer <name>. Unlike npm, Composer scripts run inside the project root with vendor/bin automatically on PATH, so you can call any installed binary by name.

{
  "scripts": {
    "test": "phpunit",
    "test:coverage": "phpunit --coverage-html=coverage",
    "lint": "php-cs-fixer fix --dry-run --diff",
    "format": "php-cs-fixer fix",
    "stan": "phpstan analyse src tests --level=8",

    "post-install-cmd": [
      "@php artisan package:discover --ansi"
    ],
    "post-update-cmd": [
      "@php artisan vendor:publish --tag=public --force"
    ],
    "post-autoload-dump": [
      "@php artisan package:discover --ansi"
    ],

    "ci": [
      "@stan",
      "@lint",
      "@test"
    ]
  },
  "scripts-descriptions": {
    "ci": "Run the full CI suite locally"
  }
}

Lifecycle event hooks — names Composer recognizes and fires automatically:

  • pre-install-cmd / post-install-cmd — wrap composer install
  • pre-update-cmd / post-update-cmd — wrap composer update
  • pre-autoload-dump / post-autoload-dump — wrap autoloader generation
  • pre-package-install / post-package-install — fired per-package
  • pre-package-update / post-package-update — fired per-package
  • pre-package-uninstall / post-package-uninstall — fired per-package

Script prefixes@php uses the PHP binary Composer is running under. @composer calls Composer itself recursively. @putenv KEY=VALUE sets an environment variable for the rest of the script. @name calls another script defined in the same file (see @stan, @lint, @test inside ci above).

PHP callbacks — scripts can also point at static methods: "post-install-cmd": "Vendor\\Class::method". The class must be autoloadable and the method receives a Composer\Script\Event object. Used by framework integrations to run code without a separate CLI binary.

repositories: VCS, path, and private Packagist alternatives

By default Composer resolves every package against packagist.org. repositories overrides that — add Git URLs, local paths, or alternative registries. Composer searches repositories in order and stops at the first match.

{
  "repositories": [
    {
      "type": "vcs",
      "url": "https://github.com/myorg/private-pkg"
    },
    {
      "type": "path",
      "url": "../shared-lib",
      "options": {
        "symlink": true
      }
    },
    {
      "type": "composer",
      "url": "https://repo.packagist.com/myorg/"
    },
    {
      "packagist.org": false
    }
  ],
  "require": {
    "myorg/private-pkg": "^1.0",
    "myorg/shared-lib": "@dev"
  }
}

Repository types:

  • vcs — any Git/SVN/Mercurial URL. Composer reads the remotecomposer.json, extracts the package name, and uses Git tags as versions. Supports GitHub, GitLab, Bitbucket, and self-hosted Git.
  • path — a local directory. The default options.symlink: true creates a symlink to your source, so edits are live without re-running composer install. Use during library development before publishing.
  • composer — a self-hosted Composer registry exposing a packages.json file. Used by Private Packagist, Repman, and JFrog Artifactory.
  • {"packagist.org": false} — disable the default registry. Use when everything must come from your private registry (offline environments, regulated deployments).

Authentication — store tokens in auth.json (gitignored) or via composer config --global --auth github-oauth.github.com TOKEN. Never put credentials in composer.json directly. The COMPOSER_AUTH environment variable accepts the same JSON for CI use.

platform, config, and platform-check

configis the top-level options object for Composer's behavior on this project. It overrides ~/.composer/config.json for project-local settings.

{
  "config": {
    "platform": {
      "php": "8.2.10",
      "ext-iconv": "1.0"
    },
    "platform-check": true,
    "optimize-autoloader": true,
    "preferred-install": "dist",
    "sort-packages": true,
    "allow-plugins": {
      "dealerdirect/phpcodesniffer-composer-installer": true,
      "phpstan/extension-installer": true,
      "composer/installers": true
    },
    "vendor-dir": "vendor",
    "bin-dir": "vendor/bin"
  },
  "minimum-stability": "stable",
  "prefer-stable": true
}
  • config.platform — fake the runtime PHP version and extensions. Lets you resolve dependencies for the production PHP version from a dev machine running a different version. Set php to your production runtime.
  • config.platform-check — default true in Composer 2.x. Generates a vendor/composer/platform_check.phpthat aborts with a clear error if the deploy target's PHP version is too old. Set false only for unusual deployment shapes.
  • config.allow-plugins — added in Composer 2.2 (Dec 2021). Composer now refuses to run third-party plugins unless explicitly allowed here. List each plugin package and set true; setting "allow-plugins": false disables all plugins.
  • config.optimize-autoloader — equivalent to passing -o on every install. Generates a classmap autoloader for speed.
  • config.preferred-installdist downloads release zips (faster, default), source clones Git repos (lets you commit back), auto picks based on package stability.
  • config.sort-packages — keeps require entries alphabetically sorted on composer require. Reduces merge conflicts.
  • minimum-stabilitystable (default), RC, beta, alpha, dev. Sets the floor for what Composer will install. Pair with prefer-stable: true to prefer stable versions when both stable and pre-release satisfy a constraint.

extra — a free-form object that frameworks and plugins read. Laravel reads extra.laravel.providers for package auto-discovery; the composer/installers plugin reads extra.installer-paths to install Drupal modules into web/modules/contrib. Composer itself ignores everything under extra.

Common composer.json errors and Composer 2.x changes

The five errors below cover the bulk of real-world failures, plus the two breaking changes from Composer 1.x to 2.x that still trip up older project upgrades.

ErrorCauseFix
Parse error on line NTrailing comma, unquoted key, or single quotesValidate with Jsonic's JSON Validator or composer validate
Your requirements could not be resolved to an installable setConflicting version constraints across packagesRun composer why-not vendor/pkg ^X.Y to see which package blocks the upgrade
The lock file is not up to datecomposer.json changed without re-running installRun composer update --lock (refreshes hash without changing versions)
Class App\Foo not foundPSR-4 mapping missing or stale autoloaderVerify namespace matches directory, then composer dump-autoload
composer/installers contains a Composer plugin which is blockedComposer 2.2+ blocks plugins unless allowlistedAdd to config.allow-plugins (see Section 6)
Cannot use ::name as identifier (Composer 2.x)Composer 1 allowed lazy partial-update semantics 2.x droppedList explicit package names instead of relying on root-only updates
Slow installs on Composer 1.xComposer 1.x downloads packages seriallyUpgrade to 2.x — parallel downloads, 2–3× faster, drop-in compatible for most projects

Composer 2.x changes worth knowing — released October 2020, current stable line. Parallel package downloads via curl_multi. New solver that produces clearer error messages. Stricter platform checks (platform-check on by default). Plugin allowlist enforced from 2.2 onward. Composer 1.x still receives security patches but is feature-frozen — every new project should be on 2.x.

Key terms

Composer
The de-facto PHP dependency manager, created in 2012 by Nils Adermann and Jordi Boggiano. Reads composer.json, resolves a dependency graph, and downloads packages to vendor/. Current major version is 2.x.
Packagist
The default public Composer registry at packagist.org. Indexes Git tags from registered repositories and serves a metadata JSON to Composer clients. No publish step — just submit a repo URL.
PSR-4
The modern PHP autoloading standard (PHP-FIG specification). Maps namespace prefixes to base directories with a one-to-one relationship between class names and file paths. Supersedes PSR-0.
autoload
PHP's mechanism for loading classes on demand without explicit require statements. Composer generates vendor/autoload.php from the rules in composer.json; including that file once registers handlers for every package.
Lockfile
composer.lock records the exact versions, source URLs, and content hashes resolved at the last install or update. Commit it for apps to guarantee reproducible installs; gitignore for libraries.
semver constraint
A version range expression (^7.8, ~3.5.2, >=10.0 <11) that constrains acceptable package versions. Composer follows the SemVer 2.0 spec with some PHP-specific extensions documented at getcomposer.org/doc/articles/versions.

Frequently asked questions

What is composer.json used for?

composer.json is the manifest file for PHP projects managed by Composer, the de-facto dependency manager for PHP since 2012. It declares which packages a project requires, how to autoload its own source code (typically via PSR-4), custom scripts to run on Composer lifecycle events, and metadata (name, description, license, authors) used when publishing the package to Packagist — the default public registry. Running composer install reads composer.json, resolves a dependency graph, downloads packages from Packagist or configured repositories into vendor/, generates an autoloader at vendor/autoload.php, and writes the exact resolved versions to composer.lock. For application projects you typically commit both files; for library packages you commit composer.json and gitignore composer.lock.

What is the difference between require and require-dev?

require lists packages that are needed for your application or library to run in production — they are installed every time, including on production servers when you run composer install --no-dev. require-dev lists packages only needed during development: testing frameworks like PHPUnit, static analyzers like PHPStan and Psalm, code style fixers, and tooling. When your package is installed as a dependency of another project, require-dev entries are completely ignored — only require is transitively installed. The typical production deploy command is composer install --no-dev --optimize-autoloader, which skips require-dev entirely and dumps an optimized classmap autoloader. Never put PHPUnit in require — it will leak into every consumer that depends on your library.

What does PSR-4 mean in autoload?

PSR-4 is the modern PHP autoloading standard published by the PHP-FIG (Framework Interop Group), superseding the older PSR-0. It maps a namespace prefix to a base directory: when PHP encounters App\Service\UserService, the autoloader strips the prefix App\, replaces backslashes with forward slashes, appends .php, and loads the file at the configured base directory. So {"App\\": "src/"} resolves App\Service\UserService to src/Service/UserService.php. PSR-4 requires one class per file, the file name to match the class name exactly (case-sensitive), and a one-to-one namespace-to-directory mapping. After editing autoload rules, run composer dump-autoload to regenerate vendor/autoload.php.

Should I commit composer.lock to git?

Commit composer.lock for application projects; gitignore it for reusable library packages. composer.lock records the exact versions and hashes resolved at the last composer install or composer update, so committing it gives every developer and your production server an identical dependency tree — that is what you want for an application. For libraries published to Packagist, the lockfile is irrelevant to consumers (they resolve their own versions against your require constraints), and committing it just creates noise on every dependency bump. The Composer docs recommend gitignoring it for libraries. CI behaviour: composer install reads composer.lock if present and errors if it is missing or out of sync; composer update ignores the lock and recomputes from composer.json.

How do I install a private package from GitHub?

Add a vcs repository entry to composer.json pointing at the Git URL, then require the package as normal. Example: {"repositories": [{"type": "vcs", "url": "https://github.com/myorg/private-pkg"}], "require": {"myorg/private-pkg": "^1.0"}}. Composer will read the target repo's composer.json to discover its name and versions (it uses git tags as versions). For authentication, set a GitHub personal access token with composer config --global --auth github-oauth.github.com YOUR_TOKEN, or use SSH URLs (git@github.com:...) if you have SSH keys configured. For larger teams, paid alternatives are Private Packagist (privatepackagist.com) and Repman — both expose a self-hosted registry that behaves like packagist.org. Use a single private repository entry per package rather than a path repository unless you are actively developing locally.

What is the difference between composer install and composer update?

composer install reads composer.lock and installs the exact versions recorded there; if composer.lock is missing it falls back to resolving from composer.json and writes a new lockfile. Use it on CI, in production deploys, and when cloning a project — it is fast and reproducible. composer update ignores composer.lock entirely, re-resolves all dependencies against the constraints in composer.json, downloads the newest versions matching those constraints, and rewrites composer.lock. Use it when you intentionally want to upgrade. Run composer update vendor/package to bump just one package. Running composer update on every deploy is a common mistake — it silently introduces patch and minor upgrades you have not tested.

Can composer.json have comments?

No — composer.json is strict JSON (RFC 8259), so // and /* */ comments will cause Composer to fail with a JSON parse error. There is no officially supported comment key the way npm reserves "//" in package.json. The conventional workaround is to put a string into description, or to add a top-level key like "_comment" that Composer ignores silently (only documented schema keys are validated against; unknown keys are tolerated by default, though composer validate --strict will warn about them). For richer commentable configuration in the same repository, keep human notes in README.md, the changelog, or a separate .composer-notes file. If you need to disable validation of an extra field, see config.allow-plugins for the Composer 2.2+ schema.

How do I lock the PHP version my project supports?

Declare PHP as a platform requirement inside require: {"require": {"php": "^8.2"}}. Composer treats php (and php-64bit, php-ipv6, plus extensions like ext-mbstring) as virtual packages — they are not installed by Composer but are matched against the runtime running Composer itself. For reproducible installs across machines that run different PHP versions, also set config.platform: {"config": {"platform": {"php": "8.2.10"}}}. This overrides the detected runtime so Composer resolves dependencies as if PHP 8.2.10 were installed, regardless of the actual local version — useful for matching production exactly. Enable composer check-platform-reqs in CI (or set config.platform-check: true, the default in Composer 2.x) to verify the deploy target meets the constraints.

Further reading and primary sources