27
\$\begingroup\$

Basically, I had written this class a little while ago to ease autoloading of our local libraries.

The premise is that everything is split by packages into multiple layers of sub-packages. Classes are named using CamelCasing. So, a class' name is related to its package as follows: PackageSubpackageSubpackageName. Now, each package can have package specific interfaces defined by isPackageName for interfaces, exceptions by PackageNameException, etc. I tried to make it flexible enough for reuse.

/**
 * A class for lazy-loading other classes
 *
 * This class enables lazy-loading of php classes.  The benefit of this is
 * three-fold.  First, there is a memory benefit, since not all classes are
 * loaded until they are needed.  Second, there is a time benefit, since not all
 * classes are loaded.  Third, it produces cleaner code, since there is no need
     * to litter files with require_once() calls.
 *
 * @category Libraries
 * @package  Libraries
 * @author   Me
 */
abstract class Loader
{

    /**
     * @var array An array of class to path mappings
     */
    protected static $classes = array();

    /**
     * @var boolean Has the loader been initialized already
     */
    protected static $initialized = false;

    /**
     * @var array An array of auto-search paths
     */
    protected static $namedPaths = array(
        'exception',
        'interface',
        'iterator',
    );

    /**
     * @var array An array of include paths to search
     */
    protected static $paths = array(
        PATH_LIBS,
    );

    /**
     * Tell the auto-loader where to find an un-loaded class
     *
     * This can be used to "register" new classes that are unknown to the
     * system.  It can also be used to "overload" a class (redefine it
     * elsewhere)
     *
     * @param string $class The class name to overload
     * @param string $path  The path to the new class
     *
     * @throws InvalidArgumentException Upon an Invalid path submission
     * @return void
     */
    public static function _($class, $path)
    {
        $class = strtolower($class);
        if (!file_exists($path)) {
            throw new InvalidArgumentException('Invalid Path Specified');
        }
        self::$classes[$class] = $path;
    }

    /**
     * Add a path to the include path list
     *
     * This adds a path to the list of paths to search for an included file.
     * This should not be used to overload classes, since the default include
     * directory will always be searched first.  This can be used to extend
     * the search path to include new parts of the system
     *
     * @param string $path The path to add to the search list
     *
     * @throws InvalidArgumentException when an invalid path is specified
     * @return void
     */
    public static function addPath($path)
    {
        if (!is_dir($path)) {
            throw new InvalidArgumentException('Invalid Include Path Added');
        }
        $path = rtrim($path, DS);
        if (!in_array($path, self::$paths)) {
                self::$paths[] = $path;
        }
    }

    /**
     * Add a path to the auto-search paths (for trailing extensions)
     *
     * The path should end with an 's'.  Default files should not.
     *
     * @param string $path The name of the new auto-search path
     *
     * @return void
     */
    public static function addNamedPath($path)
    {
        $path = strtolower($path);
        if (substr($path, -1) == 's') {
            $path = substr($path, 0, -1);
        }
        if (!in_array($path, self::$namedPaths)) {
            self::$namedPaths[] = $path;
        }
    }

    /**
     * Initialize and register the autoloader.
     *
     * This method will setup the autoloader.  This should only be called once.
     *
     * @return void
     */
    public static function initialize()
    {
        if (!self::$initialized) {
            self::$initialized = true;
            spl_autoload_register(array('Loader', 'load'));
        }
    }

    /**
     * The actual auto-loading function.
     *
     * This is automatically called by PHP whenever a class name is used that
     * doesn't exist yet.  There should be no need to manually call this method.
     *
     * @param string $class The class name to load
     *
     * @return void
     */
    public static function load($class)
    {
        $className = strtolower($class);
        if (isset(self::$classes[$className])) {
            $file = self::$classes[$className];
        } else {
            $file = self::findFile($class);
        }
        if (file_exists($file)) {
            include_once $file;
        }
    }

