Composer and Packages
Using other people's code safely: Composer, composer.json and composer.lock, installing and updating packages, autoloading with PSR-4, and checking dependencies for known vulnerabilities.
What Composer does
Almost every PHP application uses code written by others: a logging
library, an HTTP client, a PDF generator, a whole framework. Copying
those files into the project by hand works until you need an update,
or until two libraries need different versions of a third.
Composer is PHP's
dependency
manager: you declare which
packages
a project needs, and Composer finds versions that satisfy all
requirements together, including the packages those packages need,
downloads them into the project's vendor/ folder and
generates an autoloader for them.
Composer works per project, not per machine: two applications on one server can use different versions of the same library without conflict. That is the main difference from a system package manager such as apt, which installs one version for the whole system. As of October 2026 the current release is Composer 2.10, which itself needs PHP 7.2 or later; the 2.2 LTS line exists for very old PHP versions.
Packagist
Packagist (packagist.org) is the
public registry Composer searches by default. It holds the metadata
of each package (names, versions, requirements) and points to the
source code, usually a Git repository on GitHub or GitLab. As of
October 2026 it lists about 470,000 packages. Package names have two
parts, vendor and project: monolog/monolog,
guzzlehttp/guzzle, symfony/console. Anyone
can publish a package, so a name on Packagist says nothing about
quality or trustworthiness. Companies host internal packages in a
private registry (such as Private Packagist or a GitLab package
registry) and list it under repositories in
composer.json.
Installing Composer
Composer is a single PHP program, composer.phar. The
download page on getcomposer.org gives an installer script with a
checksum to verify; most distributions also package it (on Debian
and Ubuntu: the composer package, often an older
version). Check that it works with:
composer --version
composer diagnose
Do not run Composer as root. Packages can contain scripts and plugins that run during installation, and Composer itself warns when it runs with root rights.
composer.json and version constraints
composer.json is
the file in the project root that describes the project for
Composer: its name, the packages it needs, the PHP version and
extensions it requires, and how its own classes are autoloaded. You
can write it by hand or let composer init and
composer require create and update it.
{
"name": "kestrel/meter-portal",
"description": "Customer portal for submitting meter readings",
"type": "project",
"require": {
"php": ">=8.4",
"ext-pdo": "*",
"guzzlehttp/guzzle": "^7.0",
"monolog/monolog": "^3.0"
},
"require-dev": {
"phpunit/phpunit": "^13.0"
},
"autoload": {
"psr-4": { "Kestrel\\": "src/" }
},
"autoload-dev": {
"psr-4": { "Kestrel\\Tests\\": "tests/" }
},
"config": {
"sort-packages": true
}
}
-
requirelists what the application needs to run. The entriesphpandext-pdoare platform requirements: Composer refuses to install when the PHP version or an extension is missing. -
require-devlists tools only needed while developing and testing, such as PHPUnit or PHPStan (see modern PHP). They are left out of production installs. -
autoloadtells Composer where the project's own classes live (see autoloading).
Version constraints
Packages on Packagist use
semantic versioning: a new major version may break your code, minor and patch versions
should not. Constraints in composer.json
describe which versions you accept:
| Constraint | Accepts | Typical use |
|---|---|---|
^3.4 |
>=3.4.0 and <4.0.0 | the normal choice: all compatible updates |
^0.3.1 |
>=0.3.1 and <0.4.0 | below 1.0, each minor version may break things |
~3.4.2 |
>=3.4.2 and <3.5.0 | patch updates only |
3.4.2 |
exactly 3.4.2 | rarely; blocks security fixes |
>=8.4 |
8.4 or anything newer | the PHP version |
* |
any version | extensions; avoid for packages |
A range in composer.json is a promise about what
could work. Which version is actually installed is decided
once, by composer update, and written down in the lock
file.
Installing and updating packages
The everyday commands, run in the project root:
# Add a package: updates composer.json and composer.lock, installs it
composer require monolog/monolog
composer require --dev phpunit/phpunit
# Install exactly what composer.lock says (after a clone, on a server, in CI)
composer install
# Look for newer versions within the constraints, then update one package
composer outdated --direct
composer update monolog/monolog
# What is installed, and why is this package here?
composer show
composer why psr/log
# Remove a package and everything only it needed
composer remove guzzlehttp/guzzle
Using version ^3.x for monolog/monolog ./composer.json has been updated Running composer update monolog/monolog Loading composer repositories with package information Updating dependencies Lock file operations: 2 installs, 0 updates, 0 removals - Locking monolog/monolog (3.x.y) - Locking psr/log (3.x.y) Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 2 installs, 0 updates, 0 removals - Installing psr/log (3.x.y): Extracting archive - Installing monolog/monolog (3.x.y): Extracting archive Generating autoload files
Adding Monolog also installed psr/log, because Monolog
requires it. Such indirect dependencies are usually the majority of
vendor/: every one is code that runs in your
application.
composer.lock: install versus update
composer.lock is
the
lock file
Composer writes after resolving the dependencies: the exact version
of every installed package, direct and indirect, with its download
location and the exact commit (reference) it was built from. The two
main commands treat it differently:
-
composer installreads the lock file and installs exactly those versions. When there is no lock file yet, it behaves likeupdateand creates one. -
composer updateignores the old lock file, picks the newest versions that fit the constraints and writes a new lock file. Run it on purpose, on a development machine, then test and commit the newcomposer.lock.
For an application, commit composer.lock: then your
laptop, the CI pipeline and production all run the versions you
tested, and an update is a reviewable change in Git. Put
vendor/ in .gitignore; it can always be
rebuilt from the lock file. A library is installed into other
projects, which resolve their own versions; its lock file only
affects its own tests.
Because the lock file is plain JSON, a script can read it, for
example to report the installed versions on a server. In practice
composer show --locked gives the same list:
<?php
// A shortened composer.lock with example versions (the real file also has
// download URLs, source commits, licenses and more for each package)
file_put_contents('composer.lock', json_encode([
'packages' => [
['name' => 'guzzlehttp/guzzle', 'version' => '7.10.0'],
['name' => 'monolog/monolog', 'version' => '3.9.0'],
['name' => 'psr/log', 'version' => '3.0.2'],
],
'packages-dev' => [
['name' => 'phpunit/phpunit', 'version' => '13.0.5'],
],
]));
$lock = json_decode(file_get_contents('composer.lock'), true, flags: JSON_THROW_ON_ERROR);
foreach (['packages' => 'production', 'packages-dev' => 'development'] as $key => $label) {
foreach ($lock[$key] ?? [] as $package) {
printf("%-20s %-8s %s\n", $package['name'], $package['version'], $label);
}
}
guzzlehttp/guzzle 7.10.0 production monolog/monolog 3.9.0 production psr/log 3.0.2 production phpunit/phpunit 13.0.5 development
Autoloading with PSR-4
PHP loads a class file only when told to, with
require. Instead of hundreds of
require lines, PHP lets a program register an
autoloader with spl_autoload_register(): a
function PHP calls the first time an unknown class is used, which
finds and loads the right file. Composer generates such an
autoloader for every package in vendor/ and for your
own code. An application includes it once, at the top of its entry
point:
<?php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use Kestrel\Billing\Invoice;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
$log = new Logger('portal');
$log->pushHandler(new StreamHandler('php://stderr'));
$invoice = new Invoice(customerId: 7); // loaded from src/Billing/Invoice.php
PSR-4 autoloading is the
convention that makes this predictable. PSR-4 is one of the PHP-FIG
standards (see
PHP-FIG and PSRs): a
namespace prefix
is mapped to a base folder, and the rest of the class name becomes
the path below it, one folder per namespace level, with
.php added. With "Kestrel\\": "src/" (the
backslash is doubled because this is JSON), the class
Kestrel\Billing\Invoice is loaded from
src/Billing/Invoice.php.
The autoloader Composer generates is more refined, but the idea fits in a few lines. This example creates one class file the PSR-4 way and registers its own autoloader for the same mapping:
<?php
// A tiny project with two classes in src/, laid out the PSR-4 way
mkdir('src/Billing', 0777, true);
file_put_contents('src/Billing/Invoice.php', <<<'PHP'
<?php
namespace Kestrel\Billing;
class Invoice
{
public function __construct(public int $customerId) {}
}
PHP);
// What Composer's autoloader does for the mapping "Kestrel\\": "src/"
spl_autoload_register(function (string $class): void {
$prefix = 'Kestrel\\';
if (!str_starts_with($class, $prefix)) {
return; // not ours: let other autoloaders try
}
$path = 'src/' . str_replace('\\', '/', substr($class, strlen($prefix))) . '.php';
echo "Looking up $class in $path\n";
if (is_file(__DIR__ . '/' . $path)) {
require __DIR__ . '/' . $path;
}
});
$first = new Kestrel\Billing\Invoice(7); // triggers the autoloader
$second = new Kestrel\Billing\Invoice(8); // already loaded: no lookup
echo get_class($second), " for customer {$second->customerId}\n";
var_dump(class_exists('Kestrel\Reports\Monthly'));
Looking up Kestrel\Billing\Invoice in src/Billing/Invoice.php Kestrel\Billing\Invoice for customer 8 Looking up Kestrel\Reports\Monthly in src/Reports/Monthly.php bool(false)
The class name and the file name must match exactly, including upper
and lower case: src/billing/invoice.php may work on a
case-insensitive file system (Windows, macOS) and fail on the Linux
server. After changing the autoload section of
composer.json, run
composer dump-autoload to regenerate the autoloader.
Composer in deployment
A production install leaves out the development tools and builds a faster autoloader:
composer install --no-dev --optimize-autoloader --no-interaction
-
--no-devskipsrequire-dev: test tools do not belong on a public server, where they only add code that can be attacked. -
--optimize-autoloaderturns the PSR-4 rules into a class map (one lookup table of class to file), which saves file system checks on every request. -
--no-interactionmakes Composer fail instead of waiting for a question nobody will answer in a pipeline.
Usually this runs in the build step of a
CI/CD pipeline, and the
result, code plus vendor/, is deployed as one release
(see
deploying a release). Keep vendor/, composer.json and
composer.lock outside the web root, so they cannot be
requested in a browser: only the public/ folder with
index.php should be reachable.
Checking dependencies for vulnerabilities
Every package is code that runs with all the rights of your application: it can read the database password, the session data and the files. Two kinds of risk follow. A package can have a known security bug that is fixed in a newer version, or a package can be malicious itself: a hijacked maintainer account, or a look-alike name, which is a supply chain attack. The OWASP Top 10 of 2025 lists software supply chain failures as A03.
composer audit
composer audit
checks the installed packages, or with --locked those
in composer.lock, against the security advisories that
Packagist collects, and also flags packages marked as malware or
abandoned (by default an abandoned package fails the audit too). It
exits with a non-zero code when it finds a problem, so a CI job
fails on it:
composer audit --locked
composer audit --locked --no-dev --format=json # for scripts and reports
Found 1 security vulnerability advisory affecting 1 package: +-------------------+----------------------------------------------+ | Package | example/pdf-renderer | | Severity | high | | Advisory ID | PKSA-xxxx-xxxx-xxxx | | CVE | CVE-2026-xxxxx | | Title | Path traversal in font loading | | Affected versions | >=2.0.0,<2.4.3 | +-------------------+----------------------------------------------+
The usual fix is composer update example/pdf-renderer
to a version outside the affected range, then the tests and a new
lock file. Run the audit in the pipeline on every change
and on a schedule: new advisories appear for code you have
not touched. It fits into the wider
vulnerability management
process.
Recent Composer versions also act before installation. As of 2026:
since Composer 2.9, update, require and
remove refuse package versions with a known security
advisory, and since 2.10 packages flagged as malware are blocked
even during install; abandoned packages are reported
but not blocked unless you configure it. The rules live under
policy in the config section of
composer.json, and --no-blocking switches
them off for one command.
Choosing and maintaining packages
- Check before you add. Is it maintained (recent releases, answered issues), widely used, and is the name exactly right? Fewer dependencies mean fewer things to patch.
-
Allow plugins on purpose. Composer plugins and
scripts run code during installation. Composer asks before it
enables a plugin and records the answer under
allow-plugins; review that list like code. -
Update regularly, in small steps. A monthly
composer outdatedand update is easier to test than two years of changes at once. Tools such as Dependabot or Renovate open the update as a pull request. - Mind abandoned packages. A package whose maintainer stopped gets no security fixes; Packagist marks it abandoned and often names a replacement.
-
Check the lock file into review. A pull request
that changes
composer.lockchanges code that runs in production, even if no file of your own changed.
composer validate checks that
composer.json is valid and that the lock file is up to
date with it, a useful first step in a pipeline.