phpspl-autoload-registerspl-autoloader

PHP spl_autoload_register causes classes duplications?


This is my web structure for instance,

index.php

  'core/model/' 
  'core/helper/'
  'core/ext/'

I have these class files under the model folder,

MyClass.php
MyClass2.php
classfour.php

And I want to autoload them via Autoload class,

define ('WEBSITE_DOCROOT', str_replace('\\', '/', dirname(__FILE__)).'/');

function autoloadMultipleDirectory($class_name) 
{
    // List all the class directories in the array.
    $main_directories = array(
        'core/model/',
        'core/helper/',
        'core/ext/'
    );

    $sub_directories = array();

    $parts = explode('\\', $class_name);

    $file_name = end($parts).'.php';

    foreach($main_directories as $path_directory)
    {
        $iterator = new RecursiveIteratorIterator
        (
            new RecursiveDirectoryIterator(WEBSITE_DOCROOT.$path_directory), // Must use absolute path to get the files when ajax is used.
            RecursiveIteratorIterator::SELF_FIRST
        );

        foreach ($iterator as $fileObject) 
        {
            if ($fileObject->isDir()) 
            {
                $sub_directories[] = preg_replace('~.*?(?=core|local)~i', '', str_replace('\\', '/', $fileObject->getPathname())) .'/';
            }
        }
    }

    // Mearge the main dirs with any sub dirs in them.
    $merged_directories = array_merge($main_directories,$sub_directories);

    print_r($merged_directories);

    // Loop the merge array and include the classes in them.
    foreach($merged_directories as $path_directory)
    {
        if(file_exists(WEBSITE_DOCROOT.$path_directory.$file_name))
        {
            include WEBSITE_DOCROOT.$path_directory.$file_name;
        } 
    }
}

Implementations,

spl_autoload_register('autoloadMultipleDirectory');

//define classone
class classone { }

//define classtwo
class classtwo { }

//define classthree
class classthree { }

print_r(get_declared_classes());

result,

Array
(
    [0] => stdClass
    ...
    [131] => tidyNode
    [132] => classone
    [133] => classtwo
    [134] => classthree
)

but classfour, MyClass & MyClass2 are not in the list.

and for,

$test = new MyClass();

$test2 = new MyClass2();

$test3 = new classfour();

will cause the the duplications depends on how many classes I have instantiated,

Array
(
    [0] => core/model/
    [1] => core/helper/
    [2] => core/ext/
)
Array
(
    [0] => core/model/
    [1] => core/helper/
    [2] => core/ext/
)
Array
(
    [0] => core/model/
    [1] => core/helper/
    [2] => core/ext/
)

in my live server I get even stranger result,

Array
(
    [0] => core/model/
    [1] => core/helper/
    [2] => core/ext/
    [3] => core/model/./ (duplicated)
    [4] => core/model/../ (duplicated)
    [5] => core/helper/./ (duplicated)
    [6] => core/helper/../ (duplicated)
    [7] => core/ext/./ (duplicated)
    [8] => core/ext/../ (duplicated)
)

Any idea why and how can I fix it?

EDIT:

If I include the classes manually,

 include 'core/model/MyClass.php';
 include 'core/model/MyClass2.php';
 include 'core/model/classfour.php';

print_r(get_declared_classes());

$test = new MyClass();

$test2 = new MyClass2();

$test3 = new classfour();

I get the correct answer,

Array
(
    [0] => stdClass
    ...
    [131] => tidyNode
    [132] => classone
    [133] => classtwo
    [134] => classthree
    [135] => MyClass
    [136] => MyClass2
    [137] => classfour
)

classfour, MyClass & MyClass2 are now in the list.

How come!??


Solution

  • You are addressing some different issues here.

    Autoloading vs. manual loading...

    When you register an autoload function, it is called whenever you instantiate an object. In this function, you are supposed to check what class is being instantiated and provide the corresponding class definition somehow, most often by including a relevant php file. This is convenient, because you make sure that only the classes that need to be instantiated are actually declared, in a JIT manner. Note also that, if your class extends another, then the autoload function will also be called for the parent class, and so on recursively for all ancestor classes.

    This is different from manually including the class files. As soon as a myfooclass.php file is included, the relevant class is declared, regardless of whether it gets instantiated later or not.

    ...and get_declared_classes()

    get_declared_classes() does what its name implies: it returns an array with all the classes that have been declared at the time you call it. In other words, if you call it from several locations in your script, it's likely you'll get different results, depending on what classes have been declared at the time.

    The duplication

    I can see duplicate paths in your live server's results, but not the way you describe them. For example, core/model/./ is a duplicate of core/model/ because the former resolves to the latter. But core/model/../ is not a duplicate since it resolves to core/ (it only gets duplicated later on, by core/helper/../ and core/ext/../).

    A good practice would be to use realpath() which would resolve such issues (and some other ones as well) and provide you with absolute, canonicalized, existing paths. Moreover, you should either check if a path already exists before inserting it in $sub_directories:

    if ($fileObject->isDir()) 
      {
      $pathname=$fileObject->getPathname();
      $slashed=str_replace('\\', '/', $pathname);
      $filtered=preg_replace('~.*?(?=core|local)~i', '', $slashed) .'/';
      $canonical=realpath($filtered);
      if(!in_array($canonical,$sub_directories)) $sub_directories[] = $canonical;
      }
    

    ...or do an array_unique() before using it:

    $merged_directories = array_merge($main_directories,$sub_directories);
    $merged_directories = array_unique($merged_directories);
    print_r($merged_directories);
    

    Of course, these solutions are passive and do not require you to alter your application logic.

    Another failsafe would be to use include_once instead of include so as to not accidentally include the same file twice.

    Oh, and a last note: After you have found the file in question, there's no need to continue searching. Just break the loop:

        if(file_exists($path_directory.$file_name))
          {
          include_once $path_directory.$file_name;
          break; // <-- no point going on
          }