phpspl-autoloader

php autoloader: why does this work?


I have been experimenting with autoloader class directory mapping techniques, and it has been a bit of a struggle. I managed to come up with a fairly straightforward solution (on the surface), but I'm totally mystified that it works at all, while other, "more obvious" solutions failed. Below are some code snippets that illustrate my confusion.

Here's the working code:

<?php
    spl_autoload_register('my_autoloader');

    function my_autoloader($class) {
        $classMap = array(
            'classes/',
            'classes/sites/',
            'classes/data/'
        );
        foreach ($classMap as $location) {
            if (!@include_once($location . $class . '.php')) { // @ SUPPRESSES include_once WARNINGS
                // CLASS DOESN'T EXIST IN THIS DIRECTORY
                continue;
            } else {
                // CLASS IS AUTO-LOADED
                break;
            }
        }
    }
?>

Here's a snippet that I felt should work, but doesn't:

<?php
    spl_autoload_register('my_autoloader');

    function my_autoloader($class) {
        $classMap = array(
            'classes/',
            'classes/sites/',
            'classes/data/'
        );
        foreach ($classMap as $location) {
            if (file_exists($location . $class . '.php')) {
                require_once ($location . $class . '.php');
            }
        }
    }
?>

The latter makes more sense to me because while these two versions work:

<?php
    spl_autoload_register('my_autoloader');

    function my_autoloader($class) {
        require_once ('classes/sites/' . $class . '.php');
    }
?>

<?php
    spl_autoload_register('my_autoloader');

    function my_autoloader($class) {                
        $location = 'classes/sites/';                
        require_once ($location . $class . '.php');
    }
?>

This one throws "No such file or directory..." (note the lack of "sites/" in the path.

<?php
    spl_autoload_register('my_autoloader');

    function my_autoloader($class) {
        require_once ('classes/' . $class . '.php');
    }
?>

The "No such file or directory..." error made me think I could simply check for a class's supporting file and, if (file_exists()) {require_once(); break;} else {continue;}

Why doesn't that work? And, why DOES the first snippet work? The supporting path/file is never explicitly included or required.


Solution

  • OK, I figured it out. My issue was indeed not setting up the path correctly; using the __DIR__ constant was the ::ahem:: path to success. Here's the working code I'm now using:

    <?php
        spl_autoload_register('my_autoloader');
    
        function my_autoloader($class) {
            $classMap = array(
                'classes/',
                'classes/sites/',
                'classes/data/'
            );
            foreach ($classMap as $location) {
                if (file_exists(__DIR__ . '/' . $location . $class . '.php')) {
                    require_once(__DIR__ . '/' . $location . $class . '.php');
                    break;
                }
            }
        }
    ?>
    

    It turns out __DIR__ returns the directory location of the actual file in which it is called (as opposed to, say, a file that is including it), which means this will work so long as this file remains in the root of the directory with my /classes directory. And, it exists exclusively for defining these kinds of settings...

    Hope this helps someone else down the line! And, of course, if anyone can shed light on why this solution is sub-optimal, I'm all eyes...

    EDIT: One optimization I will perform will be to convert the $classMap array() to an associative array (e.g,. $classMap = array('root' => 'classes/', 'sites' => 'classes/sites/'); so I can do a lookup rather than loop through all the directories I create over time. But, I suppose that's for another thread.

    EDIT2: If anyone's interested, here's the solution I came up with for doing this as an associative array. Basically, I set up an associative array in $GLOBALS and use that to store a map of Classes -> Packages. Then, in the autoloader, I map Packages -> Locations and then call in the file necessary for the instantiated class.

    This is the global config file:

    <?php
        // INITIALIZE GLOBAL Class -> Package map
        if (!array_key_exists('packageMap',$GLOBALS)) {
            $GLOBALS['packageMap'] = array();
        }
    
        spl_autoload_register('my_autoloader');
    
        function my_autoloader($class) {
            $classMap = array(
                'root'=>'/classes/',
                'sites'=>'/classes/sites/',
                'data'=>'/classes/data/'
            );
    
            // RESOLVE MAPPINGS TO AN ACTUAL LOCATION
            $classPackage = $GLOBALS['packageMap'][$class];
            $location = $classMap[$classPackage];
            if (file_exists(__DIR__ . $location . $class . '.php')) {
                require_once(__DIR__ . $location . $class . '.php');
            }
        }
    ?>
    

    Here's a specific site config file that uses the autoloader:

    <?php   
        // LOAD GLOBAL CONFIG FILE      
        require_once('../config/init.php');
    
        // CREATE LOCAL REFERENCE TO GLOBAL PACKAGE MAP (MOSTLY FOR READABILITY)
        $packageMap = $GLOBALS['packageMap'];
    
        // ADD Class -> Package MAPPINGS
        $packageMap['DAO'] = 'data';
        $packageMap['SomeSite'] = 'sites';
        $packageMap['PDOQuery'] = 'data';
    
        // INSTANTIATE SOMETHING IN ONE OF THE PACKAGES
        $someSite = new SomeSite();
    ?>
    

    Hopefully this is useful to others...if it raises any red flags for anyone, please chime in. Ultimately, I'd like to replace the use of $GLOBALS with apc, but it's not installed on my hosted server :/. I might consider memcached, but I've heard mixed reviews...