Foreword
It's been several years I didn't start a project from scratch. I mean totally from scratch. All my projects are by default fresh Symfony installations. 2 weeks ago I was recording one of the lectures of this course and when I was preparing a super simple example I run into a Class not found
error. This reminded me the good old days of using PHP require_once
method so I decided to write a short article about the way Composer is making our life easier and how it actually autoloads files behind the scenes.
Thank you Nils and Jordi for the awesome dependency manager and happy birthday Jordi!
Base line
Don't overthink what a Namespace is.
Namespace is basically just a Class prefix (like directory in Operating System) to ensure the Class path uniqueness.
Also just to make things clear, the use statement is not doing anything
only aliasing your Namespaces so you can use shortcuts or include Classes with the same name but different Namespace in the same file.
E.g:
<?php
// You can do this at the top of your Class
use Symfony\Component\Debug\Debug;
if ($_SERVER['APP_DEBUG']) {
// So you can utilize the Debug class it in an elegant way
Debug::enable();
// Instead of this ugly one
// \Symfony\Component\Debug\Debug::enable();
}
Problem
I want to call a public method of a Class encapsulated in a specific Namespace from a different directory.
<?php
// /ComposerAutoloading/src/Christmas/Santa.php
namespace Christmas;
class Santa
{
/**
* @return void
*/
public function sayIt(): void
{
echo "Merry Christmas && Write HQ code!";
}
}
<?php
// /ComposerAutoloading/src/index.php
$santa = new \Christmas\Santa();
$santa->sayIt();
Fatal error: Uncaught Error: Class 'Santa' not found
Old fashion solution
(click to play the movie scene)
php index.php
Fatal error: Uncaught Error: Class 'Santa' not found
So, to be able to initialize the Santa Class, you have to import it to your Global Namespace of index.php by running require_once($filePath)
which behind the scenes will execute the include statement.
<?php
// /ComposerAutoloading/src/index.php
require_once __DIR__.'/Christmas/Santa.php';
$santa = new \Christmas\Santa();
$santa->sayIt(); // Merry Christmas && Write HQ code!
This is of course a good solution but un-scalable one as the project size would grow.
Composer solution
Composer, PHP Composer ;)
Instead of manually typing require_once
every single time you want to include code from a different file you just import an auto-generated, self explanatory Composer file called: autoload.php.
Like Symfony does in it's frontend controllers: index.php/app.php/app_dev.php/console.php
<?php
// /ComposerAutoloading/src/index.php
// require_once 'Christmas/Santa.php';
require __DIR__.'/../vendor/autoload.php';
$santa = new \Christmas\Santa();
$santa->sayIt(); // Merry Christmas && Write HQ code!
Ehm Lukas but... I don't have any autoload.php file nor vendor folder.
Well, that's your problem.
Naaah, it's Christmas. Let me show you how to generate them.
Everything starts with composer.json file. Run:
/ComposerAutoloading/
composer init
and wuala, composer.json without any dependencies, as we don't need them to get autoloading working:
{
"name": "enterprise-php/composer-autoloading",
"description": "An example how Composer works behind the scenes.",
"type": "project",
"authors": [
{
"name": "Lukas Lukac",
"email": "services@trki.sk"
}
],
"require": {}
}
Now you can generate the autoload.php by running:
composer dump-autoload
Generating autoload files
The moment you included autoload.php in your index.php
require __DIR__.'/../vendor/autoload.php';
you triggered Standard PHP Library (SPL) function spl_autoload_register(callable $autoloadFunction)
the composer is using to register itself to take over the responsibility of autoloading PHP files on runtime (Classes that are used in most request can be pre-loaded).
<?php
// /ComposerAutoloading/vendor/composer/ClassLoader.php
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}
Ha! Interesting. So, all my Classes across different Namespaces will be now autoloaded automatically? Let's see.
php index.php
Fatal error: Uncaught Error: Class 'Santa' not found
Damn, Santa is still not found. That's pretty bad. Why?
Well, the registered ClassLoader executes the following loadClass($class)
method (shortened, adjusted for simplicity):
<?php
// /ComposerAutoloading/vendor/composer/ClassLoader.php
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return bool|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
includeFile($file);
return true;
}
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
...
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return $file;
}
Long story short. Composer checks different types of storage in quest of trying to find your Santa Class ordered by the fastest access for performance reasons obviously.
- in memory classMap array
- APCU cache
- disk using PSR-0 and PSR-4 standards
- disk using "PEAR-like class name"
So if it's going through all this trouble why it can't find my Santa class? I mean, how hard can it be to find Santa...
Because it's not Kaspersky antivirus! It checks ONLY the places you configured it to check.
Where can I configure it? Correct, where the journey has began, in composer.json.
You can either directly specify particular Classes in the classMap attribute (useful when no Namespace, clear directory structure is defined):
{
"name": "enterprise-php/composer-autoloading",
"description": "An example how Composer works behind the scenes.",
"type": "project",
"authors": [
{
"name": "Lukas Lukac",
"email": "services@trki.sk"
}
],
"require": {},
"autoload": {
"classmap": [
"src/Christmas/Santa.php"
]
}
}
which would result after running composer dump-autoload
to a new Hash file:
<?php
// /ComposerAutoloading/vendor/composer/autoload_classmap.php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'Christmas\\Santa' => $baseDir . '/src/Christmas/Santa.php',
);
Therefore the Composer Class lookup will be straight forward:
<?php
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
OR. You can define a more broad PSR-4 standard rule readable as:
Every time you will try to find a Class with Namespace starting with "Christmas", look into the "src/Christmas" directory.
{
"name": "enterprise-php/composer-autoloading",
"description": "An example how Composer works behind the scenes.",
"type": "project",
"authors": [
{
"name": "Lukas Lukac",
"email": "services@trki.sk"
}
],
"require": {},
"autoload": {
"psr-4": {
"Christmas\\": "src/Christmas"
}
}
}
Once again, after running composer dump-autoload
you will get a new Hash like structured file autoload_psr4.php:
<?php
// /ComposerAutoloading/vendor/composer/autoload_psr4.php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'Christmas\\' => array($baseDir . '/src/Christmas'),
);
One way or the other, the output of running php index.php
will be, and I wish you too:
Merry Christmas && Write HQ code!
Summary
Composer is mainly a dependency manager but it's doing a fantastic job of auto-loading internal, external (libraries, dependencies) PHP Classes using its ClassLoader.php registered as a file loader using SPL function.
Composer behind the scenes executes the include statement after it finds the file using PSR-4 and PSR-0 naming standards based on your settings in composer.json.
If you want to play further with the code, you can find it all in this GitHub directory.
Thank you for your time reading my first technical article on Enterprise Level PHP.
I am also happy to announce the launch of my course for Junior, Advanced PHP Developers dedicated to software quality and maintainable code!!!
It's my first project launch and I have very few Twitter followers therefore if you could re-tweet it and spread the word I would be EXTREMELY HAPPY.
Top comments (3)
Thank you so much. That helped a lot.
Hey Rima! Wou I wrote this so long time ago. I am happy it helped! Cheers. If you have any one more questions: linkedin.com/in/llukac/
Old but still genius