Why Casing Matters with PHP Autoloaders
Contrary to popĀuĀlar beĀlief, auĀtoloadĀing isĀnāt magĀiĀcal. There are well deĀļ¬ned rules for how your class, trait, or inĀterĀface deĀfĀiĀnĀiĀtions are swooped in.
My purĀpose here is help you unĀderĀstand the baĀsics beĀhind auĀtoloadĀing a litĀtle betĀter. Iām asĀsumĀing you have some prior knowlĀedge of PHP. It will be helpĀful if youāve used Composer as well, but Iāll try to exĀplain things as we go.
In my opinĀion, knowĀing how to write an auĀtoloader isĀnāt that imĀporĀtant, so Iām not goĀing to cover that in great deĀtail here. Rather, I want peoĀple to be faĀmilĀiar with the auĀtoloadĀing stanĀdards and some gotchas. It might seem silly at ļ¬rst, but I hope you gain a betĀter grasp of what PHP is acĀtuĀally doĀing.
Letās do this without the autoloader
Back in the days of yore, PHP didĀnāt have an auĀtoloader. People had to get their hands dirty by manĀuĀally inĀcludĀing their ļ¬lesāor maybe they put everyĀthing in one big script (I see you). These arenāt the best opĀtions. But for next couĀple of exĀamĀples, itās what weāre goĀing to work with.
First letās just creĀate a class deĀfĀiĀnĀiĀtion and then use it. Scared yet?
<?php
namespace Tutorial;
class WordPrinter
{
public function print(): void
{
echo 'Hello World!';
}
}
$printer = new \Tutorial\WordPrinter();
$printer->print();
Some unĀnecĀesĀsary boilĀerĀplate later, we have manĀaged to creĀate a āHello Worldā exĀamĀple. I want to foĀcus on the class deĀfĀiĀnĀiĀtion part. The class name is \Tutorial\WordPrinter
. Pay atĀtenĀtion to the casĀing. Notice how the class refĀerĀence (the part where we newed up the inĀstance) is cased exĀactly the same. Does that matĀter? What hapĀpens if you add this to the end of ļ¬le?
$printer2 = new \TuToRiAl\WoRdPrInTeR();
$printer2->print();
It prints āHello World!ā twice. And this reĀsult shouldĀnāt be surĀprisĀing at all. Classnames in PHP are case-inĀsenĀsiĀtive. There is nothĀing crazy goĀing on here yet.
Hereās anĀother curve ball. Add this code to the end of the ļ¬le.
(function() {
$printer = new \Tutorial\WordPrinter();
$printer->print();
})();
We get three āHello World!ā texts. Cool but whatās the point here. It turns out class, trait, and inĀterĀface deĀfĀiĀnĀiĀtions are alĀways global. Say it with me kids! We can acĀcess the class deĀfĀiĀnĀiĀtion inĀside the funcĀtion, even though it has a difĀferĀent scope. We get a new $printer variĀable, but the same old \Tutorial\WordPrinter
class.
This conĀcept is obĀviĀous if we inĀverse the sitĀuĀaĀtion.
<?php
namespace Tutorial;
(function() {
class WordPrinter
{
public function print(): void
{
echo 'Hello World!';
}
}
})();
$printer = new \Tutorial\WordPrinter();
$printer->print();
We get a āHello World!.ā The class deĀfĀiĀnĀiĀtion is not reĀstricted to the funcĀtionās scope. Once again, this also apĀplies to trait and inĀterĀface deĀfĀiĀnĀiĀtions.
PHP keeps taĀbles mapĀping class, inĀterĀface, and trait names to their deĀfĀiĀnĀiĀtions. Each reĀquest gets its own set of taĀbles. A name can only be mapped to sinĀgle deĀfĀiĀnĀiĀtionālike a dicĀtioĀnary data strucĀture. This is why you canāt have a sinĀgle name mapped to mulĀtiĀple deĀfĀiĀnĀiĀtions. PHP will throw an erĀror.
<?php
// This will definitely error. The name Foo can only map to one definition.
class Foo {}
class Foo {}
Includes and reĀquires donāt change our sitĀuĀaĀtion much. Autoloadable (Iām alĀlowed to make up words) deĀfĀiĀnĀiĀtions are global once they are exĀeĀcuted. So we can put our class, traits, and inĀterĀfaces into sepĀaĀrate ļ¬les to keep everyĀthing orĀgaĀnized. Then, tell PHP exĀplicĀitly to bring the ļ¬le in. Hereās a quick exĀamĀple:
<?php
class Foo {}
<?php
require __DIR__ . '/Foo.php';
class Bar extends Foo
{
public static function HelloThere()
{
echo 'Hello There';
}
}
Bar::HelloThere();
Wouldnāt it be cool if PHP knew to drag that ļ¬le in on its own?
Ok, Autoloaders
Surprise! Autoloaders inĀclude or reĀquire in the ļ¬le auĀtoĀmatĀiĀcally. You donāt have to litĀter your code with inĀcludes, and only the classes you need for that reĀquest have to be fetched. This is betĀter by far.
Since PHP verĀsion 5.1.0, you can regĀisĀter an auĀtoloader usĀing the spl_auĀtoloadĀ_regĀisĀter funcĀtion. More than one can regĀisĀteredāthereās a queue of them. I only menĀtion this beĀcause you may need more than one, and their orĀder may matĀter. Here, Iāll be usĀing Composer.
Composer is the packĀage manĀager for PHP. It also will setup an auĀtoloader for us. Very cool. You can downĀload it here.
So when is the auĀtoloader inĀvoked? Itās used nearly everyĀwhere you need acĀcess to a class, inĀterĀface, or trait. Generally, you donāt have think about it. You code should ājust work.ā PHP is smart enough to use the auĀtoloader any time it needs to try a ļ¬nd an auĀtoloadĀable deĀfĀiĀnĀiĀtion. Now, keep this in mind. PHP will not have to check the auĀtoloader if that deĀfĀiĀnĀiĀtion has alĀready been loaded. It augĀments the beĀhavĀior we saw earĀlier. Letās look at a quick exĀamĀple:
<?php
// Assume this class is autoloadable
$foo = new Foo();
// The first time the class is referenced, the autoloader is used.
// The class Foo is now in PHP's class table
var_dump(class_exists('Foo', false));
// The class_exists function by default will use the autoloader to check if the class exists
// By passing false as the second parameter, it won't use the autoloader
// We still get true here! It's in the class table remember
var_dump(class_exists('FOO', false));
// True again! Remember PHP doesn't care about the casing for class names
// But what happens if the autoloader has to find the class FOO
// We'll look at that case in a minute
Once the auĀtoloader is trigĀgered, your regĀisĀtered funcĀtion (or funcĀtions) have to ļ¬nd the ļ¬le that conĀtains the deĀfĀiĀnĀiĀtion. It would be helpĀful if there was some conĀsisĀtent way to map deĀfĀiĀnĀiĀtion names to ļ¬leĀnames. Oh wait, there is!
PSR-0
A comĀmitĀtee called PHP-FIG mainĀtains a list of PHP stanĀdards recĀomĀmenĀdaĀtions (PSRs). Since proĀgramĀmers like countĀing from zero, the ļ¬rst recĀomĀmenĀdaĀtion is PSR-0, and it inĀvolves how to name classes to play nice with the auĀtoloader. You can read the stanĀdard itĀself for all the gloĀriĀous deĀtails, but the idea is simĀple. We are goĀing to map nameĀspace sepĀaĀraĀtors to the diĀrecĀtory sepĀaĀraĀtor for the curĀrent opĀerĀatĀing sysĀtem. Composer supĀports this out of the box.
{
"autoload": {
"psr-0": {"Tutorial\\": "src/"}
}
}
Run a php composer.phar dumpautoload
for Composer to genĀerĀate the auĀtoloader.
Letās get our WordPrinter class to play nice with the auĀtoloader. For it to recĀogĀnize our class, weāll have to name it to match the the preĀļ¬x proĀvided in the conĀļ¬g. And Iām goĀing to emĀphaĀsize: you have to match the casĀing of the preĀļ¬x. Composer is case-senĀsiĀtive. We alĀready have a Tutorial nameĀspace deĀclared, so weāre good.
Where should this ļ¬le be placed? Before you could put ļ¬les wherĀeverāas long as the inĀclude paths were corĀrect. Now the nameĀspace maps to a path. The path is alĀways relĀaĀtive to the loĀcaĀtion of the comĀposer.json. Youāll match the nameĀspace to enĀtry in the comĀposer.json and take the corĀreĀspondĀing path part. Then ļ¬ip all the nameĀspace sepĀaĀraĀtors to diĀrecĀtory sepĀaĀraĀtors and slap a .php to the end. So youāll put the ļ¬le here.
/path/to/composerJson/src/Tutorial/WordPrinter.php
The Tutorial nameĀspace maps to the Tutorial folder. The casĀing of the folder should match the casĀing of the nameĀspace. You have to do this on a case-senĀsiĀtive ļ¬lesysĀtem. When straight up usĀing the PSR rules, Composer has to look up things in the ļ¬lesysĀtem at runĀtime.
Eww. I know. But thereās a ļ¬x: classmaps. Weāll look at these shortly.
PSR-4
As time went on, peoĀple reĀalĀized that forcĀing the nameĀspaces to diĀrectly map to the path was inĀconĀveĀnient. It doesĀnāt play as nice with how Composer hanĀdles packĀages. You can read more about this here. Anyway, a new stanĀdard was proĀposed for auĀtoloadĀing: PSR-4. Itās not too much difĀferĀent on the surĀface of things. We can have the exĀact same diĀrecĀtory strucĀture as beĀfore.
{
"autoload": {
"psr-4": {"Tutorial\\": "src/Tutorial/"}
}
}
You should noĀtice that we had to diĀrectly map the Tutorial nameĀspace to the Tutorial folder in the comĀposer.json. But we choose to do this! It wasĀnāt forced on us. We could map the nameĀspace to any folder.
{
"autoload": {
"psr-4": {"Tutorial\\": "src/AnotherFolder/"}
}
}
And thatās pretty much it. Itās kinda PSR-0, exĀcept the preĀļ¬x doesĀnāt have to match the folder. You can imagĀine we reĀmoved the preĀļ¬x from the class name, apĀpended that preĀļ¬xās path to our exĀistĀing base path, and did PSR-0 the rest of the way.
Letās creĀate an exĀamĀple usĀing the above comĀposer.json. All the classes in the Tutorial nameĀspace have to be in the AnotherFolder diĀrecĀtory (or one its subĀdiĀrecĀtoĀries). The part of the nameĀspace that doesĀnāt match the preĀļ¬x in the comĀposer.json has to map to the ļ¬lesysĀtem.
Letās walk through this process.
The Tutorial part of the class name matches our preĀļ¬x, so weāre goĀing to āremoveā it for now.
That leaves us with \Factory\SomethingFactory.php
.
Extend our comĀposer.json base path with the path corĀreĀspondĀing to the preĀļ¬x, like so:
/path/to/composerJson/src/AnotherFolder
And then PSR-0 the rest of the class name, adding it to the end of base path.
/path/to/composerJson/src/AnotherFolder/Factory/SomethingFactory.php
Ta-da! We mapped the class name to a path. Similar to PSR-0, Composer can look up the loĀcaĀtions of ļ¬les at runĀtime usĀing the PSR-4 rules. Yet, thatās slow. Letās ļ¬x that with classmaps.
Classmap
Composer ofĀfers a third way to auĀtoloadāand this way is techĀniĀcally the simĀplest. We can genĀerĀate a classmap. Itās simĀply a key value mapĀping. The key is the auĀtoloadĀable item, like a class, inĀterĀface, or trait. The value is the ļ¬le path. Composer will put the mapĀping in the its genĀerĀated venĀdor/ācomĀposer folder. Look for the auĀtoloadĀ_Āclassmap.php ļ¬le. It will look someĀthing like this:
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'Tutorial\\Factory\\SampleFactory' => $baseDir . '/src/AnotherFolder/Factory/SampleFactory.php',
'Tutorial\\WordPrinter' => $baseDir . '/src/AnotherFolder/WordPrinter.php',
);
You can conĀvert your PSR-0 and PSR-4 rules in Composer to classmap rules by passĀing the āoptimized ļ¬ag to the inĀstall or dumpauĀtoload comĀmand. So someĀthing like php composer.phar dumpautoload --optimized
.
You can also list diĀrecĀtoĀries in the classmap secĀtion of the comĀposer.json, but this isĀnāt recĀomĀmended. It will force you to dumpauĀtoload anyĀtime you add a new ļ¬leāComĀposer wonāt use the PSR rules. If you use the PSR rules, Composer can look up where the ļ¬le is on the ļ¬y as alĀready menĀtioned. This beĀhavĀior is conĀveĀnient in deĀvelĀopĀment enĀviĀronĀmentsābut itās too slow for proĀducĀtion. You should alĀways creĀate the opĀtiĀmized classmap for proĀducĀtion enĀviĀronĀments. It is worth adding some time to your build.
When Composerās auĀtoloader is trigĀgered, it will try to look up the given name in the classmap ļ¬rst (or alĀways if an auĀthoĀrĀaĀtive classmap is used). It has to exĀactly match. Casing matĀters here! What does this mean? The casĀing of your class refĀerĀences have to match the casĀing of your deĀfĀiĀnĀiĀtions for auĀtoloadĀing to conĀsisĀtently work.
Technically, the casĀing only matĀters on ļ¬rst refĀerĀence. As alĀready menĀtioned, once the deĀfĀiĀnĀiĀtion is loaded the name is added to one of PHPās taĀbles. If itās in the table, the auĀtoloader is never trigĀgered. It can simĀply use the deĀfĀiĀnĀiĀtion, and the key lookup there is case-inĀsenĀsiĀtive. We alĀready showed that in a preĀviĀous exĀamĀple. Yet, Composerās auĀtoloader is case-senĀsiĀtive (in slightly difĀferĀent ways deĀpendĀing on if PSR-0, PSR-4, or the classmap is used). For everyĀthing to ājust alĀways work,ā you should match the casĀing of your refĀerĀences to the casĀing of the deĀfĀiĀnĀiĀtions. And the casĀing of the deĀfĀiĀnĀiĀtion name should match up to the ļ¬le path casĀings. Thereās lots of casĀing to pay atĀtenĀtion to here! Luckily, PHP should crash and burn if you get it wrong, so at least it should be catchĀable with careĀful testĀing.