PSR-4 Autoloading in PHP: How Composer Maps Namespaces to Folders

15 min read 2,927 words
PHP Autoloading with Composer: PSR-4 Standard and Why It Matters for Code Organisation featured image

PSR-4 autoloading tells Composer how to find PHP classes from their namespaces and class names. You map a namespace prefix to a folder in composer.json, then load Composer's autoloader once instead of adding a require statement for each class. You do not need a framework.

If a class disappears after adding PSR-4, inspect its declared name and the path Composer expects before changing more code. These examples are for a standalone custom PHP application, not a drop-in configuration for WordPress plugins or themes. Use PSR-4 for classes that follow its naming rules and classmap for older layouts that do not.

What PSR-4 autoloading does

PSR-4 turns a fully qualified class name into a file path. Composer implements the rule through an autoloader, which PHP calls when code references a class it has not loaded. The PHP-FIG PSR-4 specification defines the required relationship between class names and files.

Take SmallSite\Billing\Invoice. If the namespace prefix SmallSite\ points to src/, Composer looks for src/Billing/Invoice.php. It does not look for src/SmallSite/Billing/Invoice.php, because the mapped prefix is removed before constructing the remaining path.

The prefix need not match a folder name; it is a rule in Composer. To work out an expected path, remove the prefix from the requested class name and use the remaining namespace segments as directories. That gives you something specific to compare with the disk.

The PHP manual explains how registered autoloaders load a class when it is requested. The mechanism also covers interfaces and other autoloadable PHP definitions. It works without a framework if the application's entry point registers the loader. The related guide to building PHP MVC without a framework deals with application structure rather than class loading.

The autoload section in composer.json

Composer reads the mapping from composer.json. Under the psr-4 key, a namespace prefix points to a base directory relative to the project root. The Composer schema recommends PSR-4 because new classes under an existing mapping can be discovered without regenerating the loader.

The examples assume PHP 8 and Composer 2, with composer.json in the project root. For a project without an existing configuration, the relevant JSON is:

{
  "autoload": {
    "psr-4": {
      "SmallSite\\": "src/"
    }
  }
}

The autoload key holds loading rules; psr-4 selects the convention. SmallSite\\ is the namespace prefix as written in JSON, where a literal backslash is escaped with another backslash. The value src/ names the base folder, not another namespace segment.

If composer.json already exists, add the mapping within its current autoload section. Keep other settings intact and verify that src/ is the real location of the classes. Do not replace an existing configuration with the sample. These rules are about loading classes; Composer dependency management in PHP is a separate subject.

A folder layout that matches the namespaces

After the mapped prefix, each namespace segment becomes a subdirectory and the class name becomes a .php filename. The PSR-4 specification requires the case of directory segments and filenames to match the declared names.

Here the vendor directory holds Composer-generated files, while the application class belongs in src/. For SmallSite\Billing\Invoice, a possible layout is:

project/
  composer.json
  vendor/
    autoload.php
  public/
    index.php
  src/
    Billing/
      Invoice.php

Invoice.php declares the corresponding namespace and class. The PHP namespace declaration manual explains where that declaration belongs.

<?php

namespace SmallSite\Billing;

class Invoice
{
}

The web entry point registers Composer before using the class. The path below assumes public/ and vendor/ are sibling directories:

<?php

use SmallSite\Billing\Invoice;

require __DIR__ . '/../vendor/autoload.php';

$invoice = new Invoice();

The use statement provides a shorter name in this file; it does not load Invoice.php. The PHP manual on namespace importing explains that distinction. Also, __DIR__ is the directory containing index.php, so a different entry-point location needs a different require path.

Before trying the page, confirm that Invoice.php is inside the mapped base folder and declares SmallSite\Billing. After generating Composer's files, opening the page should no longer need a direct require for Invoice.php. If it still errors, keep the original working bootstrap available while tracing the requested class name.

Running composer dump-autoload, and when to optimise it for production

Changing an autoload rule requires regenerating Composer's loader. Adding a new class under an unchanged, ordinary PSR-4 mapping normally does not. Composer explains both cases in its command-line reference.

There is a difference between installing and regenerating. With a composer.lock file present, composer install installs the locked packages and generates vendor/autoload.php; composer dump-autoload rebuilds the loading files without installing or updating packages. If vendor/autoload.php is absent, first establish whether the project needs an install, because dump-autoload does not install packages.

Use a development or staging copy and keep a complete known-good release: application code, its vendor folder and generated autoload files together. Practise restoring that release in staging before touching production. Run the following commands from the project root with Composer 2 available as composer.

Before running dump-autoload on any server, inspect the scripts section of composer.json and the installed Composer plugins. The Composer scripts documentation confirms that pre-autoload-dump and post-autoload-dump handlers can execute during this command; Composer plugins can also subscribe to its events. If you cannot account for what they run, stop and review them in an isolated environment rather than executing an unfamiliar build on the live server.

Validation catches configuration errors before rebuilding the loader. Start with:

composer validate --no-check-publish

This checks the configuration without applying package-publishing requirements to a private application. Resolve reported errors before continuing. Then regenerate the normal development loader:

