Optimize function to include classes also looking in sub-directories

Asked

Viewed 1,562 times

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 subdirectory is created to organize the project classes, I have to edit this file and include a check for that subdirectory:

/* 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 base directory classes but also in any of the existing sub-directories?

  • Why not use Autopload? Very simple and practical, not to mention that your code will be within a standard (PSR).

  • @Hello Brayan, it’s an idea. If you can elaborate an answer with this solution and an example of how it works!

  • @Zuul, I’ve been following the updates of this question. There are plenty of options. If none of the answers pleased you, it wouldn’t be good to give us a feedback on that so that we can improve them?

  • 1

    @utluiz In fact, only today I am testing each of the options presented to see which "behaves" best, I will vote and assign the bonus within 3 days ;) Until then, or at least without completing the tests, I cannot say that the conditions for this are not met!

5 answers

4

Do so:

Archive at the root:

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 at root:

namespace Root;

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

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

namespace Root\foo;

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

Output when executing the first code:

Raiz Tree

So you use namespace to autoload, combining namespace with physical path, as I had commented. 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 on folders and files, it would greatly impair its performance.

  • No doubt if Namespaces can be used (legacy systems) is the best alternative.

  • 2

    Those who voted 'no' are asked to explain why in the comments.

  • I updated the autoload script according to the current recommended PHP specifications.

  • Composer’s autoload PSR-4 works exactly the same. Documentation

2

You can use the 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 . '../');

OR

set_include_path(get_include_path() . PATH_SEPARATOR . __DIR__ . '/');
  • In addition to the solution using Namespaces, this is the best alternative in my opinion, but if I remember correctly there is a limitation when using set_include_path. In the case of Windows you can set a path+file name of at most 260 characters, moreover I’m not sure but I think that the very definition of concatenated paths also has limit. This way you will almost always have problems using this solution.

  • "By default, it checks all include paths by files formed by the class name in lower case plus file extensions. inc and . php" - This would not mean that PHP internally iterates over directories looking for files?

0

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

Tip spl_autoload_register() provides a more flexible alternative to class autoloading. For this reason, the use of the function __autoload() is discouraged and may be [rendered] obsolete or removed in the future.

According to a comment present in the manual, from PHP 5.3 onwards just create a directory structure that matches 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();

-2

Cache in a Map

One of the ways to do this is to store a class cache by name in a 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 file name without the extension.

Example, given a call list_classes('classes') 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'
}

Anyway, creating this global cache, just use it in your method of autoload.

However, there will be a problem 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 the array and issue an error or warning.

Also, if there are too many folders and directories, this may affect a little the performance of the script, but will be done only once. So whether or not this technique is worth it depends on how many times the autoload 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 array will dictate the priority of the search.

Here is an example (based on ONLY):

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 directory array could be automatically loaded with an algorithm similar to the previous one:

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

This way, it is not necessary to go through all the subdirectories, but with each class load you will need to look at the file system.

Nomenclature standard

Another technique used by some frameworks, such as Zend Framework is to put a underline in the class name to represent the path from a base directory. For example, the class Animal_Cachorro fricaria in the directory /Animal.

Follow an example code (based on ONLY):

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 direct and best performing method as it is only made a cess to the file system.

However, from my point of view, it "defiles" the class names. It does not seem to me good practice to mix the directory structure with the names of its classes just to facilitate the construction of frameworks and method utilities.

  • The first two techniques are horrible, the last one would be the best from a performance point of view. And as for soiling the class names, in the most recent versions of PHP there are namespaces, which can replace "_", so this is no justification.

  • 3

    @Wolfulus I quoted 3 possible approaches without using namespaces. I made considerations about performance, maybe you have not read it carefully. There is already another answer that talks about the question of namespace, I will not repeat it. Also, these "horrible" techniques are the only option if you don’t want or can’t use namespaces or can’t refactor all your classes. Just because there is a "better" way to do things, doesn’t mean that alternative and older techniques are important to meet specific requirements and contexts.

-3

The SPL has class Directoryiterator to list directories and files. The idea is to take the directories below classes and return 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);

Browser other questions tagged

You are not signed in. Login or sign up in order to post.