Search code examples
phpphp-5.3

Including child class requires parent class included first


I have asked a similar question to this one already but I think it was badly worded and confusing so hopefully I can make it a bit clearer.

I am programming in a native Linux file system.

I have a class of HelpTopic:

class HelpTopic extends Help{}

And a class of Help:

class Help{}

Now I go to include HelpTopic:

include('HelpTopic.php');

And even though I do not instantiate HelpTopic with new HelpTopic() PHP (in a Linux file system) still reads the class signature and tries to load Help with HelpTopic.

I do not get this behaviour from a cifs file system shared from a Windows System.

My best guess is that there is some oddity with Linux that causes PHP to react this way but not sure what.

Does anyone have any ideas or solutions to this problem?

EDIT:

I have added my loading function to show what I am doing:

public static function import($cName, $cPath = null){

    if(substr($cName, -2) == "/*"){

        $d_name = ROOT.'/'.substr($cName, 0, -2);
        $d_files = getDirectoryFileList($d_name, array("\.php")); // Currently only accepts .php

        foreach($d_files as $file){
            glue::import(substr($file, 0, strrpos($file, '.')), substr($cName, 0, -2).'/'.$file);
        }
    }else{
        if(!$cPath) $cPath = self::$_classMapper[$cName];

        if(!isset(self::$_classLoaded[$cName])){
            self::$_classLoaded[$cName] = true;
            if($cPath[0] == "/" || preg_match("/^application/i", $cPath) > 0 || preg_match("/^glue/i", $cPath) > 0){
                return include ROOT.'/'.$cPath;
            }else{
                return include $cPath;
            }
        }
        return true;
    }
}

I call this by doing glue::inmport('application/models/*'); and it goes through including all the models in my app. Thing is PHP on a linux based file system (not on cifs) is trying to load the parents of my classes without instantiation.

This is a pretty base function that exists in most frameworks (in fact most of this code is based off of yiis version) so I am confused why others have not run into this problem.


Solution

  • And even though I do not instantiate HelpTopic with new HelpTopic() PHP still reads the class signature and tries to load Help with HelpTopic.

    Correct.

    In order to know how to properly define a class, PHP needs to resolve any parent classes (all the way up) and any interfaces. This is done when the class is defined, not when the class is used.

    You should probably review the PHP documentation on inheritance, which includes a note explaining this behavior:

    Unless autoloading is used, then classes must be defined before they are used. If a class extends another, then the parent class must be declared before the child class structure. This rule applies to class that inherit other classes and interfaces.

    There are two ways to resolve this problem.

    First, add a require_once at the top of the file that defines the child class that includes the file defining the parent class. This is the most simple and straight-forward way, unless you have an autoloader.

    The second way is to defione an autoloader. This is also covered in the documentation.


    The ... thing ... you're using there is not an autoloader. In fact, it's a horrible abomination that you should purge from your codebase. It's a performance sap and you should not be using it. It also happens to be the thing at fault.

    We don't have the definition of getDirectoryFileList() here, so I'll assume it uses either glob() or a DirectoryIterator. This is the source of your problem. You're getting the file list in an undefined order. Or, rather, in whatever order the underlying filesystem wants to give to you. On one machine, the filesystem is probably giving you Help.php before HelpTopic.php, while on the other machine, HelpTopic.php is seen first.

    At first glance, you might think this is fixable with a simple sort, but it's not. What happens if you create a Zebra class, and then later need to create an AlbinoZebra that inherits from it? No amount of directory sorting is going to satisfy both the "load ASCIIbetical" and the "I need the Zebra to be first" requirements.

    Let's also touch on the performance aspect of the problem. On every single request, you're opening a directory and reading the list of files. That's one hell of a lot of stat calls. This is slow. Very slow. Then, one by one, regardless of whether or not you'll need them, you're including the files. This means that PHP has to compile and interpret every single one of them. If you aren't using a bytecode cache, this is going to utterly destroy performance if the number of files there ever grows to a non-trivial number.

    A properly constructed autoloader will entirely mitigate this problem. Autoloaders run on demand, meaning that they'll never attempt to include a file before it's actually needed. Good-performing autoloaders will know where the class file lives based on the name alone. In modern PHP, it's accepted practice to name your classes such that they'll be found easily by an autoloader, using either namespaces or underscores -- or both -- to map directory separators. (Meaning namespace \Models; class Help or class Models_Help would live in Models/Help.php)

    Unfortunately most examples won't be useful here, as I don't know what kind of weird things your custom framework does. Take a peek at the Zend Framework autoloader, which uses prefix registration to point class prefixes (Model_) at directories.