PHP Composer Basics
Composer is the standard dependency manager for PHP. Instead of manually downloading libraries and writing a long chain of require_once statements, you declare what your project needs in a single composer.json file, and Composer downloads those packages — along with their own dependencies — into a vendor/ folder and generates an autoloader that makes every class available the moment you write a use statement. Almost every modern PHP framework and library, from Laravel and Symfony to PHPUnit and Guzzle, is installed through Composer, so understanding it is essential to working with real-world PHP code.
What Composer Does and How It Works
Before Composer, PHP developers either copied library source files into their project by hand or wrote fragile require_once chains, hoping no two libraries needed conflicting versions of the same dependency. Composer solves this with two jobs it performs together: dependency resolution (figuring out which exact versions of which packages satisfy every constraint in your project and its dependencies) and autoloading (generating the PHP code that loads the right class file automatically, with no manual require statements for your own code once it is registered).
Composer works with a small set of files and directories, each with a distinct job:
- composer.json — the manifest you write by hand. It lists your dependencies as loose version constraints (for example
^8.3) plus autoloading rules. - composer.lock — generated automatically. It records the exact resolved version of every package (including nested dependencies) so that everyone on the team, and your production server, install identical code.
- vendor/ — where the actual downloaded package source code lives, one folder per
vendor/packagepair. - vendor/autoload.php — a generated bootstrap file. Requiring this one file wires up autoloading for every installed package and for your own namespaced classes.
When you run composer require, Composer talks to Packagist (packagist.org), the default public package repository, resolves a dependency graph that satisfies every version constraint, downloads the matching code into vendor/, writes the resolved versions into composer.lock, and regenerates the autoloader. Version constraints commonly use ^ (compatible releases, e.g. ^8.3 allows 8.3.0 up to but not including 9.0.0), ~ (allows the last listed segment to increase), an exact version string, or * for any version — in practice, caret constraints are the default recommendation because they follow semantic versioning while still permitting bug-fix and feature updates.
Syntax
The table below covers the Composer commands you will use in day-to-day development:
| Command | What it does |
|---|---|
composer init |
Interactively creates a new composer.json in the current directory |
composer require vendor/package |
Adds a package as a dependency, downloads it, and updates composer.json and composer.lock |
composer require --dev vendor/package |
Adds a package only needed for development, such as a testing library |
composer install |
Installs the exact versions recorded in composer.lock — use this on a fresh clone or deploy |
composer update |
Re-resolves the newest versions allowed by composer.json and rewrites composer.lock |
composer remove vendor/package |
Removes a package and updates composer.json and composer.lock |
composer dump-autoload |
Regenerates the autoloader files without changing any installed package |
composer show |
Lists installed packages and their currently installed versions |
A minimal composer.json looks like this:
{
"name": "acme/basics-demo",
"require": {
"php": "^8.3"
},
"require-dev": {
"phpunit/phpunit": "^11.0"
},
"autoload": {
"psr-4": {
"App\\\\": "src/"
}
}
}
The key fields are:
| Field | Purpose |
|---|---|
name |
The vendor/package identifier, required if you publish the package |
require |
Production dependencies and their version constraints |
require-dev |
Dependencies only needed for development and testing |
autoload |
Maps namespaces or files that the generated autoloader should know about |
scripts |
Custom commands Composer runs at defined lifecycle hooks, such as post-install-cmd |
minimum-stability |
The lowest stability flag (dev, alpha, beta, RC, stable) Composer will accept |
The "App\\": "src/" line under psr-4 is a PSR-4 mapping: any class in the App namespace is expected to live under the src/ directory, following the namespace as a path. That mapping is what the examples below rely on.
Examples
Example 1: PSR-4 autoloading a class
With the composer.json above in place, running composer install generates vendor/autoload.php. Save a class under src/Math/Calculator.php:
<?php
namespace App\Math;
class Calculator
{
public function add(int|float ...$numbers): int|float
{
return array_sum($numbers);
}
public function multiply(int|float ...$numbers): int|float
{
return array_reduce($numbers, fn($carry, $n) => $carry * $n, 1);
}
}
Then, from a script anywhere in the project, require the generated autoloader and use the class with no manual require for Calculator.php itself:
<?php
require __DIR__ . '/../vendor/autoload.php';
use App\Math\Calculator;
$calc = new Calculator();
echo $calc->add(4, 8, 15) . PHP_EOL;
echo $calc->multiply(2, 3, 4) . PHP_EOL;
Output:
27
24
Because App\Math\Calculator starts with the App\ prefix mapped to src/, Composer’s autoloader translates the rest of the namespace, Math\Calculator, into the path src/Math/Calculator.php, loads it automatically, and the class is ready to use — no manual require line for that file was ever written.
Example 2: Autoloading plain functions with “files”
PSR-4 only autoloads classes, interfaces, traits, and enums — not standalone functions. For a small file of helper functions you want available everywhere, add a files entry to autoload in composer.json:
"autoload": {
"psr-4": { "App\\\\": "src/" },
"files": ["src/helpers.php"]
}
src/helpers.php:
<?php
function formatCurrency(float $amount, string $currency = 'USD'): string
{
return $currency . ' ' . number_format($amount, 2);
}
function slugify(string $text): string
{
return strtolower(trim(preg_replace('/[^A-Za-z0-9]+/', '-', $text), '-'));
}
Any file listed under files is included on every request once vendor/autoload.php runs, so the functions are simply available:
<?php
require __DIR__ . '/../vendor/autoload.php';
echo formatCurrency(1999.5) . PHP_EOL;
echo slugify('Composer Basics: PSR-4 & Files!') . PHP_EOL;
Output:
USD 1,999.50
composer-basics-psr-4-files
formatCurrency() and slugify() were never explicitly required in index.php — they exist because helpers.php was declared under files and gets pulled in automatically by the autoloader every time.
Example 3: Autoloading an interface and its implementation
PSR-4 autoloading also applies to interfaces, so a common pattern — coding against an interface and swapping implementations — works with zero manual requires. Define the interface at src/Contracts/Discount.php:
<?php
namespace App\Contracts;
interface Discount
{
public function apply(float $price): float;
}
And an implementation at src/Discounts/PercentageDiscount.php:
<?php
namespace App\Discounts;
use App\Contracts\Discount;
class PercentageDiscount implements Discount
{
public function __construct(private readonly float $percent) {}
public function apply(float $price): float
{
return round($price - ($price * $this->percent / 100), 2);
}
}
Using both from a script:
<?php
require __DIR__ . '/../vendor/autoload.php';
use App\Discounts\PercentageDiscount;
$discount = new PercentageDiscount(15);
echo 'Final price: $' . $discount->apply(80.00) . PHP_EOL;
Output:
Final price: $68
Both Discount.php and PercentageDiscount.php loaded automatically because their namespaces, App\Contracts and App\Discounts, both fall under the App\ prefix mapped to src/. Composer never needed a separate entry for each subdirectory.
How Autoloading Works Step by Step
Autoloading is not magic — it is built on a plain PHP feature called spl_autoload_register(), which lets you register a callback that PHP calls automatically whenever it encounters a class name it hasn’t loaded yet. When you require vendor/autoload.php, Composer registers its own autoloader callback using exactly that function. Here is what happens the first time your code references a class:
- PHP hits
new Calculator()(resolved toApp\Math\Calculator) and finds the class is not yet defined. - PHP calls every registered autoloader in turn, passing the fully-qualified class name, until one of them defines the class.
- Composer’s autoloader checks its generated PSR-4 map (stored in
vendor/composer/autoload_psr4.php) for a namespace prefix that matches the start ofApp\Math\Calculator. It findsApp\mapped to thesrc/directory. - It strips the matched prefix, leaving
Math\Calculator, converts the remaining backslashes to directory separators, and appends.php, producingsrc/Math/Calculator.php. - It requires that file. If the file exists and defines the expected class, PHP continues executing; if not, PHP raises a fatal “Class not found” error.
Besides psr-4, composer.json’s autoload section supports two other mechanisms worth knowing:
- classmap — Composer scans the listed directories once at install time, builds a flat map of every class name to its file path, and stores it directly (no runtime path calculation). This is how Composer supports older libraries that do not follow PSR-4 naming.
- files — as in Example 2, these files are unconditionally
required on every request, which is the only way to autoload plain functions or constants since there is no class name for PHP to trigger on.
Running composer dump-autoload regenerates vendor/composer/autoload_psr4.php, autoload_classmap.php, and the other generated files without touching any installed package — useful after you add a new namespace to autoload yourself. Adding the -o (optimize) flag, which Composer also applies automatically during composer install --no-dev -o in production deployments, pre-resolves every PSR-4 lookup into a single flat classmap ahead of time, trading a slightly larger generated file for the fastest possible class loading with no runtime path-matching logic at all.
Common Mistakes
Mistake 1: Forgetting to require the autoloader
Every entry-point script needs to load Composer’s autoloader before it can use any installed or autoloaded class. Skipping it is the single most common Composer error beginners hit:
<?php
use App\Math\Calculator;
$calc = new Calculator();
echo $calc->add(2, 2);
This fails with Fatal error: Uncaught Error: Class "App\Math\Calculator" not found, because nothing has registered Composer’s autoloader yet — PHP has no idea src/Math/Calculator.php even exists. The fix is to require vendor/autoload.php before referencing any autoloaded class:
<?php
require __DIR__ . '/vendor/autoload.php';
use App\Math\Calculator;
$calc = new Calculator();
echo $calc->add(2, 2);
In a real application this require typically happens exactly once, near the very top of a single front controller (for example public/index.php), and every other file relies on that one bootstrap having already run.
Mistake 2: A namespace that doesn’t match its file location
PSR-4 autoloading is purely mechanical — it converts a namespace directly into a file path, with no fuzzy matching. If the class’s declared namespace doesn’t match the directory it’s saved in relative to the mapped prefix, the autoloader will look in the wrong place and fail, even though the code itself is perfectly valid PHP:
<?php
// File saved at: src/Helpers/Mailer.php
// but composer.json maps App\ to src/, so this namespace
// requires the file to live at src/Services/Mailer.php
namespace App\Services;
class Mailer
{
public function send(string $to, string $body): void
{
echo "Sending to {$to}: {$body}" . PHP_EOL;
}
}
Referencing App\Services\Mailer makes Composer look for src/Services/Mailer.php — but the file actually sits at src/Helpers/Mailer.php, so it is never found. The fix is to move the file so its path matches its namespace, not to change the namespace to match the wrong folder:
<?php
// File saved at: src/Services/Mailer.php
namespace App\Services;
class Mailer
{
public function send(string $to, string $body): void
{
echo "Sending to {$to}: {$body}" . PHP_EOL;
}
}
When this kind of error happens after you’re sure the file is in the right place, run composer dump-autoload — Composer only rescans directories for the classmap and file-based rules when asked, though PSR-4 paths are computed on demand and don’t usually need this.
Best Practices
- Commit
composer.lockfor applications (websites, APIs) so every environment, including production, installs identical versions; for a reusable library you publish, it’s conventional to leave it out so consumers resolve versions against their own project. - Add
vendor/to.gitignoreand runcomposer installas part of your deploy or CI pipeline rather than committing installed packages. - Prefer caret constraints (
^8.3) over loose ones like*, so updates stay within a compatible major version instead of silently pulling in breaking changes. - Run
composer install --no-dev -ofor production builds — it skipsrequire-devpackages and generates the optimized, flattened autoloader. - Never edit files inside
vendor/directly; your changes vanish the next time someone runscomposer installorcomposer update. Fork the package or use a patching tool if you truly need to change third-party code. - Keep test-only and dev-only tools like PHPUnit under
require-dev, notrequire, so they aren’t installed in production. - Run
composer dump-autoloadafter adding a brand-new namespace mapping to composer.json, since existing installs won’t pick up autoload configuration changes on their own.
Practice Exercises
- Write a composer.json that maps the
App\namespace to asrc/directory using PSR-4, then write a classApp\Greeterwith agreet(string $name): stringmethod that returns"Hello, {$name}!". What file path must that class live at for the autoloader to find it? - Add a
filesautoload entry pointing at a newsrc/constants.phpfile that defines a constantAPP_VERSION. Explain in one sentence why this couldn’t be done with apsr-4entry instead. - You run
composer require monolog/monologon your machine and commit composer.json and composer.lock. A teammate clones the repo and runscomposer updateinstead ofcomposer install. What’s the practical risk of that choice, and which command should they have run?
Summary
- Composer is PHP’s dependency manager: it resolves version constraints from
composer.json, downloads packages from Packagist intovendor/, and records exact versions incomposer.lock. - Requiring
vendor/autoload.phponce registers an autoloader, built onspl_autoload_register(), that loads classes on demand — no manualrequireper class. psr-4maps a namespace prefix to a directory and computes file paths mechanically; the namespace after the prefix must match the directory structure exactly.classmapsupports libraries that don’t follow PSR-4;filesunconditionally loads plain functions or constants that have no class name to trigger on.- Use
composer installfor reproducible installs from composer.lock, andcomposer updateonly when you intend to move to newer allowed versions. - Never edit code inside
vendor/, and keep development-only tools underrequire-dev.
