Composer is a dependency manager AND autoloader generator — it writes vendor/autoload.php for you
PSR-4 maps namespaces to directories: App\ → src/ means App\Models\Invoice lives at src/Models/Invoice.php
Classmap is for legacy code — it scans directories and builds a static map; new classes need composer dump-autoload
Autoloading is lazy: the autoloader registers itself with SPL and only loads files when a class is actually used
In production, run composer install --optimize-autoloader to convert PSR-4 rules into a classmap for zero-lookup overhead
Biggest mistake: forgetting to escape backslashes in composer.json — requires double backslash for namespace prefixes
Plain-English First
Imagine you own a huge library with thousands of books. Every time a visitor asks for a book, instead of fetching EVERY book off EVERY shelf before they even sit down, a smart librarian only grabs the exact book they ask for, right when they ask for it. Composer is the library catalogue — it knows what books (packages) exist and where to get them. Autoloading is the smart librarian — it fetches only the PHP class files your code actually needs, exactly when it needs them. No manual fetching, no waste.
(already present, kept as-is)
What Composer Actually Does — Beyond Just Installing Packages
Most developers think of Composer purely as a package installer — run composer require, get a library. That's true, but it's only half the story. Composer does two distinct jobs: dependency management and autoload generation.
Dependency management means Composer reads your composer.json, figures out which versions of which packages satisfy all your requirements simultaneously (including packages that your packages depend on), downloads them into the vendor/ directory, and locks the exact resolved versions into composer.lock. The lock file is how your entire team — and your production server — runs the exact same code.
Autoload generation is the part most tutorials skip. After installing packages, Composer writes a single file: vendor/autoload.php. When you require that one file at the top of your application, PHP gains the ability to load any class from any installed package — plus your own application classes — automatically, on demand, with zero manual require calls. Understanding how that file works changes the way you structure your entire application.
bootstrap.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php
/**
* bootstrap.php — The single entry point that wires everything together.
* This is the ONLYrequire statement your application needs.
* Composer's autoloader handles everything else from here.
*/
require_once __DIR__ . '/vendor/autoload.php';
// At this point, ANY class from ANY installed Composer package// (plus your own PSR-4 mapped classes) is available instantly.// PHP won't actually load the class file until you USE the class — lazy loading.// Let's use Carbon (a popular date library) as a concrete example.// No require for Carbon anywhere — the autoloader finds it automatically.useCarbon\Carbon;
$launchDate = Carbon::create(2024, 3, 15, 9, 0, 0);
$now = Carbon::now();
$daysUntilLaunch = $now->diffInDays($launchDate, false);
if ($daysUntilLaunch > 0) {
echo"Launch is in {$daysUntilLaunch} days." . PHP_EOL;
} else {
echo"Launch was " . abs($daysUntilLaunch) . " days ago." . PHP_EOL;
}
// The autoloader only loaded Carbon's class files when 'new Carbon' was triggered.// Every other Composer package stayed untouched on disk — no wasted memory.
Output
Launch was 312 days ago.
The Autoloader Is Lazy By Design
Composer's autoloader doesn't read every class file at startup. It registers a callback with PHP's SPL autoload stack and only triggers when PHP encounters an unknown class name. This means including vendor/autoload.php has almost zero performance cost on its own — you only pay for the classes you actually use.
Production Insight
In production, the lazy loading works fine because each request typically uses a small subset of classes.
But if you rely on class_exists() calls with autoload=false, you bypass the autoloader entirely.
Rule: use spl_autoload_register for custom checks — never require classes manually.
Key Takeaway
vendor/autoload.php is the only require your app needs.
Composer's autoloader is lazy — zero startup cost.
Always commit composer.lock to freeze dependencies.
PSR-4 Autoloading — How to Map Your Own Classes Like a Pro
PSR-4 is a standard published by the PHP-FIG group that defines one rule: a class's fully-qualified namespace must map directly to a file path. That's it. If your class is App\Services\EmailService, and your PSR-4 root maps App\ to src/, then PHP expects to find that class at src/Services/EmailService.php. The namespace mirrors the directory structure exactly.
This is the autoloading setup you'll configure for your own application code — not just third-party packages. You declare it in composer.json under the autoload key. After any change to that section, you must run composer dump-autoload to regenerate the autoloader files. Forgetting this step is one of the most common causes of 'class not found' errors.
The beauty of PSR-4 is that it's purely convention-based. There's no configuration file per class, no database of class locations. Composer just applies the namespace-to-path transformation rule, and if the file exists, it loads it. Your directory structure becomes self-documenting — anyone looking at a namespace instantly knows where the file lives.
composer.jsonPHP
1
2
3
4
5
6
{
"name": "theforge/invoice-app",
"description": "A real-world invoicing application",
"require": {\n \"php\": \">=8.1\",\n \"nesbot/carbon\": \"^3.0\"\n },\n \"autoload\": {\n \"psr-4\": {\n \"App\\\\\": \"src/\"\n }\n },\n \"autoload-dev\": {\n \"psr-4\": {\n \"Tests\\\\\": \"tests/\"\n }\n },\n \"minimum-stability\": \"stable\"\n}\n\n/* HOW THIS MAPPING WORKS:\n *\n * Namespace prefix -> Directory\n * \"App\\\\\" -> \"src/\"\n *\n * So PHP resolves class names like this:\n * App\\\Models\\\Invoice -> src/Models/Invoice.php\n * App\\\Services\\\PdfExporter -> src/Services/PdfExporter.php\n * App\\\Http\\\Controllers\\\InvoiceController -> src/Http/Controllers/InvoiceController.php\n *\n * The autoload-dev section works identically but is EXCLUDED\n * when you run: composer install --no-dev (i.e., on production).\n * This keeps test classes off your production server automatically.\n */",
"output": "/* No PHP output — this is composer.json configuration.\n After saving this file, run:\n $ composer dump-autoload\n \n Expected terminal output:\n Generating optimized autoload files\n Generated autoload files */"
}
Building a Real Project Structure With PSR-4 Autoloading
Let's stop talking in abstractions and build something real. Here's a minimal invoice application that demonstrates PSR-4 autoloading working end-to-end across multiple classes and namespaces. This is the pattern you'll see in Laravel, Symfony, and every modern PHP framework.
The key mental model: your namespace tree and your directory tree are mirrors of each other. The App\ root namespace lives in src/. Every subdirectory is a sub-namespace. Every class file contains exactly one class, and the class name matches the filename exactly — including capitalisation. PHP's autoloader is case-sensitive on Linux/Mac in production, even if it forgives you locally on Windows.
Notice how none of the application files contain a single require or include statement for other application classes. The moment you type use App\Models\Invoice, PHP's SPL autoload stack fires the Composer callback, which translates that namespace to src/Models/Invoice.php and loads it. This is why PSR-4 is considered the foundation of maintainable PHP architecture.
src/Models/Invoice.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<?php
// FILE: src/Models/Invoice.php// Namespace MUST match directory path relative to the PSR-4 root (src/)declare(strict_types=1);
namespaceApp\Models;
use Carbon\Carbon; // Loaded from vendor/ by Composer — no require neededclassInvoice
{
privateCarbon $issuedAt;
privateCarbon $dueDate;
publicfunction__construct(
privatereadonly string $invoiceNumber,
privatereadonly string $clientName,
privatereadonly float $amountDue,
int $paymentTermDays = 30
) {\n // Carbon is available here because Composer mapped it automatically\n $this->issuedAt = Carbon::now();\n $this->dueDate = Carbon::now()->addDays($paymentTermDays);\n }publicfunctiongetInvoiceNumber(): string { return $this->invoiceNumber; }
publicfunctiongetClientName(): string { return $this->clientName; }
publicfunctiongetAmountDue(): float { return $this->amountDue; }
publicfunctionisOverdue(): bool
{
// Check if today is past the due datereturnCarbon::now()->isAfter($this->dueDate);
}
publicfunctiongetSummary(): string
{
$status = $this->isOverdue() ? 'OVERDUE' : 'Current';
$dueDateFmt = $this->dueDate->format('Y-m-d');
returnsprintf(
"Invoice %s | Client: %s | Amount: $%.2f | Due: %s | Status: %s",
$this->invoiceNumber,
$this->clientName,
$this->amountDue,
$dueDateFmt,
$status
);
}
}
// ─────────────────────────────────────────────────────────────────────────────// FILE: src/Services/InvoiceMailer.php (separate file — shown inline for clarity)// ─────────────────────────────────────────────────────────────────────────────// <?php// declare(strict_types=1);// namespace App\Services;//// use App\Models\Invoice;//// class InvoiceMailer// {// public function send(Invoice $invoice, string $recipientEmail): bool// {\n// // Real implementation would use a mail library\n// echo \"Sending invoice {$invoice->getInvoiceNumber()} to {$recipientEmail}\" . PHP_EOL;\n// return true;\n// }\n// }\n\n// ─────────────────────────────────────────────────────────────────────────────\n// FILE: index.php (project root — the application entry point)\n// ─────────────────────────────────────────────────────────────────────────────\n\n// <?php\n// declare(strict_types=1);\n//\n// require_once __DIR__ . '/vendor/autoload.php'; // The ONE require to rule them all\n//\n// use App\\\Models\\\Invoice;\n// use App\\\Services\\\InvoiceMailer;\n//\n// // PHP sees 'App\\\Models\\\Invoice', fires Composer autoloader,\n// // which loads src/Models/Invoice.php — automatically, silently, correctly.\n// $invoice = new Invoice(\n// invoiceNumber: 'INV-2024-0042',\n// clientName: 'Acme Corporation',\n// amountDue: 1850.00,\n// paymentTermDays: 14\n// );\n//\n// echo $invoice->getSummary() . PHP_EOL;\n//\n// $mailer = new InvoiceMailer();\n// $mailer->send($invoice, 'accounts@acmecorp.com');\n","output": "Invoice INV-2024-0042 | Client: Acme Corporation | Amount: $1850.00 | Due: 2025-02-14 | Status: Current\nSending invoice INV-2024-0042 to accounts@acmecorp.com"
}
PSR-0 vs PSR-4: The Deprecated Standard vs The Current One
Before PSR-4 became the standard in 2012, PHP-FIG defined PSR-0 as the first autoloading convention. PSR-0 used a different rule: it treated underscores in class names as directory separators. For example, a class named My_Project_NewClass would map to My/Project/NewClass.php. This convention was heavily inspired by the PEAR library naming scheme. It also required the entire class name (including namespace) to match the file path exactly, but unlike PSR-4, the namespace root was not configurable — it always used the full namespace as the path.
The critical limitation of PSR-0 was that the underscore-to-directory rule often clashed with modern code styles using namespaces. If you had a class named MyProject_NewClass, the underscore forced a directory separator even though semantically it might not correspond to a namespace level. PSR-4 removed this rule entirely — underscores have no special meaning in PSR-4; they are treated as literal characters in the filename. This single change simplified autoloading and eliminated a class of naming bugs.
Today, PSR-0 is deprecated but you'll still encounter it in older libraries (pre-2015). Composer still supports PSR-0 for backward compatibility, but you should never use it for new projects. The migration is straightforward: if you have a PSR-0 entry in composer.json, replace it with an equivalent PSR-4 mapping. The file structure rarely needs to change — just update the autoload configuration.
psr0-vs-psr4-comparison.txtPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
COMPARISON: PSR-0 vs PSR-4
--------------------------------------------------------------------------
Rule | PSR-0 | PSR-4
--------------------------------------------------------------------------
Underscore in class name | Becomes directory | Literal character
| separator | (no special meaning)
Namespace prefix configurable? | No — always the | Yes — map any prefix
| full namespace | to a directory
Example: | |
Class: App\Models\Invoice | |
Expectedfile (PSR-0): | App/Models/Invoice | (not applicable)
| .php |
Expectedfile (PSR-4 with | (not applicable) | src/Models/Invoice.php
App\ => src/): | |
Legacyclass: My_Library_Helper | My/Library/Helper | My_Library_Helper.php
| .php |
Deprecated since: | 2012 | N/A (current)
--------------------------------------------------------------------------
Composer.json example (PSR-0):
{
"autoload": {
"psr-0": {
"App": "src/"
}
}
}
Composer.json example (PSR-4):
{
"autoload": {
"psr-4": {
"App": "src/"
}
}
}
Output
/* No runtime output — this is a configuration reference */
Migrating from PSR-0 to PSR-4
If your project still uses PSR-0, change the key in composer.json from "psr-0" to "psr-4". The file paths are identical because PSR-0 also used namespace-to-path mapping. The only difference is underscores in class names — if you have any, you'll need to rename those files. Run composer dump-autoload and verify with a thorough test suite.
Production Insight
Some older Composer packages (e.g., older versions of Doctrine or Monolog) still declare PSR-0 in their own composer.json. Composer handles both seamlessly — the autoloader will resolve classes from both standards. You don't need to change vendor code, only worry about your own application's autoload configuration.
Key Takeaway
PSR-0 is deprecated but still supported for legacy libraries. PSR-4 is the current standard. Underscore-to-directory is the key difference — PSR-4 treats underscores literally.
Composer's Autoloader Internals — How SPL Autoload Registration Works
Composer's autoloader is not magic — it's a simple callback registered with PHP's spl_autoload_register function. When PHP encounters a class it hasn't loaded yet, it iterates through every registered autoloader in order until one succeeds. Composer registers its callback with a high priority (low number) so it runs first.
The generated vendor/autoload.php file is a thin wrapper. It includes a bootstrap script that builds an array of PSR-4 prefixes to directories (from autoload_psr4.php), classmap entries (from autoload_classmap.php), and files to include (from autoload_files.php). When the callback fires, it does: 1. Check if the class name matches any classmap entry — instant hit. 2. If not, iterate PSR-4 prefixes. For each prefix, check if the class name starts with that prefix. If yes, replace the prefix with the mapped directory, convert namespace separators to DIRECTORY_SEPARATOR, append .php, and include if the file exists. 3. If no prefix matched, return false to let the next autoloader in the stack try.
This design means multiple autoloaders can coexist — Composer only handles its own namespaces. If you have custom autoloaders for legacy code, they'll work side by side.
vendor/autoload.php (simplified)PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php
// Simplified representation of what composer generatesspl_autoload_register(function ($class) {
// 1. Check classmap first (available only with --optimize)static $classmap = [
'App\Models\Invoice' => __DIR__ . '/../src/Models/Invoice.php',
// ... thousands of entries in production
];
if (isset($classmap[$class])) {
require $classmap[$class];
return;
}
// 2. PSR-4 prefix matchingstatic $prefixes = [
'App\' => [__DIR__ . '/../src/'],
'Carbon\' => [__DIR__ . '/../vendor/nesbot/carbon/src/Carbon/'],
// ... more
];
foreach ($prefixes as $prefix => $dirs) {
$len = strlen($prefix);
if (strncmp($prefix, $class, $len) !== 0) {\n continue;\n }
$relativeClass = substr($class, $len);
foreach ($dirs as $dir) {
$file = $dir . str_replace('\', '/', $relativeClass) . '.php';
if (file_exists($file)) {
require $file;
return;
}
}
}
// 3. Not found — let next autoloader try
});
Output
/* No output - this is internal autoloader logic */
Multiple Autoloaders Can Coexist
PHP's SPL autoload stack can hold many callbacks. Composer's autoloader is just one of them. If you have a legacy library with its own autoloader, they'll run in order. You can inspect the stack with spl_autoload_functions().
Production Insight
If you register a custom autoloader AFTER including vendor/autoload.php, it may never be called if Composer succeeds first.
That's fine — but if Composer fails, PHP throws a fatal error before reaching your custom autoloader.
Fix: register custom autoloaders with prepend=true or before Composer's.
Key Takeaway
Composer registers one callback with spl_autoload_register.
It checks classmap first, then PSR-4 prefixes.
Order of autoloaders matters — Composer runs first by default.
Visual Autoloading Pipeline: From Request to File Inclusion
When a PHP application receives a request and triggers a class that hasn't been loaded yet, a well-defined sequence of steps happens under the hood. Understanding this pipeline is crucial for debugging autoloading issues and for optimising performance.
The pipeline starts at the entry point (typically index.php), where require_once __DIR__ . '/vendor/autoload.php' is executed. This file boots Composer's autoloader by registering a closure with spl_autoload_register. At this point, no class files are loaded yet — only the autoloader callback is registered on the SPL stack.
Later, when PHP encounters a class it hasn't seen before (e.g., new App\Services\PaymentGateway()), PHP pauses execution and fires every autoloader in the stack in order. Composer's callback begins its work:
Classmap Lookup: If Composer was run with --optimize-autoloader, a static array mapping class names to file paths exists. The callback checks this array first — O(1) lookup. If found, the file is included immediately.
PSR-4 Prefix Matching: If no classmap hit, the callback iterates through each namespace prefix registered in autoload_psr4.php. For each prefix, it uses strncmp to see if the requested class starts with that prefix. When found, it replaces the prefix with the registered directory, converts backslashes to directory separators, appends .php, and calls file_exists().
PSR-0 Fallback (if any): For any remaining PSR-0 entries, the same logic applies but underscores are converted to directory separators.
File Inclusion: If a matching file exists, PHP's include statement pulls it in. The class definition is now available, and PHP continues execution. If no autoloader succeeds, PHP throws a fatal error.
This flow is visualised in the diagram below. The key takeaway is that the pipeline is lazy and runs only when needed — every request does not scan all directories. Optimised autoloaders shortcut the PSR-4 loop by pre-building a classmap, which is why --optimize-autoloader is recommended for production.
Production Pipeline Shortcut
When you run composer install --optimize-autoloader, the classmap is pre-populated for every class Composer can find in your project (your PSR-4 directories + installed packages). This means the first step in the pipeline (classmap lookup) succeeds for nearly all requests, eliminating the PSR-4 prefix iteration entirely. This is the single highest-impact optimisation you can make.
Production Insight
In a high-traffic production environment, even the PSR-4 prefix loop can be measurable if you have dozens of prefixes. With --classmap-authoritative, Composer skips the file_exists checks on the classmap entries, trading a tiny risk of stale maps for faster performance. Use this only when your deploy process reliably rebuilds the classmap.
Key Takeaway
The autoloading pipeline is lazy and sequential: classmap → PSR-4 prefixes → next autoloader. Optimise production by pre-building a classmap to skip the loop.
Building Your Own Autoloader with spl_autoload_register (Without Composer)
Before Composer became universal, every PHP framework had its own autoloader. Understanding how to write one from scratch gives you deeper insight into what Composer does for you — and helps you debug when things go wrong.
spl_autoload_register() accepts a callable that PHP invokes whenever it encounters an undefined class. The function receives the fully-qualified class name as a string. Your job is to convert that string into a file path and include the file. If your function can't load the class, it simply returns without throwing an error — PHP then tries the next autoloader in the stack.
A minimalist autoloader might just map the entire namespace to a single directory, using str_replace to convert backslashes to directory separators. This is exactly what PSR-4 does, but without the configurable prefix mapping. A more useful autoloader allows prefix mapping, similar to Composer's PSR-4 support.
The key feature of spl_autoload_register() is that you can register multiple autoloaders. This is how Composer coexists with your custom code. You can also control the order (prepend parameter), and you can remove autoloaders with spl_autoload_unregister().
A common use case for a custom autoloader is when you have legacy code that doesn't follow PSR-4 and you don't want to use Composer's classmap because it requires manual rescanning. Or maybe you're building a micro-framework and want minimal dependencies. But in 2026, there's rarely a reason to not use Composer — write custom autoloaders only for very specific edge cases.
src/MyAutoloader.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php
/**
* CustomPSR-4-like autoloader without Composer.
* Registers itself with spl_autoload_register.
*/
spl_autoload_register(function (string $class): void {
// Define namespace prefix to directory mapping
$prefixes = [
'App\' => __DIR__ . '/src/',
'Lib\' => __DIR__ . '/lib/',
];
foreach ($prefixes as $prefix => $baseDir) {
$len = strlen($prefix);
if (strncmp($prefix, $class, $len) !== 0) {\n continue;\n }
// Get the relative class name (strip the prefix)
$relativeClass = substr($class, $len);
// Convert namespace separators to directory separators
$file = $baseDir . str_replace('\', '/', $relativeClass) . '.php';
if (file_exists($file)) {
require $file;
return; // Successfully loaded
}
}
// No match — return silently; next autoloader gets a chance
});
// Usage: include this file first, then any class in App\ namespace// will be loaded automatically.// Example: App\Models\User will load from src/Models/User.php
Output
/* No output — this is a bootstrap file. Include it before any class usage. */
spl_autoload_call() Function
PHP also provides spl_autoload_call($class) to manually trigger the autoload stack for a specific class without using it in code. This is useful in tests or when you want to preload classes deterministically. Avoid it in production — let lazy loading handle things.
Production Insight
Custom autoloaders should be lightweight and fast. Avoid heavy filesystem scans in the callback — that's why Composer's classmap exists. If you must use a custom autoloader, cache the resolved paths (e.g., in a static array) after the first successful lookup.
Key Takeaway
spl_autoload_register is the underlying mechanism. Composer wraps it with PSR-4 and classmap support. Writing a custom autoloader is straightforward but rarely needed in modern PHP.
Classmap vs PSR-4 vs Files — Choosing the Right Autoload Strategy
Composer supports three autoloading strategies and you'll encounter all three in real codebases. Understanding when each is appropriate — and why each exists — separates juniors from seniors.
PSR-4 is your default for all modern application code. It's convention-over-configuration: no explicit class listing required, and adding a new class is as simple as creating the file in the right directory. Use this for everything you own.
Classmap is used for legacy code that doesn't follow PSR-4 naming conventions — think old libraries with inconsistent naming, or generated code (like Doctrine proxy classes). Composer scans the specified directories, finds every class/interface/trait, and builds an explicit map. It works regardless of how the files are named. The downside: every time you add a class, you must run composer dump-autoload to update the map.
Files autoloading is for code that isn't a class at all — global helper functions, constants, or procedural scripts that need to always be available. Laravel's Illuminate helpers use this approach. Files listed here are included on every single request, so keep this list short. Never use it as a workaround for broken class autoloading — that's a code smell.
composer-autoload-strategies.jsonPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
{
"autoload": {
"psr-4": {
"App": "src/"// BEST FOR: Your own modern application code.// HOW IT WORKS: Translates namespace to file path dynamically.// WHEN TO USE: Always, for new code you write.
},
"classmap": [\n \"lib/legacy/\",\n \"lib/generated/\"\n // BEST FOR: Old code with no consistent naming convention.\n // HOW IT WORKS: Scans directories at dump-autoload time, builds\n // a complete class-to-file lookup table.\n // WHEN TO USE: Integrating a legacy library or generated PHP code.\n // GOTCHA: New classes in these dirs need composer dump-autoload\n // before they're discoverable.\n ],\n\n \"files\": [\n \"src/helpers.php\"\n // BEST FOR: Global functions that aren't classes.\n // HOW IT WORKS: PHP includes EVERY file in this list on EVERY request.\n // WHEN TO USE: Sparingly — helper functions like str_humanize() etc.\n // NEVER USE FOR: Classes. That's what psr-4 and classmap are for.\n ]\n\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// REAL EXAMPLE: src/helpers.php — functions, not classes\n// ─────────────────────────────────────────────────────────────────────────────\n\n<?php\n// These functions are available globally across the whole application\n// because this file is listed under \"files\" in composer.json autoload.\n\nif (!function_exists('format_currency')) {\n /**\n * Formats a float as a localised currency string.\n * The function_exists guard prevents fatal errors if this file\n * is ever accidentally included twice.\n */\n function format_currency(float $amount, string $currencyCode = 'USD'): string\n {\n $formatter = new NumberFormatter('en_US', NumberFormatter::CURRENCY);\n return $formatter->formatCurrency($amount, $currencyCode);\n }\n}\n\n// Usage anywhere in your application (after vendor/autoload.php is required):\n// echo format_currency(1850.00); // Output: $1,850.00\n","output": "/* composer.json config — no direct output.\n After running: composer dump-autoload\n \n Terminal output:\n Generating optimized autoload files\n Generated autoload files\n \n For helpers.php function:\n echo format_currency(1850.00);\n Output: $1,850.00 */"
},
"callout": {
"type": "info",
"title": "Interview Gold: Why Does --optimize-autoloader Use Classmap Internally?",
"text": "When you run composer dump-autoload --optimize, Composer converts your PSR-4 rules into a classmap for every class it can find. This is exactly what --optimize-autoloader does in production — it gives you the convenience of PSR-4 development workflow with the raw speed of classmap lookups. Knowing this tells an interviewer you understand both strategies at a deep level."
},
"production_insight": "Classmap requires dump-autoload every time a class is added — forget it and CI breaks.\nFiles autoloading adds overhead on every request — keep it under 3 files.\nOptimised autoloader blends PSR-4 convenience with classmap speed.",
"key_takeaway": "PSR-4: convention-based, no rescan needed.\nClassmap: explicit scan, must rerun after new classes.\nFiles: for global functions only — avoid for classes."
},
{
"heading": "Autoloading Strategy Comparison Matrix",
"content": "Selecting the right autoloading strategy (or combination) is a trade-off between convenience, performance, and compatibility. The matrix below maps each strategy against key decision factors that matter in real production applications. Use this as a quick reference when architecting a new project or debugging a deployment.\n\nEach strategy serves a distinct purpose. PSR-4 is the default for all modern application code because it requires zero maintenance — you add a file, the autoloader finds it by convention. Classmap is your fallback for legacy or generated code where you have no control over naming conventions. Files autoloading is strictly for global functions, never for classes. PSR-0 is deprecated but you'll still encounter it in older packages; Composer handles it transparently.\n\nWhen performance matters most (production), all roads lead to classmap: using --optimize-autoloader converts PSR-4 and PSR-0 entries into a pre-built map, so runtime lookup overhead is identical regardless of the strategy you configured.",
"code": {
"language": "text",
"filename": "autoloading-strategy-matrix.txt",
"code": "DECISION MATRIX: Autoloading Strategy Comparison\n\n┌──────────────────────┬──────────┬──────────┬──────────┬──────────┐\n│ Factor │ PSR-4 │ Classmap │ Files │ PSR-0 │\n├──────────────────────┼──────────┼──────────┼──────────┼──────────┤\n│ Convenience for new │ Excellent│ Poor │ N/A │ Good │\n│ classes (zero config)│ (auto) │ (need du)│ │ (auto) │\n├──────────────────────┼──────────┼──────────┼──────────┼──────────┤\n│ Runtime lookup speed │ Good │ Excellent│ N/A │ Good │\n│ (unoptimised) │ (prefix │ (hash) │ (always │ (prefix │\n│ │ loop) │ │ loaded) │ loop) │\n├──────────────────────┼──────────┼──────────┼──────────┼──────────┤\n│ Optimised speed │ Excellent│ Excellent│ N/A │ Excellent│\n│ (--optimize) │ (becomes │ (same) │ │ (becomes │\n│ │ classmap)│ │ │ classmap)│\n├──────────────────────┼──────────┼──────────┼──────────┼──────────┤\n│ Supports global │ No │ No │ Yes │ No │\n│ functions/constants │ │ │ │ │\n├──────────────────────┼──────────┼──────────┼──────────┼──────────┤\n│ Works with any │ No │ Yes │ N/A │ No │\n│ naming convention │ (must │ (scan │ │ (unders- │\n│ │ follow │ any dir)│ │ core = │\n│ │ PSR-4) │ │ │ /dir) │\n├──────────────────────┼──────────┼──────────┼──────────┼──────────┤\n│ Underscore handling │ Literal │ Literal │ N/A │ Becomes │\n│ │ │ │ │ dir sep │\n├──────────────────────┼──────────┼──────────┼──────────┼──────────┤\n│ Risk of stale cache │ None │ High │ None │ None │\n│ (if du forgotten) │ │ (missed │ │ │\n│ │ │ classes) │ │ │\n├──────────────────────┼──────────┼──────────┼──────────┼──────────┤\n│ Ideal use case │ New app │ Legacy │ Boot- │ Legacy │\n│ │ code, │ libs, │ strap │ libs │\n│ │ packages │ generated│ helpers │ (pre- │\n│ │ │ code │ │ 2015) │\n├──────────────────────┼──────────┼──────────┼──────────┼──────────┤\n│ Adoption status │ Current │ Current │ Current │ Depre- │\n│ │ standard │ │ │ cated │\n└──────────────────────┴──────────┴──────────┴──────────┴──────────┘\n\nRECOMMENDATION:\n- 95% of projects: use PSR-4 only.\n- Add classmap only for directories you cannot refactor.\n- Use files sparingly (< 5 entries).\n- Ignore PSR-0 for new code; Composer handles backward compat.",
"output": "/* Matrix reference — no runtime output */"
},
"callout": {
"type": "tip",
"title": "Quick Rule of Thumb",
"text": "If you're writing a class, use PSR-4. If you're writing a function, use the files autoload (but consider wrapping it in a staticclass instead). If you can't control the naming, use classmap. This rule covers 99% of scenarios."
},
"production_insight": "In production, the difference between PSR-4 and classmap is negligible when using --optimize-autoloader, as both become a flat map. The real cost is development friction: if you use classmap for your own code, every new class requires a 'composer dump-autoload'or it won't be found. This often causes confusion in CI when a new class is added in a feature branch but the autoloader hasn't been rebuilt.",
"key_takeaway": "PSR-4 is the best default for new code: zero maintenance, convention-based. Classmap is a necessary evil for legacy code. Files is a last resort for global functions. Always optimize for production."
},
{
"heading": "Autoloading Types Comparison: PSR-4, Classmap, Files, and PSR-0",
"content": "To make informed decisions about autoloading, it helps to see all four types side by side. Each has different trade-offs in terms of performance, convenience, and compatibility. The table below summarises the key differences.\n\nPSR-4 is the modern standard for application code and most new packages. Classmap is a fallback for legacy or generated code. Files are for global functions and constants. PSR-0 is deprecated but still encountered in older packages.\n\nWhen you run composer install --optimize-autoloader, Composer internally converts all PSR-4 and PSR-0 registrations into a classmap. This gives you the best of both: the convenience of namespace-based autoloading in development and the speed of explicit maps in production.",
"code": {
"language": "text",
"filename": "autoload-types-comparison.txt",
"code": "COMPARISON TABLE: Autoloading Strategies Supported by Composer\n\nFeature | PSR-4 | PSR-0 | Classmap | Files\n--------------------------|-------------|------------|----------------|-------------------\nClass name rule | Namespace | Namespace | Any name | N/A (no classes)\n | prefix + | prefix + | |\n | class name | class name | |\nUnderscore handling | Literal | Directory | Literal | N/A\n | | separator | |\nRequires dump-autoload | No | No | Yes | No\non new file? | | | |\nRuntime lookup overhead | Low (dev) | Low (dev) | None (prebuilt)| None (always\n | | | | included)\nRequires namespace | Yes | Yes | No | No\nconvention? | | | |\nUse for | Application | Legacy | Generated code | Global functions\n | code, new | libraries |, libraries |, constants,\n | packages | (pre-2015) | with no | procedural\n | | | namespace | bootstrap\n | | | convention |\nCommon example | Laravel App | Old Zend | Doctrine proxy | Laravel\n | classes | Framework | classes | helpers\n | | classes | |\nImplicit in | PSR-4 | PSR-0 | classmap | files\ncomposer.json key | | | |",
"output": "/* No runtime output — refer to the table for decision-making */"
},
"callout": {
"type": "tip",
"title": "When to Use Each Autoloader Type",
"text": "For 95% of modern PHP projects: stick with PSR-4 only. Add classmap entries only if you integrate a legacy library that can't be refactored. Use files as a last resort for global helpers — consider using a class with static methods instead to keep everything autoloadable."
},
"production_insight": "If you use classmap for your application code, you must run composer dump-autoload every time you add or remove a class file. Many CI pipelines forget this step, causing spurious failures. Prefer PSR-4 and rely on --optimize-autoloader for production speed.",
"key_takeaway": "PSR-4 is for modern code (most common). PSR-0 is legacy. Classmap is for non-namespaced code. Files are for global functions. Optimise with classmap in production."
},
{
"heading": "Optimization Flags Reference: Speed Up Autoloading in Production",
"content": "Composer offers several flags that affect autoloader performance and behavior. Knowing which flag does what — and when to use it — can significantly improve production response times and eliminate deployment surprises. The table below lists every relevant flag, its effect, and its recommended use case.\n\nMost of these flags are passed to `composer install` or `composer dump-autoload`. They can also be set globally in your `composer.json` under the `config` key for consistency across environments.\n\n**Critical distinction**: `--optimize-autoloader` and `--classmap-authoritative` are not the same. The first converts PSR-4 to a classmap but still performs `file_exists` checks; the second skips those checks entirely, assuming the map is correct. Skipping checks gives a minor speed boost but risks fatal errors if the classmap is stale (e.g., a class file was deleted but the map wasn't rebuilt). Always rebuild the classmap immediately before deploying when using authoritative mode.",
"code": {
"language": "text",
"filename": "composer-optimization-flags.txt",
"code": "FLAG REFERENCE TABLE: Composer Autoloader Optimization Flags\n\n────────────────────────────────────────────────────────────────────────────────\nFlag | Effect | Use Case\n────────────────────────────────────────────────────────────────────────────────\n--optimize-autoloader (-o) | Converts PSR-4/PSR-0 entries | Production: every deploy.\n | into a classmap. Still runs | Safe default: no stale\n | file_exists on each lookup. | classmap issues.\n────────────────────────────────────────────────────────────────────────────────\n--classmap-authoritative (-a)| Same as -o, plus Composer | High-traffic production.\n | skips file_exists checks. | Ensure CI rebuilds\n | Phreads DIRECTORY_SEPARATOR | classmap on every deploy.\n | | Risk: stale map = crash.\n────────────────────────────────────────────────────────────────────────────────\n--apcu-autoloader | Caches classmap in APCu | Large projects (10K+\n | shared memory. Reduces | classes). Requires\n | disk I/O for class matches. | APCu extension enabled.\n────────────────────────────────────────────────────────────────────────────────\n--no-dev | Excludes autoload-dev entries | Production: essential.\n | from the generated classmap. | Keeps test classes off\n | | production server.\n────────────────────────────────────────────────────────────────────────────────\n--no-scripts | Skips scripts section (e.g., | CI builds where you\n | post-install-cmd). | don't want side effects.\n────────────────────────────────────────────────────────────────────────────────\n--prefer-dist | Downloads package archives | CI: faster than cloning\n | instead of Git clones. | from VCS. Default.\n────────────────────────────────────────────────────────────────────────────────\n\nCOMPOSER.JSON CONFIG (for project-wide defaults):\n{\n \"config\": {\n \"optimize-autoloader\": true,\n \"classmap-authoritative\": false,\n \"apcu-autoloader\": true,\n \"preferred-install\": \"dist\"\n }\n}\n\nNote: setting config keys ensures these flags are applied even when\nsomeone runs 'composer install' without passing the flag.",
"output": "/* Reference table — no direct output */"
},
"callout": {
"type": "warning",
"title": "Stale Classmap: The Silent Killer",
"text": "If you use --classmap-authoritative and then add or remove a PHP file without rebuilding the classmap, your autoloader will return 'class not found'for the newclassand will NOT fall back to PSR-4 scanning. This is by design: authoritative mode assumes the map is complete. Always rebuild before deploy."
},
"production_insight": "For most applications, --optimize-autoloader alone gives 95% of the benefit with zero risk. Only enable --classmap-authoritative if you have measured a bottleneck in file_exists calls and your deploy pipeline guarantees a fresh autoload dump every time. APCu autoloading is beneficial when you have a high number of classes (>5,000) and are on a single server; it becomes less useful on containerised (e.g., Docker) environments where APCu memory is per-container.",
"key_takeaway": "Use --optimize-autoloader on every production deploy. Consider --classmap-authoritative only with bulletproof CI rebuilds. APCu autoloading helps large projects. Always use --no-dev in production."
},
{
"heading": "Common Autoloading Errors and How to Fix Them",
"content": "Even experienced developers hit autoloading issues regularly. Most boil down to a handful of causes. Here are the most common errors, their symptoms, and the exact fix.\n\n1. Class not found after adding a new file.\n - Symptom: Fatal error: Class 'App\\\Services\\\NewFeature' not found.\n - Likely cause: The file exists but the namespace declaration doesn't match the directory path. For example, the file is at src/Services/NewFeature.php but declares namespace App\\\Models (should be App\\\Services).\n - Fix: Open the file and check the namespace statement. It must be identical to the path relative to the PSR-4 root minus the filename.\n\n2. Case mismatch between filename and class name.\n - Symptom: Works on Mac/Windows, fails on Linux.\n - Likely cause: Class is PaymentGateway but the file is paymentgateway.php. On case-insensitive filesystems, the autoloader finds it; on Linux it doesn't.\n - Fix: Rename the file to match the class name exactly. Add a CI check that validates filename matches classname.\n\n3. Forgetting to run composer dump-autoload after classmap changes.\n - Symptom: A class in a classmap directory is not found after being added.\n - Fix: Run composer dump-autoload (or composer du). ForPSR-4 this isn't needed, but for classmap it's mandatory.\n\n4. Composer.json syntax error (single backslash).\n - Symptom: Composer validate fails, orPSR-4 entries silently ignored.\n - Fix: Alwaysuse double backslashes in \"psr-4\" keys. Run composer validate after edits.\n\n5. Class is in autoload-dev but production uses --no-dev.\n - Symptom: Works in development, fails on production with \"Class not found\".\n - Fix: Move the class from autoload-dev to autoload if it's needed in production. Or ensure production deployment doesn't exclude dev dependencies if the class is required.\n\n6. Theclass belongs to a package that wasn't installed with composer require.\n - Symptom: After git pull, a newclass is missing.\n - Fix: Run composer install --no-dev to sync vendor/ with composer.lock.\n\n7. PHP version mismatch causing opcache to cache old autoloader.\n - Symptom: After updating autoload configuration, the old class map is still used.\n - Fix: Clear opcache with opcache_reset() or restart PHP-FPM. Also run composer dump-autoload.",
"code": {
"language": "php",
"filename": "debug-autoloading.php",
"code": "<?php\n/**\n * Quick autoloading debug script.\n * Place this in your project root and run: php debug-autoloading.php\n */\n\nrequire_once __DIR__ . '/vendor/autoload.php';\n\n// List all registered autoloaders\n$autoloaders = spl_autoload_functions();\necho \"Registered autoloaders: \" . count($autoloaders) . PHP_EOL;\nforeach ($autoloaders as $i => $loader) {\n echo \" #{$i}: \" . (is_array($loader) ? get_class($loader[0]) . '->' . $loader[1] : (is_string($loader) ? $loader : 'Closure')) . PHP_EOL;\n}\n\n// Check if a specific class is autoloadable\n$className = $argv[1] ?? 'App\\\\\Models\\\\\Invoice'; // Change to your class\n$exists = class_exists($className);\necho PHP_EOL;\necho \"Class '{$className}' exists: \" . ($exists ? 'YES' : 'NO') . PHP_EOL;\n\n// If the class exists, show its file path\nif ($exists) {\n $reflection = new ReflectionClass($className);\n echo \"Defined in: \" . $reflection->getFileName() . PHP_EOL;\n}\n\n// Dump PSR-4 prefixes from Composer's generated file\n$psr4File = __DIR__ . '/vendor/composer/autoload_psr4.php';\nif (file_exists($psr4File)) {\n $psr4 = require $psr4File;\n echo PHP_EOL . \"PSR-4 prefixes (from autoload_psr4.php):\" . PHP_EOL;\n foreach ($psr4 as $prefix => $dirs) {\n echo \" {$prefix} => \" . implode(', ', $dirs) . PHP_EOL;\n }\n}",
"output": "Registered autoloaders: 1\n #0: Closure\n\nClass 'App\\\\\Models\\\\\Invoice' exists: YES\nDefined in: /var/www/src/Models/Invoice.php\n\nPSR-4 prefixes (from autoload_psr4.php):\n App\\\\\ => /var/www/src/\n Carbon\\\\\ => /var/www/vendor/nesbot/carbon/src/Carbon/"
},
"callout": {
"type": "warning",
"title": "Opcache Can Hide Autoloader Changes",
"text": "PHP's opcache caches compiled PHP files. If you update composer.json or add new files, the old autoloader might remain cached. Always clear opcache after any autoloader change. In a CLI script, use opcache_reset(). For web requests, restart PHP-FPM or the web server."
},
"production_insight": "The most costly autoloading error in production is the case-insensitive filesystem mismatch. It's silent until deployment. Prevent it by developing inside a Linux container (Docker) with a case-sensitive filesystem. Add a CI step that runs the debug script above and asserts every expected class is loadable.",
"key_takeaway": "Most autoloading errors fall into six categories: namespace mismatch, case-sensitivity, forgotten dump-autoload, composer.json syntax, autoload-dev exclusion, and package not installed. Debug with spl_autoload_functions() and the generated autoload_psr4.php file."
},
{
"heading": "Cross-Platform Case Sensitivity Checklist: Avoiding Linux Deployment Failures",
"content": "The number one cause of 'class not found' in production is a filename case mismatch that works on Mac/Windows but breaks on Linux. Use this checklist during development, code review, andCI to eliminate this class of bug entirely.\n\n**Why it happens**: Mac's APFS (Apple File System) defaults to case-insensitive — it treats 'PaymentGateway.php' and 'paymentgateway.php' as the same file. Windows NTFS is also case-insensitive. Linux ext4, XFS, and Btrfs are case-sensitive. Composer's autoloader performs an exact string match on the file path; if the namespace says 'PaymentGateway' but the file is lowercase, the `file_exists` check returns false on Linux.\n\n**The fix is not just renaming files**. It's about building a process that prevents mismatches from reaching production. The checklist below covers every step from local setup to CI enforcement.",
"code": {
"language": "text",
"filename": "case-sensitivity-checklist.md",
"code": "## Cross-Platform Case Sensitivity Checklist\n\n### 🚀 Development Environment\n- [ ] Use Docker with a Linux image (php:8.2-cli, etc.) for all development.\n- [ ] Mount the project into the container — the container's filesystem is case-sensitive.\n- [ ] Never rely on the host Mac/Windows filesystem for autoloading tests.\n- [ ] Set `export COMPOSER_DISABLE_FILE_CHECK=1` is irrelevant; just test on Linux.\n\n### 📝 Code Conventions\n- [ ] Class names must be PascalCase (e.g., `InvoiceService`).\n- [ ] Filenames must exactly match the class name + .php (e.g., `InvoiceService.php`).\n- [ ] Namespace segments must match directory names exactly (case-sensitive).\n- [ ] Prohibit underscores in filenames (use PSR-4, not PSR-0).\n- [ ] Enforce with PHPStan rule: `phpstan/phpstan-php-parser` can validate class names.\n\n### 🔄 Git Workflow\n- [ ] Enable case-sensitive Git checks: `git config core.ignorecase false` in repo.\n- [ ] Add a pre-commit hook that runs `find . -name '*.php' | while read f; do ...` to ensure basename matches classname.\n- [ ] Review diffs for file renames that only change case (Git may miss them without config).\n\n### 🤖 CI Pipeline (GitHub Actions Example)\n```yaml\n- name: Check file names match class names\n run: |\n find src -name '*.php' | while read file; do\n classname=$(basename \"$file\" .php)\n grep -q \"class $classname\" \"$file\" || echo \"ERROR: $file classname mismatch\"\n done\n```\n- [ ] Also run `composer validate --strict` to catch JSON syntax errors.\n- [ ] Run `composer dump-autoload` as part of CI and check exit code.\n- [ ] Use a matrix build on both Ubuntu (case-sensitive) and macOS (insensitive) to catch early.\n\n### 🚢 Deployment\n- [ ] Deploy using a Docker image built from a Linux base.\n- [ ] Run `composer install --no-dev --optimize-autoloader` during image build.\n- [ ] Verify classmap is generated: check `vendor/composer/autoload_classmap.php` for your classes.\n- [ ] Never deploy from a non-Linux machine directly; always use CI to build.\n\n### 🐛 Quick Recovery\n- [ ] If a production class is missing, SSH into the server and run:\n `find /path/to/app -name '*.php' | xargs grep -l 'class PaymentGateway'`\n- [ ] Compare the actual filename with the expected class name from the error.\n- [ ] Rename file, update any imports (rare), rebuild autoloader, and redeploy.","output": "/* Checklist reference — apply to your workflow */"
},
"callout": {
"type": "warning",
"title": "Git on Case-Insensitive Systems",
"text": "If you rename a file from 'paymentgateway.php' to 'PaymentGateway.php' on MacorWindows, Git may treat it as the same file (since the filesystem is case-insensitive). You must use `git mv` or enable `core.ignorecase false` in your Git config. Otherwise, the lowercase filename will remain in the repository andbreak on Linux."
},
"production_insight": "The largest Laravel projects I've audited had at least one case-sensitivity bug in the codebase — they only worked because the dev team all used Macs. The fix is always the same: enforce a case-sensitive environment from day one. Docker is not optional; it's a production-safety tool. Add the CI check that compares filename to class name; it's a five-line shell script that saves hours of debugging.",
"key_takeaway": "Case-sensitivity bugs are preventable entirely by developing inside a Linux container, enforcing naming conventions in CI, and using Git with case-sensitive tracking. No amount of runtime debugging beats a detector that prevents the bug from landing."
},
{
"heading": "Automating Tasks with Composer Scripts (post-install-cmd, post-update-cmd)",
"content": "Composer's scripts feature allows you to run arbitrary PHP commands or shell scripts at specific lifecycle events. The most common hooks are: post-install-cmd (after composer install), post-update-cmd (after composer update), and pre-autoload-dump (before generating the autoloader). These are defined in the scripts section of composer.json.\n\nA real use case: after installing dependencies (composer install), you might want to clear the application cache, generate fresh configuration files, or copy assets. By hooking into post-install-cmd, this happens automatically without manual steps. Another common use is running database migrations after an update.\n\nYou can specify multiple callbacks by listing them in an array. Each callback can be a PHP class method (using a static callable) or a shell command prefixed with @ (e.g., @php some-script.php). Composer also provides a set of predefined scripts like @php which points to the PHP binary, or @composer for the Composer binary itself.\n\nA critical best practice: keep scripts idempotent — they should produce the same result whether run once or multiple times. Also, avoid long-running scripts that could timeout or fail silently. For production, you typically run scripts only on deploy, not on every install in CI.\n\nThe pre-autoload-dump event is particularly interesting for autoloading. Some packages use it to generate class maps or metadata that the autoloader then includes. If you're building a package that needs to hook into the autoloader generation, this is the event to use.",
"code": {
"language": "php",
"filename": "composer.json",
"code": "{\n \"name\": \"theforge/invoice-app\",\n \"description\": \"Invoicing application with automatic post-install tasks\",\n \"require\": {\n \"php\": \">=8.1\",\n \"nesbot/carbon\": \"^3.0\"\n },\n \"autoload\": {\n \"psr-4\": {\n \"App\\\\\": \"src/\"\n }\n },\n \"scripts\": {\n \"post-install-cmd\": [\n \"@php artisan cache:clear\",\n \"@php artisan config:cache\"\n ],\n \"post-update-cmd\": [\n \"@php artisan migrate --force\",\n \"App\\\\\Maintenance\\\\\RunAfterUpdate::execute\"\n ],\n \"pre-autoload-dump\": [\n \"@php generate-classmap.php\"\n ],\n \"custom-script\": [\n \"php -r 'echo \\\"Hello, world!\\\\n\\\";'\"\n ]\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// EXAMPLE: App\\\\\Maintenance\\\\\RunAfterUpdate (static class method)\n// ─────────────────────────────────────────────────────────────────────────────\n\n<?php\nnamespace App\\\\\Maintenance;\n\nclass RunAfterUpdate\n{\n public static function execute(): void\n {\n echo \"Running post-update tasks...\" . PHP_EOL;\n // e.g., clear cache, send notification, generate docs\n }\n}","output": "Running post-update tasks...\n(and any output from the artisan commands)"
},
"callout": {
"type": "tip",
"title": "Using @php Instead of Hardcoding php Binary Path",
"text": "Always use @php instead of writing php directly. @php uses the same PHP binary that Composer is running with. This prevents version mismatches when you have multiple PHP installations on your system."
},
"production_insight": "Avoid running heavy scripts in post-update-cmd during production deployment. Instead, use a dedicated deploy script that runs after the lock file is installed. Use post-install-cmd for lightweight tasks like clearing a writable cache directory. Never put destructive operations (like database schema changes) into a script that runs on every composer install — use a controlled deployment pipeline instead.",
"key_takeaway": "Composer scripts automate tasks triggered by lifecycle events. post-install-cmd runs after every install/update. Use callables or @php commands. Keep scripts idempotent and production-safe."
}
]
● Production incidentPOST-MORTEMseverity: high
The Case-Sensitive Server That Broke Every Deployment
Symptom
After deploying a new feature that introduced several service classes, the production server returned white screens with 'Fatal error: Class App\Services\PaymentGateway not found'. The files existed in src/Services/paymentgateway.php (lowercase filename) but the class was declared as 'PaymentGateway'.
Assumption
The developer assumed Composer's PSR-4 autoloader would find the class regardless of filename case — after all, it worked on their Mac with a case-insensitive filesystem.
Root cause
Composer's autoloader performs a simple namespace-to-path transformation plus a file_exists check. On case-sensitive Linux filesystems, 'src/Services/PaymentGateway.php' does not match 'src/Services/paymentgateway.php'. The autoloader returns false, and PHP throws a fatal error. The Mac's case-insensitive APFS masked the discrepancy for months.
Fix
Renamed all files to match the class name exactly: PaymentGateway.php, InvoiceMailer.php, etc. Added a CI pipeline step that runs 'composer dump-autoload' and checks for any mismatches using 'find src -name '*.php' | while read f; do basename "$f" .php; done | ...'. Also added a PHPStan rule to enforce PSR-4 naming.
Key lesson
Always develop inside a Linux container (Docker) to catch case-sensitivity issues early.
Never trust autoloading on a case-insensitive filesystem — test on the target OS.
Enforce naming conventions with automated tooling (PHPStan, custom CI checks).
Production debug guideA symptom-to-action grid for the most frequent autoloader failures — no theory, only commands and fixes.5 entries
Symptom · 01
Fatal error: Class 'App\Services\PaymentGateway' not found. File exists.
→
Fix
Check filename case: ls -la src/Services/paymentgateway.php vs src/Services/PaymentGateway.php. On Linux, they're different files.
Symptom · 02
Class from a newly installed package not found.
→
Fix
Run composer dump-autoload to regenerate the autoloader. New packages often add PSR-4 entries that require a refresh.
Symptom · 03
Class from your own application not found after adding a file.
→
Fix
Verify the file declares the correct namespace: the namespace must match the directory path relative to the PSR-4 root. Run composer dump-autoload -o to check logs.
Symptom · 04
Class not found on production but works on dev.
→
Fix
Compare composer.lock between environments. Run composer install --no-dev on production — dev autoloading rules are excluded. Check if the class is in the autoload-dev section.
Symptom · 05
Class not found, but vendor/autoload.php is required.