composer dump-autoload

Look for a successful exit and any messages about class mappings. Generated files include vendor/composer/autoload_psr4.php; edit composer.json, never that generated file. Where regeneration or its scripts produce an unexpected effect, return to the complete previous release and test that it runs before proceeding.

Check that the class can actually load

A successful Composer command does not establish that a particular class resolves. The PHP class_exists() manual explains that this function can trigger autoloading. Create check-autoload.php in the project root, outside the public web directory:

<?php

require __DIR__ . '/vendor/autoload.php';

echo class_exists('SmallSite\\Billing\\Invoice')
    ? "FOUND\n"
    : "MISSING\n";

Run the file from the project root in a fresh PHP 8 process. That avoids retaining class definitions from an earlier test:

php check-autoload.php

FOUND means PHP has the class available in that process; it does not identify which autoloader supplied it. A legacy loader or an earlier include can hide a broken PSR-4 mapping. Keep this test script free of extra includes, inspect the Composer mapping separately and compare the actual declaration with the requested class.

MISSING may mean no file was found, or that a loaded file did not declare the requested class under that name. A fatal error points to code failing during loading. Once you have inspected the result, delete the temporary script and exercise the affected application route. Do not remove more manual requires on the strength of FOUND alone.

Optimising the production autoloader

The Composer autoloader optimisation guide recommends a generated classmap for production. Known classes can then resolve without ordinary PSR-4 filesystem checks. Build against the same release that will go live, after its classes are in place.

Composer's CLI documentation states that dump-autoload normally inherits the development-mode setting from the last install or update. The autoload-dev rule is for the root project's development or test classes; it should not be included in a production loader. State the desired mode explicitly:

composer dump-autoload -o --no-dev

Here --no-dev omits autoload-dev rules; it does not itself uninstall development packages. Test this generated loader against the production code and use Composer's strict diagnostics to catch mapping mistakes or duplicate definitions. The Composer changelog lists --strict-ambiguous as added in version 2.8.0, so older Composer 2 releases need it left out:

composer dump-autoload -o --no-dev --strict-psr --strict-ambiguous

A failing exit code needs investigation before deployment. If production optimisation causes a regression, rebuild and test the correct release in staging rather than changing generated files on the server.

Do not enable classmap-authoritative solely because optimisation succeeds. Composer documents that an authoritative classmap refuses classes absent from its map, which matters for applications generating class files at runtime. That choice needs a separate compatibility check.

Moving a require-based site across one folder at a time

Move a small class group rather than deleting every require. Namespacing changes a class's fully qualified name as well as its location, so callers may need edits. Keep manual loading for unrelated code until that code has its own tested migration.

Decide what happens to stored objects before renaming a class. The PHP serialization manual confirms that serialized objects preserve their class name. Sessions containing serialized objects, object caches and queued jobs carrying the old class name can fail after a rename. Inventory those stores and decide whether to drain, migrate or temporarily support the old name; if no safe option is known, defer the rename. Rolling back code alone does not repair payloads already rewritten or consumed.

  • Step 1: Save a baseline. Preserve the complete working release, including vendor/. Identify the forms and background tasks that use the chosen class folder. Exercise those paths in staging, then perform and test a restore of the whole release before editing.
  • Step 2: Register Composer alongside old loading. Add the PSR-4 mapping, validate composer.json and generate the loader after reviewing scripts and plugins. Register vendor/autoload.php in the bootstrap without removing existing requires. Confirm the current site paths still work; otherwise return to the known-good release.
  • Step 3: Convert one folder. Move a billing class to src/Billing/Invoice.php and declare namespace SmallSite\Billing. Update every caller that names it, including configuration strings. Run the fresh-process class check and exercise the billing paths. An unresolved stored-object compatibility issue means stopping here.
  • Step 4: Remove redundant requires. Remove direct includes only for migrated classes, leaving untouched files alone. Search for references to their old paths and look for duplicate definitions. Exercise each affected entry point against staging data; a regression calls for the complete earlier release, not a mixture of old and new files.
  • Step 5: Deploy and verify a single release. Deploy its code and vendor directory as one unit, with the matching generated loader: upload the release to a new folder and switch to it in one step, or deploy inside a maintenance window, so no request runs against a mix of old and new files. Test web routes and workers after the switch, then read application errors. If the release is unhealthy, restore the tested known-good release and verify those same paths again. Deal separately with any persistent data already changed.

Servers can retain older PHP code after deployment. The PHP OPcache documentation says that when opcache.validate_timestamps is disabled, changed files require a cache reset, invalidation or web-server restart to take effect. Follow the server's established reset procedure after switching releases; the reset has to reach the PHP process that serves web requests, such as the PHP-FPM pool, because restarting only the web server or resetting OPcache from the command line may leave it untouched. Then request an affected page again; a CLI-only test is insufficient. Long-running queue workers also need an orderly restart to load the new code, as explained in Symfony's worker deployment guidance. The application need not use Symfony; follow its own worker supervisor procedure and confirm pending jobs resume safely.

A passing class_exists() check does not verify methods or business logic. Where automated tests are unavailable, record a repeatable manual check for each changed path before moving the next folder. Keep that check useful for rollback as well.

