Blog / Coding tips

PHP Composer Autoload PSR-4 Example (Step by Step)

Composer PSR-4 autoload means: you declare a namespace prefix and a folder in composer.json, run composer dump-autoload, then require 'vendor/autoload.php' once at the start of your app. After that, new App\Billing\Invoice loads src/App/Billing/Invoice.php automatically — no hand-written require for each class. The namespace path must mirror the directory path after the prefix. If they disagree, you get “Class not found”.

This is the packaging half of plain PHP apps that grow past a single file. It sits beside PDO, sessions and validation without replacing them. The demo below uses Composer 2.10 and PHP 8.4; you can follow along with a tiny Invoice class.

Why autoload beats require_once lists

Early PHP projects look like:

<?php
require_once __DIR__ . '/lib/Database.php';
require_once __DIR__ . '/lib/User.php';
require_once __DIR__ . '/lib/Invoice.php';

That list rots. Forget one file and you get fatal errors only on certain pages. An autoloader loads a class the first time you reference it. Composer generates that autoloader from a clear convention (PSR-4) so every dependency you install (composer require ...) is available the same way.

PSR-4 is the modern mapping: namespace prefix → base directory. PSR-0 is an older underscored style; new projects should use PSR-4.

Project layout

project/
  composer.json
  src/
    App/
      Billing/
        Invoice.php
  public/
    index.php
  vendor/          (created by Composer)
    autoload.php

public/index.php is the web entry point. Application code lives under src/App so the document root does not expose your classes directly.

composer.json autoload section

{
  "name": "mfaysal/autoload-demo",
  "autoload": {
    "psr-4": {
      "App\\": "src/App/"
    }
  }
}

Notes:

  • "App\\" is the namespace prefix. The double backslash is JSON escaping for a single \.
  • "src/App/" is the folder relative to composer.json. Trailing slash is conventional.
  • A class App\Billing\Invoice therefore must live at src/App/Billing/Invoice.php.
  • Case matters on Linux: Invoice.php not invoice.php.

The class itself

<?php
declare(strict_types=1);

namespace App\Billing;

final class Invoice
{
    public function __construct(
        public readonly string $number,
        public readonly int $pence,
    ) {}

    public function formatGbp(): string
    {
        return '£' . number_format($this->pence / 100, 2);
    }
}

The first line after declare is namespace App\Billing; — that must match the folder Billing under the App\ root. The class name must match the file name.

Generate the autoloader and use it

composer dump-autoload
# or: php composer.phar dump-autoload

Then in public/index.php or a CLI script:

<?php
declare(strict_types=1);

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

use App\Billing\Invoice;

$inv = new Invoice('INV-1001', 1999);
echo $inv->number, ' = ', $inv->formatGbp(), PHP_EOL;

Test output:

INV-1001 = £19.99
class loaded from: /workspace/seo-posts-php-sched/test/post05/src/App/Billing/Invoice.php

One require of vendor/autoload.php is enough per request. Do not require individual class files afterwards unless you are debugging.

For production deployments, composer install --no-dev -o generates an optimised classmap so lookups are faster. During development, plain dump-autoload is enough; Composer also registers a more flexible autoloader that finds new classes after you add files (you still re-dump after changing composer.json mappings).

What Composer writes in vendor/

You do not edit these by hand:

  • vendor/autoload.php — the entry point you require
  • vendor/composer/autoload_psr4.php — prefix → directory map
  • vendor/composer/autoload_classmap.php — when optimised, class → file

Your job is to keep composer.json truthful and run dump-autoload when the map changes.

Multiple prefixes and “files” autoload

{
  "autoload": {
    "psr-4": {
      "App\\": "src/App/",
      "Tools\\": "src/Tools/"
    },
    "files": [
      "src/helpers.php"
    ]
  }
}

files entries are always loaded when the autoloader is required — useful for a handful of functions (for example a global e() helper from the htmlspecialchars post). Prefer namespaced classes for most code so nothing loads until needed.

Dev-only autoload (test helpers) goes under "autoload-dev".

