Composer Autoloading Deep Dive: PSR-4, Classmap, and Files Strategies

Most PHP developers know just enough about Composer autoloading to make it work. But when class resolution fails in a Drupal module, a package publishes duplicate class definitions, or a legacy library throws "class not found" mid-request, you need to understand what the autoloader is actually doing. This article covers all four autoloading strategies, how they interact, and the performance trade-offs that matter in production.

How the Composer Autoloader Works

Running composer install or composer dump-autoload generates vendor/autoload.php. Including that file registers a chain of autoloaders with PHP's spl_autoload_register(). When PHP encounters an unknown class name, it calls each registered autoloader in order until one resolves the file or all fail.

Composer's generated autoloader tries strategies in this order:

  1. Classmap (fastest — direct hash lookup)
  2. PSR-4 prefix matching
  3. PSR-0 prefix matching (legacy)
  4. Files (already included at bootstrap — not triggered by class lookup)

PSR-4: The Standard Approach

PSR-4 maps a namespace prefix to a directory. The rest of the fully qualified class name maps to a relative file path under that directory.

{
  "autoload": {
    "psr-4": {
      "Acme\\Blog\\": "src/",
      "Acme\\Admin\\": "admin/src/"
    }
  }
}

With this configuration:

ClassFile resolved to
Acme\Blog\Postsrc/Post.php
Acme\Blog\Repository\PostRepositorysrc/Repository/PostRepository.php
Acme\Admin\Dashboardadmin/src/Dashboard.php

The prefix must end with a backslash. Multiple prefixes can map to the same directory. The autoloader strips the prefix, converts the remaining namespace separators to directory separators, and appends .php.

PSR-4 in Drupal Modules

Drupal enforces a specific PSR-4 layout for modules. The module's composer.json (or Drupal core's own autoloading logic via core/core.services.yml) maps:

{
  "autoload": {
    "psr-4": {
      "Drupal\\my_module\\": "web/modules/custom/my_module/src/"
    }
  }
}

Drupal's root composer.json handles this globally via the drupal/core-composer-scaffold plugin and Drush's class discovery, so individual module composer.json files are optional — but including them enables the module to be published as a standalone Packagist package.

Multiple Roots for One Prefix

PSR-4 allows an array of directories for a single prefix. Composer checks each directory in order:

{
  "autoload": {
    "psr-4": {
      "Acme\\Blog\\": ["src/", "lib/"]
    }
  }
}

This is useful when splitting a namespace across a src directory and generated code.

Classmap: Maximum Performance

Classmap tells Composer to scan directories and build a complete class→file map at dump time. Every class lookup becomes a simple array access — no filesystem checks at runtime.

{
  "autoload": {
    "classmap": ["src/", "lib/", "something.php"]
  }
}

Composer generates a file like:

// vendor/composer/autoload_classmap.php
return [
    'Acme\\Blog\\Post'       => $baseDir . '/src/Post.php',
    'Acme\\Blog\\Tag'        => $baseDir . '/src/Tag.php',
    'LegacyLib_Util'         => $baseDir . '/lib/LegacyLib/Util.php',
];

Classmap is the right choice for:

  • Legacy code that doesn't follow PSR-0 or PSR-4 naming conventions
  • PEAR-style libraries (underscore-separated class names)
  • Any library where you want guaranteed resolution without prefix matching

Optimising PSR-4 with Classmap in Production

The most important production optimisation is --optimize-autoloader (or -o), which converts all PSR-4 and PSR-0 lookups into a classmap at dump time:

# During deployment
composer install --no-dev --optimize-autoloader --prefer-dist

# Or just regenerate the autoloader
composer dump-autoload --optimize --no-dev

This eliminates filesystem stat calls for every unresolved class. On a Drupal site with hundreds of classes this can reduce bootstrap time by 20–40 ms.

Authoritative Classmap

Go one step further with --classmap-authoritative (-a):

composer dump-autoload --classmap-authoritative --no-dev

This tells the autoloader: "if a class is not in the classmap, it doesn't exist — don't fall back to PSR-4 lookups." PHP throws a fatal error immediately instead of searching the filesystem. Do not use this during development — any new class file added without a dump-autoload will cause an immediate fatal.

Files: Guaranteed Inclusion at Bootstrap

The files strategy is not triggered by class name — it runs unconditionally every time vendor/autoload.php is included. Use it for global functions, constants, and polyfills that cannot be autoloaded.

