tags:

views:

58

answers:

4

I'm trying to define my PHP __autoload function to be as bulletproof and flexible as possible.

Here's the breakdown of my application's structure:

/dev (root)
    /my_app
        /php
            /classes
                - Class1.php
                - Class2.php
                - Class3.php
        /scripts
            myscript.php (the file I have to include the classes in)

It's rather straight forward. My problem is this: how do I write my __autoload function so that I can include any class I want regardless of how deeply nested the calling file is within the directory structure. I know it has something to do with the __FILE__ , realpath and dirname functions, but I'm not certain as to the appropriate way to combine them to achieve the flexibility I'm after.

Here's a quick test I did:

<?php
echo realpath(dirname(__FILE__)) . "/php/classes/Class1.php";
?>

The result:

/home/mydirectory/dev.mysite.com/my_app/php/scripts/php/classes/Class1.php

As you can see, the result doesn't match where the class is located. However, if I moved the myscript.php file into /my_app folder, it would print correctly.

Suggestions on making this more flexible?

A: 

There are basically two ways. Either you specify the full absolute path to your class directory (/home/mydirectory/dev.mysite.com/my_app/php/classes/), which I would not recommend because it involves changing the absolute path if you change hosts. Or you may use a relative path, which is way easier and portable:

require_once '../classes/'.$classname;

No need to get the realpath here, PHP is fine with relative paths. ;)

PS: realpath(dirname(__FILE__)) is duplicate I think. __FILE__ is already a "real path" and therefore you don't need to call realpath.

nikic
@nikic, Thanks for the reply. However, this doesn't work if we're using __autoload since the goal is to only write a require statement to include the file that contains the __autoload function. My question is how to detect the relative path to the classes folder regardless of where __autoload is being called from.
Levi Hackwith
+1  A: 

I would suggest looking into spl_autoload. just add the proper directories to your include_path

Something like this can help probably you get started:

ini_set($your_class_dir_here .PATH_SEPERATOR. ini_get('include_path'));

You'll have to either provide your own autoloader using spl_autoload_register or lowercase all your filenames.

Here's one of my own autoloaders, which uses php namespaces to overcome some directory issues.

<?php

namespace Red
{
    // since we don't have the Object yet as we load this file, this is the only place where this needs to be done.
    require_once 'Object.php';

    /**
     * Loader implements a rudimentary autoloader stack.
     */
    class Loader extends Object
    {
        /**
         * @var Loader 
         */
        static protected $instance = null;

        /**
         * @var string 
         */
        protected $basePath;

        /**
         * @return Loader
         */
        static public function instance()
        {
            if (self::$instance == null)
            {
                self::$instance = new self();
            }
            return self::$instance;
        }

        /**
         * Initialize the autoloader. Future expansions to the 
         * autoloader stack should be registered in here.
         */
        static public function Init()
        {
            spl_autoload_register(array(self::instance(), 'autoLoadInNamespace'));
        }

        /**
         * PHP calls this method when a class is used that has not been
         * defined yet. 
         * 
         * I'm returning a boolean for success which isn't required (php ignores it)
         * but makes life easier when the stack grows.
         * 
         * @param string $fullyQualifiedClassName
         * @return boolean 
         */
        public function autoLoadInNamespace($fullyQualifiedClassName)
        {
            $pathParts = preg_split('/\\\\/', $fullyQualifiedClassName, -1, PREG_SPLIT_NO_EMPTY);
            array_unshift($pathParts, $this->basePath);
            $pathToFile = implode(DIRECTORY_SEPARATOR, $pathParts) . '.php';

            if (file_exists($pathToFile))
            {
                require_once $pathToFile;
                return true;
            }
            return false;
        }

        /**
         * Constructor is protected because we don't want multiple instances
         * But we do want an instance to talk to later on.
         */
        protected function __construct()
        {
            $this->basePath = realpath(dirname(__FILE__) . DIRECTORY_SEPARATOR . '..');
        }
    }
}

#EOF;

It's part of a class named Loader in the \Red namespace and gets initialized from a simple bootstrap file:

<?php
// This is where the magic is prepared. 
require_once implode(DIRECTORY_SEPARATOR, array(dirname(__FILE__), 'Red', 'Loader.php'));
// Initialize the autoloader, no more require_once for every class
// and everything is OOP from here on in.
Red\Loader::Init();

#EOF
Kris
A: 

I would not opt for automatic file location detection. Unless well thought about it can be a security vulnerability, you need to design a naming schematic that supports subdirectories, and it forces you to use one file per class (which can either be a good or bad thing, but I find it inflexible). I would use a global or static array where you keep a mapping className => pathToClass.

Bryan
that sounds like a maintenance nightmare. unless you use a build process to create that array.
Kris
@Bryan: But that array would be one more thing I'd have to maintain every time I created a new class; that's the problem I want to avoid. I don't want to have to keep an array up to date or copy-and-paste ANOTHER require statement into all the files that reference the new class
Levi Hackwith
+1  A: 

$_SERVER['DOCUMENT_ROOT'] should contain the full path to the root directory of your web server. From there you should be able to continue the path through your folder structure to the class directory. If you stored the app name in session, the same code could be used pretty much anywhere.

//set in config file
if(!isset($_SESSION['APP_DIR'])) $_SESSION['APP_DIR'] = "my_app";

//autoload
//builds a string with document root/app_name/classes
//takes the app name and replaces anything not alphanumeric or _ with an _ and
//makes it lowercase in case of case sensitive. as long as you follow this naming
//scheme for app directories it should be fine for multiple apps.
$classPath = $_SERVER['DOCUMENT_ROOT'] . '/' .
           strtolower(preg_replace('/\W+/', '_', $_SESSION['APP_DIR'])) .
           '/classes/';
Jonathan Kuhn
I'm trying to stick to a relative path.
Levi Hackwith