Buy

Have you ever heard of an autoloader? Even if you have, you might not know what they do or how they work.

What does an Autoloader Do?

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:

20 lines bootstrap.php
... 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:

19 lines bootstrap.php
... 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.

Adding your Autoloader

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:

29 lines bootstrap.php
... 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.

Making your Autoloader Work (a little)

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:

29 lines bootstrap.php
... 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.

Creating a Smarter Autoloader

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!

94 lines lib/Service/BattleManager.php
... 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.

143 lines index.php
... 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:

29 lines bootstrap.php
... 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:

29 lines bootstrap.php
... 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!

Leave a comment!

  • 2017-10-16 Serge Boyko

    No problem! Thanks for clarifying the issue.

  • 2017-10-16 Victor Bocharsky

    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!

  • 2017-10-13 Serge Boyko

    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!