    /**
     * Find the file to include based upon its name
     *    
     * This splits the class name by uppercase letter, and then rejoins them
     * to attain the file system path.  So FooBarBaz will be turned into
     * foo/bar/baz.  It then searches the include paths for that chain.  If baz
     * is a directory, it searches that directory for a file called baz.php.
     * Otherwise, it looks for baz.php under the bar directory.
     *
     * @param string $class The name of the class to find
     *
     * @return string The path to the file defining that class
     */
    protected static function findFile($class)
    {
        $regex = '#([A-Z]{1}[a-z0-9_]+)#';
        $options = PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE;
        $parts = preg_split($regex, $class, null, $options);

        $subpath = '';
        $file = strtolower(end($parts));
        $test = strtolower(reset($parts));
        if ($test == 'is') {
            array_shift($parts);
            return self::findNamedFile($class, $parts, 'interface');
        }
        foreach ($parts as $part) {
            $subpath .= DS . strtolower($part);
        }    
        foreach (self::$paths as $path) {
            $newpath = $path . $subpath;
            if (is_file($newpath . '.php')) {
                return $newpath . '.php';
            } elseif (is_file($newpath . DS . $file . '.php')) {
                return $newpath . DS . $file . '.php';
            }
        }
        if (in_array($file, self::$namedPaths)) {
            //Get rid of the trailing part
            array_pop($parts);
            return self::findNamedFile($class, $parts, $file);
        }    
        return '';
    }

    /**
     * Find a file for named directories (interfaces, exceptions, iterators, etc)
     *
     * @param string $class The class name of the exception to find
     * @param array  $parts The parts of the class name pre-split
     * @param string $name  The name of the named directory to search in
     *    
     * @return string The found path, or '' if not found
     */
    protected static function findNamedFile($class, array $parts, $name)
    {    
        if (empty($parts)) {
            return '';
        }
        $name = strtolower($name);
        //Add a trailing s, since individual files are not plural
        $filename = $name;
        $name .= 's';
        //Try the global path first
        $subpath = DS . $name . DS . strtolower(implode('', $parts)) . '.php';
        foreach (self::$paths as $path) {
            $newpath = $path . $subpath;
            if (is_file($newpath)) {
                return $newpath;
            }
        }
        //Try to build a full sub path for package specific named files
        $package = array_shift($parts);
        $subpath = DS . strtolower($package) . DS . $name . DS;
        if (!empty($parts)) {
            $subpath .= strtolower(implode('', $parts)) . '.php';
        } else {
            $subpath .= $filename . '.php';
        }
        foreach (self::$paths as $path) {
            $newpath = $path . $subpath;
            if (is_file($newpath)) {
                return $newpath;
            }
        }
        return '';
    }
}

It's also fully unit tested.

What are your thoughts? Is it over-complex?

\$\endgroup\$

4 Answers 4

14
\$\begingroup\$

The first problem I see is that there are a lot of cases where someone will want to create a class with more than one word in the name (DataMapper), and the autoloader you have provided won't allow for that. I would recommend using another character to delimit between package names. The Zend Framework uses Package_SubPackage_SubPackage_Class and that works very well.

As an aside, I'm not sure what your specific reasons are for writing your own autoloader (whether it is for production, education, etc.), but if you are planning to use it for production, I would recommend the Zend_Loader class from the Zend Framework, as it is supported, fully tested, and continually being developed. You can read it's quickstart guide Here

\$\endgroup\$
4
  • 4
    \$\begingroup\$ Well, it's personal preference, but I can't stand using _ in class names. I find it's harder to type, and breaks it apart too much. As far as multiple words in a name, yes that's a valid point. And why don't I use Zend? Well, it's personal preference. I've tried it in the past, and didn't care for it. I can use it, but it just rubs me the wrong way... Thanks for the insight though... \$\endgroup\$
    – ircmaxell
    Commented Jan 19, 2011 at 21:37
  • 2
    \$\begingroup\$ To clarify, when I was talking about Zend, I wasn't recommending the entire framework, but just the autoloader class, which can be used completely decoupled from everything else in zf. \$\endgroup\$
    – Wade Tandy
    Commented Jan 19, 2011 at 21:50
  • 2
    \$\begingroup\$ HiMyNameIsRobertAndIWorkLongHoursSadFace vs Hi_My_Name_Is_Robert_And_I_Work_Long_Hours_Sad_Face, i belive this is the reason for the _ \$\endgroup\$
    – RobertPitt
    Commented Jan 19, 2011 at 23:36
  • \$\begingroup\$ @RobertPitt: I'd argue that if your class name is really that long, you need to refactor or rethink your packaging system... \$\endgroup\$
    – ircmaxell
    Commented Jan 20, 2011 at 14:40
8
\$\begingroup\$

Some points i found:

Thats the class is marked as abstract struck me as odd as i found out it only has static method calls and since it uses "self::" for static call i guess there is no meaningful way to extend the class anyways. (With the LSB issue).

I don't see any big issue with the class beeing "all static" and i assume it fits into your project. (You don't have a clear bootstrap and you don't want/need multiple instances of that class)


The include_once $file; strucks me as a little odd as the "_once" part shouldn't be need. But if you wrote the loader at a later stage in the project i see where it might be needed to not run into issues with classes getting loaded two times.

Usually i'd say you don't have to make php remember if it already touched a file (and do a disk expensive realpath() on it) since the load function will only be called one time for each previously unknown class.


All in all i think the code/usefulness ratio is fine and it isn't overly complex.

Alternatives

The upcoming "standards" and libs are alreay mentioned so i'll just point out another way to do autoloading that "performs better" and is less intrusive (requires less code in your application)

The PHP Autoload Builder will scan your codebase and provide one file with a big array mapping for all your classes (interface etc.) that you only need to include in your bootstrap. It can be run again to pick up new classes or the resulting file can be edited by hand. (Some people build tools around it so it automatically recreates itself in development if a class isn't found).

\$\endgroup\$
0
6
\$\begingroup\$

When it comes to Autoloaders, I would tend make it compatible with any of my projects, therefore I would always stay to the best coding standards such as Zend.

There is a proposal that states the layout of classes, directory structure, namespaces where autoloaders work very good.

The following describes the mandatory requirements that must be adhered to for autoloader interoperability.

  • Mandatory:

  • A fully-qualified namespace and class must have the following structure:

         \<Vendor Name>\(<Namespace>\)*<Class Name>
    
  • Each namespace must have a top-level namespace ("Vendor Name").

  • Each namespace can have as many sub-namespaces as it wishes.

  • Each namespace separator is converted to a DIRECTORY_SEPARATOR when loading from the file system.

  • Each _ character in the CLASS NAME is converted to a DIRECTORY_SEPARATOR. The _ character has no special meaning in the namespace.

  • The fully-qualified namespace and class is suffixed with ".php" when loading from the file system.

  • Alphabetic characters in vendor names, namespaces, and class names may be of any combination of lower case and upper case.

  • Examples:

  • \Doctrine\Common\IsolatedClassLoader => /path/to/project/lib/vendor/Doctrine/Common/IsolatedClassLoader.php

  • \Symfony\Core\Request => /path/to/project/lib/vendor/Symfony/Core/Request.php

  • \Zend\Acl => /path/to/project/lib/vendor/Zend/Acl.php

  • \Zend\Mail\Message => /path/to/project/lib/vendor/Zend/Mail/Message.php

Using the above to construct your autoloader will surely be migratable around your projects regarding weather you have namespaces or not.

@Reference

There is a very nice class that I use in around 6 projects, and I find that this is perfect and you should study and see what you can do with it.

Class Link

An example usage would be like so:

$classLoader = new SplClassLoader('Doctrine\Common', '/libs/doctrine');
$classLoader->register();

$classLoader = new SplClassLoader('Ircmexell\MyApplication', 'libs/internal');
$classLoader->register();
\$\endgroup\$
2
\$\begingroup\$

For about a year I used a very simple autoloader with common PHP files root for my project and all included libraries (Zend, Rediska and so on).

The root of my project contains /app and /external directories.

All libraries in /external are fully checked out from svn/git, and then I make a symlink for their PHP code in /app.

For example, for PHPExcel:

pwd 
/var/www/project/app
ls -lah PHPE*
PHPExcel -> ../external/PHPExcel/PHPExcel
PHPExcel.php -> ../external/PHPExcel/PHPExcel.php

Then I put something like this in my index.php file:

set_include_path (PATH . 'app');
require 'somepath/Autoloader.php';
Autoloader::registerAutoload ();

It allows me to use one commonly (it's fully compatible with Zend, Rediska, PHPExcel and many other libraries) and is a very simple autoloader for all libraries.

class Autoloader
{

    public static function registerAutoload ()
    {
        spl_autoload_register (array (__CLASS__, 'autoload'));
    }

    /**
     * @static
     * @param string $class
     * @return void
     */
    public static function autoload ($class)
    {
        require str_replace (array ('_', '\\'), '/', $class) . '.php';
    }

}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.