Common “Class not found” causes

  1. Forgot require vendor/autoload.php in that entry script.
  2. Namespace / folder mismatch — namespace App\Billing but file in src/App/Billling/.
  3. Wrong prefix in composer.json — "App\\": "src/" while files live in src/App/... (would expect App\App\Billing\...).
  4. Case mismatch on Linux hosting.
  5. Did not re-run composer dump-autoload after editing composer.json.
  6. Running a script from another cwd with a broken relative path to vendor/autoload.php — use __DIR__.
  7. Optimised classmap in production without regenerating after adding a class — redeploy with composer dump-autoload -o.

How this fits a small real app

Typical bootstrap:

<?php
declare(strict_types=1);

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

date_default_timezone_set('Europe/London'); // see timezone post
session_start();

$pdo = App\Database\Connection::make(); // your PDO wrapper

Then controllers use PDO (prepared statements), sessions (login example), and escaping on output. Composer does not secure your app; it only loads classes. Keep secrets out of the repo — environment variables rather than hard-coded DSN passwords — and validate uploads if you accept files (upload validation).

Without Composer (for understanding)

Under the hood PSR-4 is “turn namespace into path”. A minimal sketch:

<?php
spl_autoload_register(function (string $class): void {
    $prefix = 'App\\';
    $base = __DIR__ . '/src/App/';
    if (strncmp($prefix, $class, strlen($prefix)) !== 0) {
        return;
    }
    $relative = substr($class, strlen($prefix));
    $file = $base . str_replace('\\', '/', $relative) . '.php';
    if (is_file($file)) {
        require $file;
    }
});

Composer’s generated autoloader is more complete (classmaps, multiple packages, plugins). Use Composer in real projects; the sketch is only to demystify it.

Common mistakes

  1. Putting composer.json in the wrong directory relative to src/.
  2. Web root pointed at the project root so /vendor is public — point the vhost at public/ instead.
  3. Committing vendor/ on some teams vs not — either works if deployment runs composer install; do not commit secrets.
  4. Mixing old require trees with half-autoloaded code.
  5. Using underscores in class names expecting PSR-0 behaviour.
  6. Forgetting use App\Billing\Invoice; and then referencing Invoice in the global namespace.

FAQ

Do I need Composer for a single-file script?

No. Autoload pays off once you have multiple classes or third-party libraries. For one-off scripts, a single file is fine.

PSR-4 vs classmap?

PSR-4 maps prefixes to directories and discovers classes by path. A classmap lists concrete class → file pairs (faster, generated). Composer can build a classmap from your PSR-4 tree with -o.

Should App map to src/ or src/App/?

Both are valid. "App\\": "src/" means App\Billing\Invoice → src/Billing/Invoice.php. "App\\": "src/App/" means src/App/Billing/Invoice.php. Pick one and stay consistent; I like the second when other top-level folders sit beside App.

How do I autoload tests?

Use "autoload-dev": { "psr-4": { "Tests\\": "tests/" } } and require the same vendor/autoload.php from PHPUnit.

Does autoload replace namespaces?

No — it depends on namespaces (or a classmap). Without a namespace, Composer can still autoload via classmap, but PSR-4 expects namespaces.

What if I cannot run Composer on the server?

Run composer install --no-dev -o in CI or on your laptop, deploy the generated vendor/ directory, and keep PHP versions compatible. Still develop with Composer locally.

First-time Composer install on a project

cd project
composer init   # or write composer.json by hand
# add the autoload psr-4 block, then:
composer dump-autoload
composer require monolog/monolog   # example third-party lib

After composer require, the package’s own PSR-4 prefixes are merged into vendor/composer/autoload_psr4.php. Your App\ mapping stays intact. Commit composer.json and composer.lock; whether you commit vendor/ depends on the team. Many PHP hosts run composer install --no-dev -o on deploy.

Front controller pattern

public/index.php   -> requires vendor/autoload.php, routes request
src/App/Http/...   -> controllers
src/App/Domain/... -> business logic

Apache/nginx should set the document root to public/ so that /vendor and /src are not reachable by URL. That is an ops concern as much as an autoload concern — autoload makes more PHP files exist; the web server decides which are executable via HTTP.

Autoload and shared helpers

If you add src/helpers.php with function e(): string, list it under "files". Those functions load on every request that pulls in the autoloader — keep the file tiny. Prefer App\Support\Str::e() if you want lazy loading.

Upgrading PHP versions

Composer’s platform config can pin "php": ">=8.2". Run installs on the same major version you deploy. Autoload itself rarely breaks across versions; typed properties and readonly in your classes might. Test composer install in CI on the target PHP, same as I ran the Invoice demo on 8.4.

