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/package pair.
  • 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 to App\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 of App\Math\Calculator. It finds App\ mapped to the src/ directory.
  • It strips the matched prefix, leaving Math\Calculator, converts the remaining backslashes to directory separators, and appends .php, producing src/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.lock for 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 .gitignore and run composer install as 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 -o for production builds — it skips require-dev packages and generates the optimized, flattened autoloader.
  • Never edit files inside vendor/ directly; your changes vanish the next time someone runs composer install or composer 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, not require, so they aren’t installed in production.
  • Run composer dump-autoload after 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 a src/ directory using PSR-4, then write a class App\Greeter with a greet(string $name): string method that returns "Hello, {$name}!". What file path must that class live at for the autoloader to find it?
  • Add a files autoload entry pointing at a new src/constants.php file that defines a constant APP_VERSION. Explain in one sentence why this couldn’t be done with a psr-4 entry instead.
  • You run composer require monolog/monolog on your machine and commit composer.json and composer.lock. A teammate clones the repo and runs composer update instead of composer 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 into vendor/, and records exact versions in composer.lock.
  • Requiring vendor/autoload.php once registers an autoloader, built on spl_autoload_register(), that loads classes on demand — no manual require per class.
  • psr-4 maps a namespace prefix to a directory and computes file paths mechanically; the namespace after the prefix must match the directory structure exactly.
  • classmap supports libraries that don’t follow PSR-4; files unconditionally loads plain functions or constants that have no class name to trigger on.
  • Use composer install for reproducible installs from composer.lock, and composer update only when you intend to move to newer allowed versions.
  • Never edit code inside vendor/, and keep development-only tools under require-dev.