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:
- Classmap (fastest — direct hash lookup)
- PSR-4 prefix matching
- PSR-0 prefix matching (legacy)
- 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:
| Class | File resolved to |
|---|---|
Acme\Blog\Post | src/Post.php |
Acme\Blog\Repository\PostRepository | src/Repository/PostRepository.php |
Acme\Admin\Dashboard | admin/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-devThis 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-devThis 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/uuidused to ship this way) - Symfony's
stringcomponent 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
| Symptom | Likely cause | Fix |
|---|---|---|
| Class not found after adding file | Classmap not regenerated | composer dump-autoload |
| Wrong class loaded (stale) | Classmap authoritative with old cache | composer dump-autoload -a |
| Drupal module class not found | Missing PSR-4 mapping in root composer.json | Add "Drupal\\my_module\\": "web/modules/custom/my_module/src/" to root autoload |
| Function "X" already defined | Files entry included by two packages | Wrap functions in function_exists() guards |
| Slow bootstrap in production | PSR-4 filesystem scanning on every request | composer 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-devso they're excluded from production installs - Run
composer dump-autoload --optimize --no-devduring every production deployment - Add
--classmap-authoritativein 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.jsonor via Drupal's own discovery — not just inside the module's owncomposer.json