PHP Autoloading with PSR-4
In medieval times, a PHP application was a collection of files that all require_once’d each other in order to import functionality. As applications grew and became more unwieldy, most developers would come up with an ad-hoc solution of something like an imports.php file that contained require_once statement references to all of the files in your application, so that you just needed to import that one file.
The PHP community eventually settled on a standard for this, called autoloading. It’s described in PSR-4 and most libraries understand it (including composer, the PHP package manager).
In order to set up a PHP application with PSR-4 autoloading, you’ll want composer installed obviously, and your project’s directory ought to look like this:
my-project/
src/
tests/
Your project has a top-level namespace. This will be the root of all of your namespaces and import paths (more on that in a second). For our example, we’ll use a top-level namespace of MyProject.
At this point you’ll want to composer init in your working directory (for now, just mash enter and accept all of the defaults, and then choose no when it asks if you want to install anything interactively), which will create a composer.json and a vendor/ directory. The whole thing looks like this now:
my-project/
src/
tests/
vendor/
composer.json
Within composer.json, we’ll now tell it how to find all of our files relative to the top-level namespace. Add the following to your composer.json, if you don’t already have an autoload section:
...
"autoload": {
"psr-4": {
"MyProject\\": "src/"
}
}
...
This tells composer to generate an autoloader that will attempt to find any class in the MyProject namespace by starting in our src/ directory and going down. Nice!
Let’s see what that looks like in real PHP files. Let’s create two examples.
src/Hello.php:
<?php
namespace MyProject;
class Hello
{
public function message(): string
{
return "Hello";
}
}
src/Hello/World.php:
<?php
namespace MyProject\Hello;
class World
{
public function message(): string
{
return "Hello world";
}
}
Here we’ve got two files. One of them is nested just under the root MyProject namespace, so it lives just under src/. The other is in a subnamespace of MyProject called Hello. As a result, it lives in src/Hello. PSR-4 is relatively simple - subnamespaces like Hello should live in a directory of the same name (and should be in capital case like our examples).
Let’s see how we reference these files we’ve created via PSR-4 and namespaces. Let’s install PHPUnit:
$ composer require phpunit/phpunit
At this point, composer generates an autoload file based on the specification in our composer.json. It looks like this:
$ cat vendor/composer/autoload_psr4.php
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'phpDocumentor\\Reflection\\' => array($vendorDir . '/phpdocumentor/reflection-common/src', $vendorDir . '/phpdocumentor/reflection-docblock/src', $vendorDir . '/phpdocumentor/type-resolver/src'),
'Webmozart\\Assert\\' => array($vendorDir . '/webmozart/assert/src'),
'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'),
'Prophecy\\' => array($vendorDir . '/phpspec/prophecy/src/Prophecy'),
'MyProject\\' => array($baseDir . '/src'),
'Doctrine\\Instantiator\\' => array($vendorDir . '/doctrine/instantiator/src/Doctrine/Instantiator'),
'DeepCopy\\' => array($vendorDir . '/myclabs/deep-copy/src/DeepCopy'),
);
You can see that all of the stuff PHPUnit depends on are listed here, along with our namespace MyProject.
We will create some simple test files:
tests/HelloTest.php:
<?php
use PHPUnit\Framework\TestCase;
use MyProject\Hello;
final class HelloTest extends TestCase
{
public function testHello(): void
{
$h = new Hello();
$this->assertEquals(“Hello", $h->message());
}
}
tests/Hello/WorldTest.php:
<?php
use PHPUnit\Framework\TestCase;
use MyProject\Hello\World;
final class WorldTest extends TestCase
{
public function testWorld(): void
{
$w = new World();
$this->assertEquals(“Hello world", $w->message());
}
}
These two test files demonstrate how to reference namespaced files - we know that for example our World.php lives in src/Hello, and thus the use statement will refer to it in its sub-namespace via MyProject\Hello\World;
Verify that everything works correctly:
$ vendor/bin/phpunit tests/
PHPUnit 9.1.1 by Sebastian Bergmann and contributors.
.. 2 / 2 (100%)
Time: 22 ms, Memory: 4.00 MB
OK (2 tests, 2 assertions)
Nice.
Some more quick examples:
You can skip the use statements if you don’t mind fully qualifying the class namespaces like so:
<?php
use PHPUnit\Framework\TestCase;
final class WorldTest extends TestCase
{
public function testHello(): void
{
$w = new MyProject\Hello();
$this->assertEquals("Hello", $w->message());
}
public function testWorld(): void
{
$w = new MyProject\Hello\World();
$this->assertEquals("Hello world", $w->message());
}
}
Note here that we removed the use statement referring to MyProject and its subnamespaces, and instead when we instantiate the classes we want we simply include the entire path starting at the root namespace.
You can also import parts of a namespace and do the same thing. Here, we’ll just add a use statement for MyProject\Hello:
<?php
use PHPUnit\Framework\TestCase;
use MyProject\Hello;
final class WorldTest extends TestCase
{
public function testHello(): void
{
$w = new Hello();
$this->assertEquals("Hello", $w->message());
}
public function testWorld(): void
{
$w = new Hello\World();
$this->assertEquals("Hello world", $w->message());
}
}
Here, this import gives us access to the Hello class without having to qualify further. However, we still need to provide PHP the rest of the path to get to the World class since it’s in a subnamespace.