Our bootstrap file opens with 47 require_once calls. I counted them on Monday, while a class that plainly existed refused to load, to find out how much of the mess was mine. Below those 47 lines the real load order lives in the includes directory, in roughly 300 more calls, one of which sits inside an if block that only runs for the warehouse module.
The ERP started as a scheduling tool and grew sideways for years. Production runs PHP 5.4, my machine runs 5.5.13, released on 29 May. There is also a PEAR install under /usr/share/pear, sitting on the include_path, which nobody has upgraded since somebody put it there.
Two loading strategies in one repository
The newer CRM screens are a Symfony 2 app in the same repository, started before I arrived, and that half came with a working vendor/autoload.php from day one. So we had two worlds. On one side, classes appear when you name them. On the other, a human maintains the order in which files are read.
The failure mode never varies. You add a class, you forget the require, and you get Fatal error: Class 'Report_Invoice_Aging' not found in /var/www/app/includes/report.php on line 61. Or you add it in the wrong place, a class extends a parent that has not loaded yet, and the message names a different class than the one you broke. I once put count(get_included_files()) at the end of a request: 181 files on a page that used maybe twenty of them.
Classmap first, PSR-4 for anything new
PSR-4 was moved from proposed to accepted on 3 December 2013, and Composer picked it up in 1.0.0-alpha8, tagged 6 January 2014, in the same release that deprecated target-dir. The timing matters for a codebase like ours, because PSR-4 drops the two parts of PSR-0 that hurt here: the namespace prefix no longer has to be repeated in the directory path, and the _ character in a class name stops meaning a directory separator.
Our old classes are called things like Report_Invoice_Aging and live under includes/report/, which matches no standard at all, so we did not try to make it match one. Per the schema documentation, Composer’s classmap scans every .php and .inc file in the directories you hand it and writes what it finds to vendor/composer/autoload_classmap.php. It has no opinion about naming. That is the whole bridge.
{
"require": {
"php": ">=5.4",
"phpoffice/phpexcel": "1.8.*"
},
"autoload": {
"psr-4": {
"App\": "src/App/"
},
"classmap": [
"includes/",
"legacy/"
],
"files": [
"includes/functions.php"
]
}
}
The rule we settled on is that anything we open for a real reason gets a namespace and moves under src/App/, and everything else stays in the classmap under its old name. Four months in, the classmap holds 214 classes, down from 388, and there are 96 under App. The classmap has to be rebuilt whenever a legacy class is added or renamed, which is what composer dump-autoload is for and which people forget. PSR-4 code needs no rebuild at all. That is the argument for moving, and it is a better argument than tidiness.
Deploys run composer install --no-dev --optimize-autoloader. The CLI documentation is blunt about what --optimize does: it converts the PSR-0 and PSR-4 rules into a classmap, it is recommended for production, and it is not the default because it takes a while to run. On our tree it takes about four seconds and removes the directory probing from every request.
The class that would not load
The class from the first paragraph was AppErpInvoiceNumbering, in src/App/ERP/Invoice/Numbering.php. It loaded on my machine and fatalled on the staging box. The oldest bug in the book, and it still cost me an afternoon.
The specification says it in capitals: the subdirectory name MUST match the case of the sub-namespace names, and the file name MUST match the case of the terminating class name. My laptop’s filesystem does not care about case. The staging box is ext4, which does. And the optimized autoloader had been hiding the mistake for two weeks, because a classmap records the path it actually found and never reconstructs one from a namespace. Production was fine. It broke for the one developer who ran plain composer dump-autoload and then loaded the page.
Renaming ERP to Erp then took two commits, because git on a case-insensitive filesystem does not notice a rename that only changes case. Rename to a third name, commit, rename back, commit. I have started grepping for directories whose case does not match the namespace before I trust a green page.
The vendor directory, and the argument about the lock file
The vendor directory genuinely worried people here, and I do not think that was silly. It is a few thousand files nobody in the room wrote, arriving at deploy time, on a server with no outbound internet. We committed it for the first six weeks, 41 MB of it, and every pull request became unreadable. Now Composer runs on the staging box and we rsync the result, which is not elegant and has not failed yet.
composer.lock is committed, and the documentation’s reason is the one we arrived at independently: everyone ends up installing the exact same versions. What took longer to land is that install and update are not two words for one action. Someone ran composer update to add a single package, the lock file moved 19 packages forward including two Symfony components, and form rendering in the CRM changed in a way nobody had asked for. Servers run install. update is a deliberate act, with the package named, by a person who then reads the diff of the lock file.
One PEAR package is still on the include_path, because a payroll report writes an old XLS variant I have not tested PHPExcel against yet. The payroll module also still owns a 34 file require chain of its own. Nobody touches payroll in the same quarter as a cutover, so the include_path stays in php.ini for at least two more releases.
Sources
- PSR-4: Autoloader: the mapping rules, and the requirement that directory and file names match the case of the namespace and class.
- PSR-0: Autoloading Standard: the older convention, including the
_character in a class name acting as a directory separator. - fig-standards commit history: the commit moving PSR-4 from proposed to accepted, dated 3 December 2013.
- Composer CHANGELOG: 1.0.0-alpha8 (6 January 2014) added
psr-4support and deprecatedtarget-dir;dump-autoload --optimizegoes back to 1.0.0-alpha5. - Composer CLI documentation: what
--optimizeconverts, and why it is not enabled by default. - The composer.json schema: the
psr-4,classmapandfilesautoload keys and the generated files they produce. - Composer basic usage: why the lock file belongs in version control and what
installdoes thatupdatedoes not. - PHP release archive for 2014: PHP 5.5.13 release date.