Go to homepage

Build your own PSR-4 autoloader

26th January 2021 | 7 minutes

Deprecated: Return type of Illuminate\View\ComponentAttributeBag::offsetExists($offset) should either be compatible with ArrayAccess::offsetExists(mixed $offset): bool, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /Users/pretzelhands/Projects/php/pretzelhands.com/vendor/illuminate/view/ComponentAttributeBag.php on line 236

Deprecated: Return type of Illuminate\View\ComponentAttributeBag::offsetGet($offset) should either be compatible with ArrayAccess::offsetGet(mixed $offset): mixed, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /Users/pretzelhands/Projects/php/pretzelhands.com/vendor/illuminate/view/ComponentAttributeBag.php on line 247

Deprecated: Return type of Illuminate\View\ComponentAttributeBag::offsetSet($offset, $value) should either be compatible with ArrayAccess::offsetSet(mixed $offset, mixed $value): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /Users/pretzelhands/Projects/php/pretzelhands.com/vendor/illuminate/view/ComponentAttributeBag.php on line 259

Deprecated: Return type of Illuminate\View\ComponentAttributeBag::offsetUnset($offset) should either be compatible with ArrayAccess::offsetUnset(mixed $offset): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /Users/pretzelhands/Projects/php/pretzelhands.com/vendor/illuminate/view/ComponentAttributeBag.php on line 270

Deprecated: Return type of Illuminate\View\ComponentAttributeBag::getIterator() should either be compatible with IteratorAggregate::getIterator(): Traversable, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /Users/pretzelhands/Projects/php/pretzelhands.com/vendor/illuminate/view/ComponentAttributeBag.php on line 280

You can find the repository for this project on GitHub

If you're anything like me, you've probably wondered before: "What kind of magic makes Composer go?" - You look at the source code of vendor/autoload.php and see it links to yet another autoloader that's called autoload_real.php Then there's a bunch of hashes and it's confusing and eventually you just accept that Composer is magic and you aren't worthy of its secrets.

Today, let's look at how you can build your own Composer. At least the autoloading part. We won't talk about the seven million other features that Composer provides to us aside from that.

Setting up the project

This time, all you need to bring is any kind of PHP installation. Technically it needs to be at least PHP 5.3, but I very much hope that you aren't running anything that old.

As always, we'll start with what we want our project structure to look like

psr-4/
├─ autoloader.php
├─ app.php
├─ src/
│  ├─ Pretzel/
│  │  ├─ RandomClass.php

autoloader.php is going to be what it says. This file will be roughly equivalent to vendor/autoload.php when using Composer.

app.php is going to be our test file where we include the autoloader and try to use our classes.

src/ will contain our application-specific code. The folder doesn't have to be Pretzel. You pick whatever makes you happy.

Our stated goal is to load every class that is located in src/Pretzel. Each subdirectory constitutes a new namespace. So any file in src/Pretzel/Factory has a namespace of Pretzel\Factory.

The magic spice: spl_autoload_register

All we really need to create an autoloader is that function: spl_autoload_register. You can read more details about it in the PHP documentation.

You call it like this

<?php

spl_autoload_register(function(string $class) {
    // Perform unknowable magic here
});

All you need to pass in is a callable of some kind that receives one argument: a string with a class name. You can register multiple of these callbacks too. This is useful if you want to have a few logically different ways to load classes.

After your function is registered, PHP will call it every time a class is used for the first time. This is usually either when you call new on it, or when you use a static function on it. Let's see that in action.

Interacting with spl_autoload_register

I'll set up my autoloader.php like this:

// autoloader.php

<?php