Standalone functions need not become classes to adopt PSR-4 for existing classes. The separate decision about when to use classes in PHP does not change how a class should be found.

Why class not found happens after adding PSR-4

PHP reports a missing class when it cannot resolve the requested definition; in PHP 8 the message reads like Class "SmallSite\Billing\Invoice" not found. For a PSR-4 mapping, trace the name that the application requests into the path Composer should try. Remember that an earlier include can make an isolated success misleading.

Wrong capitals in directories or filenames

PSR-4 requires the declared case to match the path. SmallSite\Billing\Invoice maps to Billing/Invoice.php under its configured base folder, not billing/invoice.php. A case-insensitive development disk may hide a mistake that appears on a case-sensitive server. Correct the mismatch in a reversible change and test on a production-like filesystem before releasing it.

Wrong namespace prefix or base directory

With SmallSite\ mapped to src/, Composer expects Billing/Invoice.php beneath src/. Mapping the prefix to src/SmallSite/ changes that expectation. Write down the requested fully qualified name, remove the configured prefix and compare the remaining path with the disk. If the mapping was wrong, correct composer.json, regenerate the loader and retest. Keep the earlier release available until that works.

Missing trailing backslash in composer.json

The Composer schema requires PSR-4 prefixes to end with a backslash so similar prefixes are not confused. Write "SmallSite\\" in JSON rather than "SmallSite". After fixing the key, run validation and regenerate the loader. Then test the class from a fresh process against the intended file.

The autoloader was never registered

Correct mappings do nothing in an entry point that never includes vendor/autoload.php. A web route may succeed while a scheduled script has no bootstrap at all. Register the loader in each applicable entry point or shared bootstrap, and exercise both web and CLI paths. Revert the bootstrap change if an include path is wrong.

An old classmap or duplicate definition

A classmap generated for an earlier release may not know about newly added files, especially in authoritative mode. A manual require can also define a class before Composer gets the chance. Regenerate against the exact release, inspect strict diagnostics and run the fresh-process check. If the problem remains, return to the full prior release rather than renaming more classes.

When PSR-4 is not enough (classmap and files)

Legacy class filenames sometimes do not match PSR-4 paths. The Composer autoload schema supports classmap for these classes and files for PHP files that should be included directly, such as standalone functions. They can coexist with PSR-4 during migration.

For this example, new classes live in src/, and older code remains under legacy/. The combined configuration is:

{
  "autoload": {
    "psr-4": {
      "SmallSite\\": "src/"
    },
    "classmap": [
      "legacy/classes/"
    ],
    "files": [
      "legacy/functions.php"
    ]
  }
}

Only add these entries when the paths exist. Preserve other composer.json settings, then validate and regenerate the loader. Recheck a legacy entry point after the change; the earlier release is the recovery point if loading changes unexpectedly.

When classmap makes sense

Composer scans classmap paths and records which file defines each class. That can preserve an older directory layout while newer code follows PSR-4, but adding or moving mapped classes requires another dump-autoload. Test a legacy class and inspect Composer's ambiguous-class diagnostics. If the mapping introduces conflicts, return to the last working configuration and release.

When files makes sense

The files rule loads named PHP files when Composer's autoloader is included. It is suitable for function declarations that class autoloading cannot discover. Read the file before adding it: database writes or immediate output could run earlier than intended.

After adding a files entry, test the affected entry points for duplicate declarations and unwanted side effects. If behaviour changes, restore the complete earlier release with its old explicit includes. Use PSR-4 for new namespaced classes, classmap where a legacy class layout cannot yet change, and files only for code intended to load immediately.

Start with one working class

Choose a class whose callers and stored data you can inspect, then prove it loads before removing its require statements. If the change spans old class names or several entry points, you can work with me on PHP code clean-ups and migrations. For PSR-4 autoloading, verify the namespace-to-file path against the complete release that will actually run.

Frequently Asked Questions

Can I use PSR-4 autoloading without a framework?
Yes. Composer can load your own namespaced PHP classes in a custom application. It needs a matching prefix-to-folder rule and an entry point that registers the generated autoloader. The approach does not require you to rebuild the application's architecture.
Why does Composer say my class cannot be found?
The requested name may not match the declared class, or the file may be somewhere other than the configured base folder. Also examine capital letters and whether the entry point registers Composer. A stale generated map can cause the same symptom.
Must I run dump-autoload after every new class?
No. A new class following an existing ordinary PSR-4 mapping can normally be discovered without regenerating the loader. Rebuild after changing the mapping or classmap contents, or when preparing an optimised production release with the current class list.
Why does class_exists return true when the mapping is wrong?
The class might already have been included or found by another registered autoloader. The function only reports whether PHP has the class available. Start a fresh process without extra manual includes and inspect Composer's mapping separately rather than treating a true result as proof of PSR-4 resolution.
Should I use classmap instead of PSR-4?
PSR-4 fits classes whose namespaces match their file locations. Classmap is useful when older filenames do not follow that convention, while files handles definitions that need direct inclusion. A migration can use these loading methods together if their interactions are checked.