CCelestiallines.com
← Back to blog

Composer Autoloading Explained: PSR-4 vs Classmap

By Pawan Singh · Jun 2026

What Autoloading Actually Solves

Before autoloading, every PHP file needed a manual require for every class it used — painful to maintain as a codebase grows, and easy to get wrong (missing a require, or requiring the same file twice and triggering a fatal redeclaration error). Composer's autoloader instead registers a function that PHP calls automatically the first time an unknown class is referenced, which then figures out the right file to include on demand.

PSR-4: Namespace Maps to Directory

PSR-4 is the modern standard: a namespace prefix maps to a base directory, and the rest of the namespace mirrors the folder structure. Configure it in composer.json:

{
    "autoload": {
        "psr-4": {
            "App\\": "app/"
        }
    }
}

With this mapping, the class App\Services\Mailer must live at app/Services/Mailer.php. No registration is needed per class — the rule is purely structural, which is why PSR-4 scales effortlessly to large codebases and needs zero regeneration when you add a new class in a properly mapped directory.

Classmap: Scan and Index Every Class

Classmap autoloading works differently — instead of a naming convention, Composer scans the specified directories/files at composer dump-autoload time and builds a direct map of every class name to its exact file path:

{
    "autoload": {
        "classmap": ["database/seeders", "legacy/"]
    }
}

This is the right tool for legacy code that doesn't follow PSR-4 naming conventions, or files with multiple classes per file (which PSR-4 doesn't support cleanly, since PSR-4 assumes one class per file at a predictable path). The tradeoff: the map has to be regenerated every time you add a class, since there's no structural rule Composer can rely on at runtime the way it can with PSR-4.

The "files" Autoload Type: Plain Function Libraries

PSR-4 and classmap both exist to resolve classes on demand. The files autoload type is different — it's for code with no classes at all, like a global helpers file of plain functions:

{
    "autoload": {
        "files": ["src/helpers.php"]
    }
}

Every file listed under files is required unconditionally on every single request, as soon as the autoloader boots — there's no "on demand" behavior like PSR-4/classmap, since there's no class name to trigger lazy loading against. Keep this list small; each entry adds a small fixed cost to every request regardless of whether the page actually calls any of its functions.

dev vs Production Autoloading

composer.json also supports an autoload-dev block, for classes only needed during testing (test cases, fixtures, factories):

{
    "autoload-dev": {
        "psr-4": { "Tests\\": "tests/" }
    }
}

Running composer install --no-dev in production excludes both the dev dependencies themselves and this autoload mapping — test-only classes never ship, and never need to be resolvable, on a production server.

Optimizing the Autoloader for Production

By default, PSR-4 autoloading does a small amount of runtime work per class lookup (constructing the expected path from the namespace). Two flags trade that flexibility for raw speed:

composer dump-autoload -o
composer install --optimize-autoloader --no-dev

-o (or --optimize-autoloader) converts PSR-4 mappings into a classmap for anything it can resolve statically, avoiding the runtime path computation. --classmap-authoritative goes a step further — it tells the autoloader "if a class isn't in this generated map, don't even bother checking the filesystem, just fail" — only safe once you're confident every class really is covered by the generated map, since it removes the fallback that would otherwise catch a class added but not yet mapped.

Why "Class Not Found" Happens After Adding a New Class

Composer's autoload files (vendor/autoload.php and friends) are generated, not dynamic — they're a cached snapshot. Adding a new PSR-4-compliant class doesn't strictly require regeneration (PSR-4 resolves by convention at runtime, in its non-optimized form), but classmap entries and the optimized/production autoloader both bake in a static list that goes stale the moment you add a class outside what was already scanned. After adding new classes, especially in a classmap-mapped directory or right before deploying with an optimized autoloader:

composer dump-autoload

Which Should You Use?

  • PSR-4 — the default for all new code. Laravel, Symfony, and virtually every modern package use it exclusively.
  • Classmap — only for legacy code you can't or don't want to restructure, or directories with non-class files mixed in that don't fit a namespace convention (like raw seed/migration scripts).
  • files — for plain function libraries with no classes at all, loaded unconditionally on every request; keep this list minimal.

Untangling a messy or legacy PHP autoloading setup? Get in touch — I work as a freelance PHP developer.

Comments