spl_autoload_register(function(string $class) {
    die($class)
}

For now we'll just print the class name that was desired and exit. We also need a class to load, so let's build a quick PretzelFactory. It returns a pretzel emoji from a static method. It's very fancy.

// src/Pretzel/Factory/PretzelFactory

<?php

namespace Pretzel\Factory;

class PretzelFactory {
    public static function getPretzel(): string
    {
        return '🥨';
    }
}

We need to use that class somewhere. This is what our app.php file is for. We'll have to include our autoloader, pull the class in and then call the static method. Something like this.

// app.php

<?php

require __DIR__ . '/autoloader.php';

use Pretzel\Factory\PretzelFactory;

echo PretzelFactory::getPretzel();

When we run our app.php file, this is the result.

$ php app.php
Pretzel\Factory\PretzelFactory

So we get the fully-qualified class name (FQCN) passed to us. Now PSR-4 states that a FQCN name consists of:

  • A namespace prefix (e.g. Pretzel) that corresponds to a base directory (e.g. src/Pretzel)
  • Zero or more sub-namespaces (e.g. Factory) that correspond to a directory (e.g. src/Pretzel/Factory)
  • One class name corresponding to a PHP file with that same name (e.g. PretzelFactory.php)

Some of you might have realized by now, that our FQCN is essentially a direct path to the file we need to load. How convenient!

Loading the appropriate files

To load our file, first we need to turn our FQCN into an actual path. This can be done by replacing every backslash with a forward slash and appending .php at the end. With that, we can already autoload files from a hard-coded base path!

Let's try it out!

// autoloader.php

<?php

function fqcnToPath(string $fqcn) {
    return str_replace('\\', '/', $fqcn) . '.php';
}

spl_autoload_register(function (string $class) {
    $path = fqcnToPath($class);

    require __DIR__ . '/src/' . $path;
});

After adding this, we can just run our app.php script again

$ php app.php
🥨

Great success! This shows you what autoloading boils down to: A fancy way to require a PHP file just in time when it's needed. But if we want to be (reasonably) spec-compliant, we still have work to do.

Enjoying the read?

Then we should totally keep in touch! Every time I do something interesting, you'll be the first to know

Right now the path we load from is hard-coded to be src/ but according to PSR-4, every package is free to define its own namespace prefix that maps to a path. This is essentially what you do every time you set up autoloading in Composer

{
    "autoload": {
        "psr-4": {
            "Pretzel\\": "src/Pretzel"
        }
    }
}

So let's try to parse this JSON snippet and use it to load our classes. I'll call it conductor.json, because that's what the logo of Composer depicts, and we're basically a cheap knock-off. 🤪

Autoloading according to configuration

The first thing we have to do is to grab our JSON file and turn it into something we can actually work with (i.e. an associative array). Luckily, PHP has native functions for both of those things.

// autoloader.php

<?php

$configuration = json_decode(
    file_get_contents('conductor.json'),
    true
);

$namespaces = $configuration['autoload']['psr-4'];

// .. SNIP ..

Now, the user is free to specify as many namespaces and base directories as she wants, so we need to check if the prefixed/first namespace is available in our psr-4 associative array. We could use a foreach loop, but we can also make it a bit more efficient using a function called strtok().

// autoloader.php

<?php
// .. SNIP ..

spl_autoload_register(function (string $class) use ($namespaces) {
    $prefix = strtok($class, '\\') . '\\';

    // We don't handle that namespace.
    // Return and hope some other autoloader handles it.
    if (!array_key_exists($prefix, $namespaces)) return;

    $baseDirectory = $namespaces[$prefix];
});

What strtok does is tokenize the string according to a separator. If we pass in the backslash, it will return us everything up to the first backslash, excluding that. To correctly match our configuration, we just add it again ourselves.

For the performance nerds among you this makes sure that we find the proper base directory in O(1) time.

Then we check if this namespace prefix maps to a path in our configuration. If it does, great! If it doesn't, we just return from the function and hope that someone else handles this case. We can't do anything else with it.

The next step is to remove the prefixed namespace from our class name. This is because we know exactly what directory the prefix maps to, and we want to start building our final path from that. We can do this in our fqcnToPath function.

// autoloader.php

<?php
// .. SNIP ..

function fqcnToPath(string $fqcn, string $prefix) {
    $relativeClass = ltrim($fqcn, $prefix);

    return str_replace('\\', '/', $relativeClass) . '.php';
}

spl_autoload_register(function (string $class) use ($namespaces) {
    $prefix = strtok($class, '\\') . '\\';

    // We don't handle that namespace.
    // Return and hope some other autoloader handles it.
    if (!array_key_exists($prefix, $namespaces)) return;

    $baseDirectory = $namespaces[$prefix];
    $path = fqcnToPath($class, $prefix);
});

Once again PHP has our back! We use ltrim to remove the main namespace from the beginning of our FQCN. This gives us the "relative" class, in the same sense as having a relative path. The rest of the function stays the same.

At this point we have a base directory and a relative path going out from that base directory. So the final piece that's missing is trying to require the appropriate file!

// autoloader.php

<?php
// .. SNIP ..

spl_autoload_register(function (string $class) use ($namespaces) {
    $prefix = strtok($class, '\\') . '\\';

    // We don't handle that namespace.
    // Return and hope some other autoloader handles it.
    if (!array_key_exists($prefix, $namespaces)) return;

    $baseDirectory = $namespaces[$prefix];
    $path = fqcnToPath($class, $prefix);

    require $baseDirectory . '/' . $path;
});

At this point we can run our app.php again and see if it works as advertised.

$ php app.php
🥨

Great! So now we can add PSR-4 autoloading configuration to our JSON file just as if it were Composer. Of course, one should probably add some more input validation to this code. Users could forget to specify a \ in their namespace. Or they could accidentally add a / in the base directory. But I'll leave that part to you, dear reader.

Closing thoughts

While this is a simplified example of what Composer does internally, it still shows you the general principles of PSR-4 autoloading and what goes into it. Once you break it down, it's really not as complex as it seems!

The main reason I even got the idea was due to a side project I'm silently hacking away on. I needed a simple way to load in PHP classes that define plugins, and well, here we are.

As always, the entire code for this blog post is available on GitHub.

Go to homepage