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.
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
.
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.
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:
Pretzel
) that corresponds to a base directory (e.g. src/Pretzel
)Factory
) that correspond to a directory (e.g. src/Pretzel/Factory
)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!
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.
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. 🤪
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.
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.