2
\$\begingroup\$

I created a small working autoloader based on some of the answers and ideas found on Stack Exchange (I lost the reference). However, I don't know much about PHP (I mainly know C, a bit C++ and Java). I'm trying to create a small MVC website for school project using my own templates and JavaScript as well. I mainly created this autoloader to include class files (I'm not sure if any other files can be included with autoloader too).

Can you please check this autoloader and give your insight on what can be done better and if there are any shortcomings?

It's important to know that I don't use namespaces currently because it would overcomplicate things.

<?php


    class Autoload
    {
        private $allFiles = array();

        public function __construct() {

            $this->findDir(__DIR__, $this->allFiles);
            spl_autoload_register(array($this, 'classLoader'));
        }

        private function classLoader($class) {

            if (class_exists($class, false)) {
                return true;

            } else {
                $this->includeClass($this->allFiles, $class);
                // should I return here true as well?
            }
        }

        private function findDir($currDir, &$files) {

            $dirsInCurr = glob($currDir . "/*", GLOB_ONLYDIR);
            $this->findFiles($currDir, $files);

            //recursive call
            foreach ($dirsInCurr as $dir) $this->findDir($dir, $files);
        }

        private function findFiles($dir, &$files) {

            $fPaths = glob($dir . "/*.php");
            foreach ($fPaths as $f) $files[] = $f . "\n";

        }

        private function includeClass($files, $class) {

            foreach ($files as $f) {
                if (strpos($f, strtolower($class) . ".php")) require_once trim($f);
            }
        }
    }

    $autoload = new Autoload();

?>
\$\endgroup\$
  • \$\begingroup\$ Save your headache, I made a flexible loader myself a while ago. Clone it here at GitHub, enjoy. https://github.com/thielicious/aLoad.git \$\endgroup\$ – Thielicious Dec 18 '16 at 15:48
  • 1
    \$\begingroup\$ @Thielicious: Thank you for sharing. Though creating an autoloader and understanding it is a little headache like you describe, it helps me to learn more and understand PHP. \$\endgroup\$ – Name Dec 19 '16 at 0:38
  • \$\begingroup\$ You should not use "glob" in your autoloader, especially for recursive purpose, because it will load the entire directory even if the file is the first of the forst dir. Take a look at opendir, witch uses a file pointer. It's also highly recommanded to have a naming strategy tha matches directories to avoid full scan. \$\endgroup\$ – djleop Dec 19 '16 at 8:10
3
\$\begingroup\$

There are several ways to improve the code.

Let's start with the constructor.

  1. You should not make any assumptions about the place of a file in the current file system, because that makes later changes very hard and it hurts reusability.

    So get rid of the __DIR__ constant and send the directory of the classes as an argument to the constructor instead.

  2. Registering callbacks and finding a file are two very different tasks. They don't belong to the same class. So register a method of an instance of the autoloader from the outside instead, and remove the call to spl_autoload_register() from your class.

  3. You should use namespaces for all of your classes, interfaces and traits. This prevents collisions in case that you want to use other peoples code in your project later. Add a $namespace argument to your constructor.

In the end, your constructor shouldn't do more than setting up private members, maybe add some preparation for these. Example:

/**
 * Autoload constructor.
 *
 * @param string $namespace Pass the namespace unescaped.
 * @param string $dir
 */
public function __construct( $namespace, $dir )
{
    // Make sure it ends with a '\'.
    $namespace       = rtrim( $namespace, '\\' ) . '\\';
    $this->namespace = $namespace;
    $this->length    = strlen( $namespace );
    $this->dir       = rtrim( $dir, '/' ) . '/';
}

Now, to the actual class loader classLoader().

  1. I wouldn't call it classLoader, because it will load traits and interfaces too. Just call it load(), that's clear enought for a class named Autoload. :) And because you will register and test it directly, make it public.

  2. Do not return a value in that method. If you find a matching file, just include it and stop, return nothing otherwise. PHP will figure the rest out for you.

  3. class_exists() is pointless here, because that method will be called only if the class/trait/interface doesn't exist yet. Just remove it.

  4. The private methods called from that "parent" method don't deal with namespaces, and they are too slow. You don't need glob() here. (On a side note GLOB_ONLYDIR is not available on Solaris/Sun OS, use that constant only if portability is not a concern for your code.)

    Figure out the correct complete file path first, then check if that file exists and is readable! The latter is important, because you cannot rely on the fact that you can actually include all existing files.

So that method may be reduced to something like this:

/**
 * @param string $search
 * @return void
 */
public function load( $search )
{
    if ( strncmp( $this->namespace, $search, $this->length ) !== 0 ) {
        return;
    }

    $name = substr( $search, $this->length );
    $path = $this->dir . str_replace( '\\', '/', $name ) . '.php';

    if ( is_readable( $path ) ) {
        require $path;
    }
}

Finally to the registration. As you can see, you can reuse the class now to find files in different directories mapped to a namespace. Let's say all of your own files are living under the namespace CodeReview in the directory src/ (this is a convention in PHP).

In the main file of your application, you can include the Autoloader and register its load() method now like this:

require __DIR__ . '/src/Autoload.php';
spl_autoload_register( [ new Autoload( 'CodeReview', __DIR__ . '/src/' ), 'load' ] );

Now let's say, you have a class Response living under namespace CodeReview\Http.

namespace CodeReview\Http;

class Response {}

It should be placed under /src/CodeReview/Http/Response.php.

You can create a new instance now either with:

new CodeReview\Http\Response;

… or you put a use statement at the top of the file that is creating that instance:

use CodeReview\Http;

new Response;

Your class is much simpler now, you can test it, map it to different directories and namespaces, use it in different projects, and put it in any place you like.

|improve this answer|||||
\$\endgroup\$
  • \$\begingroup\$ @toscho- As it turns out, my own autoloader is not functioning as I intended it to work. It seems when spreading code between more folders than I originally tested, it just can't find those classes. Thank you for your comments and help! \$\endgroup\$ – Name Dec 20 '16 at 17:17
  • \$\begingroup\$ @toscho- As I have never used namespaces before, can you please give me a small example how to use them so that this autoloader can find my classes. \$\endgroup\$ – Name Dec 20 '16 at 17:22
  • \$\begingroup\$ @toscho- I have been using it now and it works great. Just one notice with this is that file names have to start with captial letters, ex. ViewEng.php. So far my files have started with lower case. Should filenames start with capital letters or should I upgrade this autoloader to find files starting with lower case? \$\endgroup\$ – Name Dec 22 '16 at 12:28
  • 1
    \$\begingroup\$ @Name The file name should match the class name. See PSR 4, a standard that is widely accepted nowadays. The implementation above follows that standard. \$\endgroup\$ – fuxia Dec 22 '16 at 14:49
  • 1
    \$\begingroup\$ @MikeiLL Yes, absolutely correct. :) \$\endgroup\$ – fuxia Mar 18 '17 at 0:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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