Troubleshooting flow chart

  1. Error mentions class name — check namespace and path case.
  2. Error mentions failed opening required — path to vendor/autoload.php is wrong.
  3. Works in CLI, fails in Apache — different working directory or PHP user cannot read vendor/.
  4. Works until deploy — forgot to run Composer on the server or to upload vendor/.
  5. New class ignored with -o classmap — rebuild autoload on deploy.

Once that checklist is muscle memory, Composer fades into the background and you spend time on sessions, uploads and XSS instead of require_once archaeology.

Example: moving an old project onto Composer

  1. Create composer.json with the App\\ mapping.
  2. Move classes into src/App/... matching namespaces.
  3. Add namespace declarations to each file.
  4. Replace the old require_once bootstrap with vendor/autoload.php.
  5. Run the test suite / click through login, uploads and forms.
  6. Only then delete the legacy includes/ tree.

Do not rename everything in one untested commit. Keep a branch. If a class is referenced as a string ('Invoice' in a config), update those strings to FQCNs (App\Billing\Invoice).

Autoload and opcache

In production, PHP’s opcache caches compiled bytecode. After deploy, reload PHP-FPM so both new files and a new Composer classmap are visible. A half-reloaded server shows intermittent class-not-found errors that look like autoload bugs but are really cache.

Security note

vendor/ contains third-party code. Run composer audit occasionally, pin versions in composer.lock, and do not expose the directory on the web. Autoloading a package does not mean you trust its security history blindly — especially for packages that touch auth, crypto or file parsing.

Worked mapping examples

| Class | Prefix App\ → src/App/ | File | |-------|----------------------------|------| | App\Billing\Invoice | | src/App/Billing/Invoice.php | | App\Http\LoginController | | src/App/Http/LoginController.php | | App\Support\e (invalid) | functions are not PSR-4 classes | use files or a class method |

| Class | Prefix App\ → src/ | File | |-------|------------------------|------| | App\Billing\Invoice | | src/Billing/Invoice.php |

Pick one column style for the whole repo. Mixing both is the fastest route to class-not-found.

Scripts section in composer.json

{
  "scripts": {
    "dump": "composer dump-autoload -o",
    "test": "phpunit"
  }
}

composer test then runs your suite. Autoload-dev must include test namespaces. This keeps local and CI commands identical.

Relation to frameworks

Laravel, Symfony and Slim all assume Composer autoload from day one. Learning PSR-4 on a tiny plain PHP demo — like the Invoice class above — makes their directory layouts less magical. When you later composer create-project a framework, you already know why app/ or src/ appears in composer.json.

Minimal public/index.php

<?php
declare(strict_types=1);

require dirname(__DIR__) . '/vendor/autoload.php';

use App\Billing\Invoice;

header('Content-Type: text/plain; charset=utf-8');
echo (new Invoice('INV-1001', 1999))->formatGbp();

Point the vhost at public/, run composer dump-autoload, open the site, and you should see £19.99. From there, grow folders under App\ for HTTP, domain and infrastructure without inventing a new loading system each time.

Takeaway

Composer PSR-4 is a naming contract: namespaces mirror directories, one autoload entry boots them all, and third-party packages join the same mechanism. Once that contract holds, you can focus on behaviour — passwords, sessions, uploads, timezones — instead of file plumbing.

Checklist

  1. composer.json has a PSR-4 prefix ending in \\.
  2. Folders under the base directory match namespace segments.
  3. Class name matches file name, including case.
  4. composer dump-autoload has been run after map changes.
  5. Entry script requires vendor/autoload.php via __DIR__.
  6. Document root is public/, not the project root.
  7. Production deploy runs composer install --no-dev -o (or ships a built vendor/).

Follow that list and “class not found” becomes a rare, usually typo-shaped event.

Further reading

Map the prefix, mirror folders to namespaces, require vendor/autoload.php once, and dump the autoloader when the map changes. That is the whole Composer PSR-4 habit for small PHP apps.

// note

How to read this note.

This is a learning note from studying the web. It is one small topic, written so I can remember it. It is not a course and not a claim that I have finished the subject.

If a sentence is wrong, say so from the contact page and name this title. Drafts never appear here. Related notes, when they exist, are other published posts, and the same sample rule applies to each of them.

Related notes