Optimize function to include classes also looking in sub-directories

11

I have the following function to include classes when we are trying to use a class that has not yet been defined:

/**
 * Attempt to load undefined class
 * @param object $class_name object class name
 */
function __autoload($class_name) {

    $incAutoload = dirname(__FILE__);
    $filename = $class_name.'.class.php';

    /* App Classes
     */
    $appPath = $incAutoload.'/classes/'.$filename;

    if (is_file($appPath)) {
        require_once($appPath);
        return true;
    }

    /* Website Classes
     */
    $sitePath = $incAutoload.'/classes/site/'.$filename;

    if (is_file($sitePath)) {
        require_once($sitePath);
        return true;
    }

    // ...
}

Problem

Whenever a sub-directory is created to organize the project classes, I have to edit this file and include a check for this sub-directory:

/* Google Classes
 */
$path = $incAutoload.'/classes/google/'.$filename;

if (is_file($path)) {
    require_once($path);
    return true;
}

Question

How can I optimize this function so that it looks for the class in the classes base directory but also in any of the existing sub-directories?

    
asked by anonymous 16.01.2014 / 16:40

5 answers

4

Do this:

Root file:

new \Root\Classe();
new \Root\foo\Tree();

spl_autoload_register(function ($className) {
    $className = str_replace('Root\', '', $className);
    $className = strtr($className, '\', DIRECTORY_SEPARATOR);
    require $className.'.php';
});

"Class.php" file, also in root:

namespace Root;

class Classe {
    public function __construct() {
        echo 'Raiz';
    }
}

"Tree.php" file, located at "root / foo":

namespace Root\foo;

class Tree {
    public function __construct() {
        echo 'Tree';
    }
}

Output when executing the first code:

  

Root Tree

So you use the namespace to do autoload, combining the namespace with the physical path, as I mentioned. It's fast, simple, elegant and without iteration, and you only upload the files that really matter. Accessing the disk is a slow process, if you have numerous classes, by an iteration process over folders and files, would greatly impair their performance.

    
18.01.2014 / 19:35
2

You can use spl_autoload , see an example:

spl_autoload_register(NULL, FALSE);
spl_autoload_extensions('.php');
spl_autoload_register();

set_include_path(get_include_path() . PATH_SEPARATOR . '../');

OU

set_include_path(get_include_path() . PATH_SEPARATOR . __DIR__ . '/');
    
21.01.2014 / 15:11
0

There are several ways to work around this. Some are iterative, but this comes at a price in terms of performance. I would do something similar to proposes Calebe Oliveira, however without using the __autoload function, because, according to the manual :

  

Tip    spl_autoload_register() provides a more flexible alternative to 'autoloading' classes. For this reason, the use of the __autoload() function is discouraged and may be deprecated or removed in the future.

According to a comment in the manual, PHP 5.3 in you just have to create a directory structure that corresponds to the structure of your namespaces , and include the following at the beginning of your code, once:

spl_autoload_extensions(".php"); // comma-separated list
spl_autoload_register();
    
20.01.2014 / 15:12
-2

Cache on a Map

One way to do this is to store a class cache by name in array . I did a basic implementation:

function list_classes($dir, $array = array()){
    $files = scandir($dir);
    foreach($files as $f){
        if($f != '.' && $f != '..'){
            if (strcmp(pathinfo($f)['extension'], 'php') == 0) {
                $array[basename($f, '.php')] = $f;
            } else if(is_dir($dir.'/'.$f)) {
                list_classes($dir.'/'.$f, $array);
            }
      }
    }
    return $array;
}


$classes_cache = list_classes(dirname(__FILE__));
var_dump($classes_cache);

The above code recursively lists the files .php of the current directory, including subdirectories and stores in a array (or map) whose index (or key) is the filename without the extension.

Example, given a list_classes('classes') call from main.php :

/
  main.php
  /classes
      Class1.php
      Class2.php
      /other_classes
          Class3.php

The result of the array would be:

{
  'Class1' => 'Class1.php',
  'Class2' => 'Class2.php',
  'Class3' => 'other_classes/Class3.php'
}

Finally, by creating this global cache, just use it in your autoload method.

However, there will be problems if there are files with the same name in different directories. In this case, it would be interesting to add a check if the item already exists in array and issue an error or warning.

In addition, if there are too many folders and directories, this can affect script performance a bit, but it will only be done once. So whether or not this technique is worth it depends on how many times the autoload method will be called.

Directory List

A second approach is to create a array of directories and search if the class exists in each of them. Note that the order of array will dictate search priority.

Here's an example (based on SO ):

function __autoload($class_name) {

    $array_paths = array(
        'classes/', 
        'classes/site'
    );

    foreach($array_paths as $path) {
        $file = sprintf('%s%s/class_%s.php', dirname(__FILE__), $path, $class_name);
        if(is_file($file)) {
            include_once $file;
        } 

    }
}

The array of directories could be automatically loaded with an algorithm similar to the one above:

$array_paths = glob(dirname(__FILE__).'/../*', GLOB_ONLYDIR);

In this way, it is not necessary to go through all the subdirectories, but with each class load it will be necessary to look at the filesystem.

Naming Pattern

Another technique used by some frameworks, such as Zend Framework , is to place an underline in the class name to represent the path from a home directory. For example, the class Animal_Cachorro would rub in the /Animal directory.

Here is a sample code (based on SO ):

function __autoload($class_name) {
    $filename = str_replace('_', DIRECTORY_SEPARATOR, strtolower($class_name)).'.php';
    $file = dirname(__FILE__).'/'.$filename;
    if (!file_exists($file)) return FALSE;
    include $file;
}

This is the most straightforward and best performing method, as only one file system crash is done.

However, from my point of view, it "dirty" the name of the classes. It does not seem good practice to mix the directory structure with the names of your classes just to make it easier to build frameworks and utilities.

    
16.01.2014 / 17:36
-3

SPL has class DirectoryIterator to list directories and files. The idea is to get the directories below classes and return them as an array and then list all the files in each folder.

function listarDiretorios($dirInicial){
        $dir = new  DirectoryIterator($dirInicial);

        $dirs = array();
        foreach ($dir as $item){
            if(!$item->isDot() && $item->isDir()){
                $dirs[] = $item->getFileName();
            }
        }
        return $dirs;
}

function listarArquivos($raiz, $dirs){
    $files = array();
    foreach ($dirs as $item){
        $dir = new  DirectoryIterator($raiz.$item);
        foreach ($dir as $subitem){
            if(!$subitem->isDot() && $subitem->isFile()){
                $files[$item][] = $subitem->getFileName();
            }
        }

    }
    return $files;
}

use:

//essas constantes podem ser definados em um arquivo config.
define('PATH', dirname(dirname(__FILE__)));
define('CLASSES_PATH', dirname(__FILE__). DIRECTORY_SEPARATOR. 'classes'.DIRECTORY_SEPARATOR);

$dir = listarDiretorios(CLASSES_PATH);
$files = listarArquivos(CLASSES_PATH, $dir);
    
16.01.2014 / 17:24