When developing any PHP project, it is a standard practice to define each class in it’s own file and name the file after the class. For example, a User class is defined in a User.php file and a Article class is defined in a Article.php file. However, before they are used, these files need to be included using any PHP include functions like require_once() . Easy, right? NO 🙁 As our project grows, we will have more and more classes and the number of include statements will keep on increasing, which is tedious and not very developer-like thing to do.

To overcome this issue, we can create a loader class which loads any class on demand. We will use spl_autoload_register() , a special PHP function that gives us the ability to create a series of functions that can be called to load any class.

Let’s name our loader class ClassLoader. We keep this loader class in Autoload directory and namespace the class accordingly.

<?php
 
namespace App\Autoload;
 
class ClassLoader
{
    const UNABLE_TO_LOAD = 'Unable to load class';
    
    protected static $dirs = array();
    
    protected static $registered = 0;
 
}

The $dirs property is used to hold list of directories that may contain class files. $registered will later be used to check if the loader class has already been registered as autoloader. All the methods and properties in this class are defined as static . This is because it gives us the ability to treat this loader class as a singleton.

Now we create a method loadFile() that simply loads any file. This function takes a file name with complete path as argument. It then checks if the file exists using file_exists() function and loads it using require_once() .

protected static function loadFile($file) : bool
{
    if (file_exists($file)) {
        require_once $file;
        return TRUE;
    }
    return FALSE;
}

Notice we prefer require_once() to other PHP include functions because require_once() will throw a fatal error if the file is not found. We can use the return value of loadFile() function to loop through a list of alternate directories before throwing any exception if the file is not found.

Next we define autoLoad() which performs the logic to locate a file based on it’s class name. This method takes a class name with complete namespace as it’s argument. It then derives the corresponding file name by converting the PHP namespace separator into directory separator for the server and append .php extension to the end using the str_replace() function.

$fileName = str_replace('\\', DIRECTORY_SEPARATOR, $class) . '.php';

We then loop through all the directories that $dirs holds and try to load the class file. If it is found, a flag variable is set and we break out of the loop.

foreach (self::$dirs as $start) {
    $file = $start . DIRECTORY_SEPARATOR . $fileName;
    if (self::loadFile($file)) {
        $success = TRUE;
        break;
    }
}

If the file is not found in the list of directories, we then attempt to load the file from the current directory. If it is still not found, we throw an exception.

if (!$success) {
    if (!self::loadFile(__DIR__ . DIRECTORY_SEPARATOR . $fileName)) {
        throw new \Exception(self::UNABLE_TO_LOAD . ' ' . $class);
    }
}

Finally we return the state of the flag variable. The complete method is as follows:

public static function autoLoad($class) : bool
{
    $success = FALSE;
    
    $fileName = str_replace('\\', DIRECTORY_SEPARATOR, $class) . '.php'; 
    
    foreach (self::$dirs as $start) {
        $file = $start . DIRECTORY_SEPARATOR . $fileName;
        if (self::loadFile($file)) {
            $success = TRUE;
            break;
        }
    }
 
    if (!$success) {
        if (!self::loadFile(__DIR__ . DIRECTORY_SEPARATOR . $fileName)) {
            throw new \Exception(self::UNABLE_TO_LOAD . ' ' . $class);
        }
    }
 
    return $success;
}

We define a method addDirs() to add more directories to our list of directories. This function takes either an array of directories or a single directory as argument.

public static function addDirs($dirs)
{
    if (is_array($dirs)) {
        self::$dirs = array_merge(self::$dirs, $dirs);
    } else {
        self::$dirs[] = $dirs;
    }
}

If the value provided is an array, array_merge() is used. Otherwise we simple add the directory to our $dir array.

Now to use the autoload functionality of this class, we need to register autoLoad() method as a Standard PHP Library (SPL) autoloader. This is done using spl_autoload_register() . We define a init() method that adds a directory to the list of supported directories and also registers autoload as an autoloading method.

public static function init($dirs = array())
{
    if ($dirs) {
        self::addDirs($dirs);
    }
    if (self::$registered == 0) {
        spl_autoload_register(__CLASS__ . '::autoload');
        self::$registered++;
    }
}

Finally we define the constructor for this class. The constructor initializes the array of directories using the init() method. It also helps us in creating an instance of this ClassLoader class should we need it.

public function __construct(array $dirs = array())
{
    self::init($dirs);
}

Now our loader class is complete. The full code of the class is as follows:

<?php
 
namespace App\Autoload;
 
class ClassLoader
{
    const UNABLE_TO_LOAD = 'Unable to load class';
    protected static $dirs = array();
    protected static $registered = 0;
 
    /**
     * Initializes directories array
     * 
     * @param array $dirs
     */
    public function __construct(array $dirs = array())
    {
        self::init($dirs);
    }
 
    /**
     * Adds directories to the existing array of directories
     * 
     * @param array | string $dirs
     */
    public static function addDirs($dirs)
    {
        if (is_array($dirs)) {
            self::$dirs = array_merge(self::$dirs, $dirs);
        } else {
            self::$dirs[] = $dirs;
        }
    }
 
    /**
     * Adds a directory to the list of supported directories
     * Also registers "autoload" as an autoloading method
     *
     * @param array | string $dirs
     */
    public static function init($dirs = array())
    {
        if ($dirs) {
            self::addDirs($dirs);
        }
        if (self::$registered == 0) {
            spl_autoload_register(__CLASS__ . '::autoload');
            self::$registered++;
        }
    }
 
    /**
     * Locates a class file 
     * 
     * @param string $class
     * @return boolean
     */
    public static function autoLoad($class) : bool
    {
        $success = FALSE;
        
        $fileName = str_replace('\\', DIRECTORY_SEPARATOR, $class) . '.php'; 
        
        foreach (self::$dirs as $start) {
            $file = $start . DIRECTORY_SEPARATOR . $fileName;
            if (self::loadFile($file)) {
                $success = TRUE;
                break;
            }
        }
        
        if (!$success) {
            if (!self::loadFile(__DIR__ . DIRECTORY_SEPARATOR . $fileName)) {
                throw new \Exception(self::UNABLE_TO_LOAD . ' ' . $class);
            }
        }
        return $success;
    }
 
    /**
     * Loads a file
     * 
     * @param string $file
     * @return boolean
     */
    protected static function loadFile($file) : bool
    {
        if (file_exists($file)) {
            require_once $file;
            return TRUE;
        }
        return FALSE;
    }
}