DEV Community

loading...
Cover image for The magic behind autoloading php files using Composer

The magic behind autoloading php files using Composer

Lukas Lukac
I train devs how to program full-stack blockchain apps on Ethereum with {Go, JS, Smart Contracts, IPFS}. StackOverflowing for 10+ years
Originally published at enterprise-level-php.com ・6 min read

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

Old fashion James Bond movie
(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.

  1. in memory classMap array
  2. APCU cache
  3. disk using PSR-0 and PSR-4 standards
  4. 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.

Read more about the course!

Discussion (2)

Collapse
weevi profile image
Rima Paskeviciute

Thank you so much. That helped a lot.

Collapse
web3coach profile image
Lukas Lukac Author

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/