7
votes

How does PHP's spl_autoload_register resolve circular dependencies with require_once?

Circular dependencies can be resolved some cases, but not all. Let's start with an example of when it fails. Suppose we have three classes defined in separate files:

cat.php

class Cat extends Animal {}

animal.php

require_once('cat.php');
class Animal extends Creature {}

creature.php

class Creature {}

Let's say we also have a script that has an autoloader and creates an instance of Animal:

run.php

spl_autoload_register(function($className) {
    require_once("$className.php");
});

$a = new Animal();

Running this script with "php run.php" will result in a PHP Fatal error:

PHP Fatal error: Class 'Animal' not found in .../Cat.php

I think this makes intuitive sense to me, because of the circular dependency between Animal and Cat:

  1. The autoloader attempts to load animal.php
  2. Loading animal.php causes cat.php to load due to the require_once()
  3. Loading cat.php fails becasue it extends Animal, and Animal can't be loaded twice by the autoloader.

Here are some modifications to ensure that we don't get a fatal

  1. animal.php should not have a require_once('cat.php')
    • This seems like the best solution as it effectively removes the circular dependency between Animal and Cat
  2. Animal class should not extend Creature
  3. Instead of using the Autoloader in run.php, just have a require_once() for both animal.php and creature.php

Questions:

  1. Why does #2 work? Why does Animal not extending Creature result in the resolution of the circular dependency between Animal and Cat?
  2. Why does #3 work? Isn't the autoloader just doing a require_once() under the hood?

The complete code (with some additional logging) from this examples can be found here

3
Could you add-in a directory structure of where the files are located, relative to the root? - Xorifelse
In this example, all the files are in the same folder. All working code is in the example linked at the very end of the post. - Richard Pon
Learn how to 'so' (StackOverflow), we scratch your back, you scratch our by upvoting or mark it as answer if it resolved the issue. The next question you may post may not receive answers as users might track your profile to see if you reward them. - Xorifelse

3 Answers

4
votes

Since your autoloader does - what its name says - auto load your classes, you dont need any other require then the one in the autoloader function.

If you use require_once instead of require in it, it will still only load it once, no matter if you extending from it or just create an object.

So just use the code you posted in your question and remove the require_once() in your animal.php since the autoloader already requires it.


Side note: If you dont want to deal with creating your own autoloader, you could use the composer autoloader. Its easy to install and very useful, because it deals with sub directories and makes you follow a strict namespace convention.

If you want to do so, you need to install composer first. Then, you create a file called composer.json in your base directory with following content

{
    "autoload": {
        "psr-4": { "YourProject\\": "src/" }
    }
}

You then need to execute following command in your command line:

cd path/to/your/project
composer dump-autoload

If you have done it put your classes in basedirectory/src Note that you now have to give all classes a namespace, in this case if would be namespace YourProject. You are finally done!

Now go in your base directory and create a file, lets call it index.php:

require_once ('vendor/autoloader.php');

$a = new YourProject\Animal();

Sorry for long side note, sir!

0
votes

Remove require_once('cat.php'); from animal.php

If an object does not exist, the autoloader is called making a separate call each object that does not exist. So if every object has its own file name you are alright.

The codeflow currently go's a little like this:

$a = new Animal();

#animal does not exist, spl_autoload('animal');
#include cat.php
#creature does not exist, spl_autoload('creature')

However, this go's by script path location. Meaning cat.php is perhaps not in the same directory as animal.php, thats why the file may not exist.

However, for a more dynamic autoloading system I recommend the the PSR-4 autoloader. It uses namespaces to determine the location of the class within the directory tree.

new \path\to\Animal();

Would cause the auto loader include the file of /path/to/animal.php

0
votes

The reason your example is failing is that you're attempting to use the definition of Animal before it's defined.

First, you attempt to autoload Animal:

$a = new Animal(); // This triggers the require_once of animal.php

In animal.php, you require cat.php before you define the Animal class

require_once('cat.php');    // This file will be fully evaluated before the next line
class Animal extends Creature {}  // Since the prior line relied on the existence of Animal, you get a compile error.

The Animal class can't be autoloaded inside cat.php, since you already called require_once on its containing file; it just hasn't fully been evaluated.

As others have said, this is a result of mixing direct require_once calls with an autoloader. As another workaround, you could move your require_once('cat.php') to the end of the file, and it should work as you're expecting it to.