{
  "autoload": {
    "files": [
      "src/helpers.php",
      "src/constants.php"
    ]
  }
}
<?php
// src/helpers.php — loaded on every request via Composer files autoload
if (!function_exists('array_value_recursive')) {
    function array_value_recursive(string $key, array $arr): array {
        $result = [];
        foreach ($arr as $k => $v) {
            if ($k === $key) {
                $result[] = $v;
            }
            if (is_array($v)) {
                $result = array_merge($result, array_value_recursive($key, $v));
            }
        }
        return $result;
    }
}

Common use cases:

  • PHP function libraries (ramsey/uuid used to ship this way)
  • Symfony's string component helper functions
  • Polyfills for older PHP versions (symfony/polyfill-*)
  • Application-wide constants defined outside a class

Files from dependencies load before files from the root package. The exact order among dependencies is not guaranteed — avoid relying on cross-package include order.

PSR-0: Legacy Support

PSR-0 is deprecated but still encountered in old Drupal contrib, PEAR-era libraries, and Zend Framework 1. It differs from PSR-4 in one key way: underscores in class names are converted to directory separators.

{
  "autoload": {
    "psr-0": {
      "Twig_": "lib/"
    }
  }
}

The class Twig_Environment resolves to lib/Twig/Environment.php. Do not introduce new PSR-0 mappings. Migrate legacy code to PSR-4 when possible.

Autoloading in Development: dev Autoload

Test classes, fixtures, and tooling helpers belong in autoload-dev rather than autoload. They are excluded when dependencies install with --no-dev:

{
  "autoload": {
    "psr-4": {
      "Acme\\Blog\\": "src/"
    }
  },
  "autoload-dev": {
    "psr-4": {
      "Acme\\Blog\\Tests\\": "tests/"
    },
    "files": [
      "tests/bootstrap.php"
    ]
  }
}

Debugging Autoload Failures

Regenerate and Check

# Always regenerate after adding new classes or changing composer.json
composer dump-autoload

# Check where a class resolves
php -r "
require 'vendor/autoload.php';
\$loader = require 'vendor/autoload.php';
var_dump(\$loader->findFile('Acme\\\\Blog\\\\Post'));
"

Common Problems

SymptomLikely causeFix
Class not found after adding fileClassmap not regeneratedcomposer dump-autoload
Wrong class loaded (stale)Classmap authoritative with old cachecomposer dump-autoload -a
Drupal module class not foundMissing PSR-4 mapping in root composer.jsonAdd "Drupal\\my_module\\": "web/modules/custom/my_module/src/" to root autoload
Function "X" already definedFiles entry included by two packagesWrap functions in function_exists() guards
Slow bootstrap in productionPSR-4 filesystem scanning on every requestcomposer dump-autoload -o

Inspecting the Generated Autoloader

# See all registered PSR-4 namespaces
php -r "
\$loader = require 'vendor/autoload.php';
print_r(\$loader->getPrefixesPsr4());
"

# Count classmap entries
php -r "
\$map = require 'vendor/composer/autoload_classmap.php';
echo count(\$map) . ' classes in classmap\n';
"

Practical Composer Scripts Integration

{
  "scripts": {
    "post-install-cmd": [
      "@php artisan package:discover --ansi"
    ],
    "post-autoload-dump": [
      "Drupal\\Core\\Composer\\Composer::vendorTestCodeCleanup"
    ],
    "dump-prod": [
      "composer dump-autoload --optimize --no-dev --classmap-authoritative"
    ]
  }
}

Summary Checklist

  • Use PSR-4 for all new code; namespace prefix must end with a backslash, directory must exist
  • Use classmap for legacy libraries that don't follow PSR naming conventions
  • Use files only for global functions and constants — not for classes
  • Put test classes in autoload-dev so they're excluded from production installs
  • Run composer dump-autoload --optimize --no-dev during every production deployment
  • Add --classmap-authoritative in CI/production for maximum speed, but never in development
  • After adding new class files, always run composer dump-autoload — it is not automatic
  • Debug resolution with $loader->findFile('Fully\\Qualified\\ClassName')
  • Drupal modules need their namespace registered in the root composer.json or via Drupal's own discovery — not just inside the module's own composer.json
Tags

Add new comment

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.
Please share this article on your favorite website or platform.