If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
Have you ever heard of an autoloader? Even if you have, you might not know what they do or how they work.
Autoloaders change everything. In PHP, you can't reference a class or a function
unless you - or someone - requires or includes that file first. That's why - in
bootstrap.php
- we have a require
statement for every file:
... lines 1 - 8 | |
require_once __DIR__.'/lib/Service/Container.php'; | |
require_once __DIR__.'/lib/Model/AbstractShip.php'; | |
require_once __DIR__.'/lib/Model/Ship.php'; | |
require_once __DIR__.'/lib/Model/RebelShip.php'; | |
require_once __DIR__.'/lib/Model/BrokenShip.php'; | |
require_once __DIR__.'/lib/Service/BattleManager.php'; | |
require_once __DIR__.'/lib/Service/ShipStorageInterface.php'; | |
require_once __DIR__.'/lib/Service/PdoShipStorage.php'; | |
require_once __DIR__.'/lib/Service/JsonFileShipStorage.php'; | |
require_once __DIR__.'/lib/Service/ShipLoader.php'; | |
require_once __DIR__.'/lib/Model/BattleResult.php'; |
Without these, we can't access the classes inside of them.
This is no bueno: it means that I have to remember to add another line here, whenever I create a new class. You know why else it's not good? Suppose I don't use all of these classes during some requests? Well, right now, I'm loading every class into memory, even if we never need to use them. This is actually slowing down my app!
Well, guess what: in modern PHP, you never see require
or include
statements.
They're gone. How is that possible? The Answer is: autoloaders.
First, kill the BattleManager.php
require
statement:
... lines 1 - 12 | |
require_once __DIR__.'/lib/Model/BrokenShip.php'; | |
require_once __DIR__.'/lib/Service/ShipStorageInterface.php'; | |
... lines 15 - 19 |
Not surprisingly, we get an error:
Class
Battle\BattleManager
not found.
How do we fix this? The answer is by calling a very special function from the core
of PHP called spl_autoload_register()
. Pass this a single argument: a function
with a $className
argument. We'll use an anonymous function:
... lines 1 - 2 | |
spl_autoload_register(function($className) { | |
... lines 4 - 10 | |
}); | |
... lines 12 - 29 |
Here's the deal: as soon as you call spl_autoload_register
, right before PHP throws
the dreaded "class not found" error like this, it will call our function and pass
it the class name. Then, if we - somehow - can locate the file that contains this
class and require it, PHP will continue on like normal with no error.
In fact, in modern PHP development, this is how every single class is loaded. In some cases, this little function is called hundreds of times on every request.
Let's start coding our autoloader with some simple logic: if ($className == 'Battle\BattleManager')
,
then we know where that file lives. require __DIR__.'/lib/Service/BattleManager.php
.
Then, add a return
:
... lines 1 - 2 | |
spl_autoload_register(function($className) { | |
if ($className == 'Battle\BattleManager') { | |
require __DIR__.'/lib/Service/BattleManager.php'; | |
return; | |
} | |
// we don't support this class! | |
}); | |
... lines 12 - 29 |
We're done!
For now, if the autoloader function is called for any other class, we'll do nothing. PHP will throw its normal "class not found" error.
With just that, refresh. Mind blown. We just got our app to work without manually
requiring the BattleManager.php
file. Of course, right now, this isn't much better
than having a require
statement. Actually, it's more work.
How could we make this function smarter? How could we make it automatically find new classes and files as we add them to the system?
Well I have an idea. BattleManager
lives in the Service
directory. What if we
changed its namespace to match that? Or to get crazier, what if we gave every class
a namespace that matches its directory?
If we did that, the autoload function could use the namespace to locate any file.
The class - Service\BattleManager
would live at Service/BattleManager.php
.
It's brilliant!
... lines 1 - 2 | |
namespace Service; | |
class BattleManager | |
... lines 6 - 94 |
Now that we've changed the namespace to Service,
we need to update any references
to BattleManager
- like in index.php
. Change the use
statement to Service
.
... lines 1 - 2 | |
use Service\BattleManager; | |
... lines 4 - 143 |
Yes!
Finally, in bootstrap.php
, instead of manually checking for just this one class,
say that the path is always equal to __DIR__/lib/
then str_replace()
- we'll
replace the back slash with a forward slash:
... lines 1 - 2 | |
spl_autoload_register(function($className) { | |
$path = __DIR__.'/lib/'.str_replace('\\', '/', $className).'.php'; | |
... lines 5 - 10 | |
}); | |
... lines 12 - 29 |
Notice I put two back-slashes. Since this is the escape character, if you only
have one, it looks like you're escaping the next quote character. So, to get one slash,
we need to use two - it's an ugly detail. Anyways, replace backslashes with a forward slash
and pass that the class name. Finally, add the .php
at the end.
Just in case, check to see if that file exists. If it does, require $path
:
... lines 1 - 2 | |
spl_autoload_register(function($className) { | |
$path = __DIR__.'/lib/'.str_replace('\\', '/', $className).'.php'; | |
if (file_exists($path)) { | |
require $path; | |
} | |
// we don't support this class! | |
}); | |
... lines 12 - 29 |
That's it. Go back, refresh, and... everything still works.
And now, we are incredibly dangerous. We can now get rid of every single require
statement really easily. Let's do it!
Hey Serge,
Thanks for mentioning DIRECTORY_SEPARATOR constant. It could be useful in some cases, e.g. some functions return path with specific slash ("/" or "\") depends on OS and that's the case where that constant could be useful if you want to explode the path etc. But Windows already understand both slashes.
Cheers!
I am not sure, but I think the autoloader won't work on Windows because this system uses backslashes ('\'), not slashes ('/') in its path.
To fix this you can replace '/' in the code with `DIRECTORY_SEPARATOR` PHP constant, that will give you a relevant separator for your system.
Hope it helps someone!