PHP的spl_autoload_register如何通过require_once解决循环依赖问题?

7

我可以帮忙翻译中文。以下是需要翻译的内容:

PHP的spl_autoload_register如何通过require_once解决循环依赖?

在某些情况下,循环依赖可以被解决,但并非所有情况都适用。让我们从一个失败的例子开始。假设我们有三个类定义在不同的文件中:

cat.php

class Cat extends Animal {}

animal.php

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

creature.php

class Creature {}

假设我们还有一个自动加载器并创建Animal实例的脚本: run.php
spl_autoload_register(function($className) {
    require_once("$className.php");
});

$a = new Animal();

使用"php run.php"运行此脚本将导致PHP致命错误:

PHP致命错误:在.../Cat.php中找不到类'Animal'

我认为这对我来说很直观,因为动物和猫之间存在循环依赖:

  1. 自动加载程序尝试加载animal.php
  2. 由于require_once()的原因,加载animal.php会导致cat.php被加载
  3. 加载cat.php失败,因为它扩展了Animal,并且Animal不能被自动加载程序加载两次。

以下是一些修改,以确保我们不会出现致命错误

  1. animal.php不应该有require_once('cat.php')
    • 这似乎是最好的解决方案,因为它有效地消除了Animal和Cat之间的循环依赖关系
  2. Animal类不应该扩展Creature
  3. 不要在run.php中使用Autoloader,只需对animal.php和creature.php使用require_once()

问题:

  1. 为什么#2起作用?为什么Animal不扩展Creature会导致Animal和Cat之间的循环依赖关系得到解决?
  2. 为什么#3起作用?自动加载程序不只是在幕后执行require_once()吗?

此示例的完整代码(带有一些附加日志记录)可以在此处找到。


你能否添加一个目录结构,以相对于根目录的方式指明文件所在位置? - Xorifelse
在这个例子中,所有的文件都在同一个文件夹中。所有的工作代码都在本文末尾链接的示例中。 - Richard Pon
学习如何使用'SO'(StackOverflow),我们互相帮助,您可以通过点赞或将其标记为答案来回报我们。如果您不奖励他们,下一个问题可能无法得到答案,因为用户可能会跟踪您的个人资料。 - Xorifelse
3个回答

4
由于自动加载器会自动加载您的类,因此您不需要任何其他要求,只需在自动加载器函数中使用一个即可。
如果您在其中使用require_once而不是require,则仍将仅加载一次,无论您是否从中扩展或仅创建对象。
因此,只需使用您在问题中发布的代码并删除animal.php中的require_once(),因为自动加载器已经要求了它。

附注:如果您不想处理创建自己的自动加载程序,可以使用composer自动加载程序。它很容易安装并且非常有用,因为它处理子目录并使您遵循严格的命名空间约定。

如果您想这样做,首先需要安装composer。然后,在您的基本目录中创建一个名为composer.json的文件,并填入以下内容

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

您需要在命令行中执行以下命令:
cd path/to/your/project
composer dump-autoload

如果您已经完成了,请将您的类放在basedirectory/src中。请注意,现在您需要给所有类都加上命名空间,例如:namespace YourProject。最后,您已经完成了!现在进入您的基本目录并创建一个文件,我们称之为index.php:
require_once ('vendor/autoloader.php');

$a = new YourProject\Animal();

对于这个长的侧注,非常抱歉,先生!


非常感谢您的解释和建议。上面的示例实际上是从一个更复杂的代码库中简化出来的。在animal.php中使用require_once('cat.php')的最初原因是,animal.php和cat.php中都定义了全局常量。 - Richard Pon
不用客气。但请记住,如果您创建自己的自动加载程序,您必须自己处理子目录。如果我的回答对您有帮助,请考虑接受它! - Manuel Mannhardt
你的回答对于那些想要了解更多关于自动加载和最佳实践的人肯定是有用的。我真正想知道的是为什么PHP只能有时候通过spl_autoload_register()使用自动加载来解决这些循环依赖关系。 - Richard Pon

0

从animal.php中删除require_once('cat.php');

如果对象不存在,则调用自动加载程序,为每个不存在的对象进行单独调用。因此,如果每个对象都有自己的文件名,那么就没问题了。

目前代码流程大致如下:

$a = new Animal();

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

然而,这是根据脚本路径位置来确定的。这意味着cat.php可能不在与animal.php相同的目录中,因此该文件可能不存在。

但是,对于更动态的自动加载系统,我建议使用PSR-4 autoloader。它使用命名空间来确定目录树中类的位置。

new \path\to\Animal();

是否会导致自动加载器包含 /path/to/animal.php 文件?


从animal.php中删除require_once绝对是消除循环依赖的一种方法。不清楚的是,为什么PHP只能在某些情况下解决这种循环依赖,比如Animal没有扩展Creature。 - Richard Pon

0

你的示例失败的原因是你试图在定义 Animal 之前使用它。

首先,你尝试自动加载 Animal:

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

在 animal.php 中,在定义 Animal 类之前,你需要先引入 cat.php。
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.

Animal类无法在cat.php中自动加载,因为您已经在其包含文件上调用了require_once;它还没有完全被评估。

正如其他人所说,这是混合直接require_once调用和自动加载程序的结果。作为另一种解决方法,您可以将require_once('cat.php')移动到文件末尾,然后它应该按照您的期望工作。


很抱歉,问题出在文件系统相对于调用脚本的位置。如果“自动加载器”脚本位于另一个目录中,我假设它与执行脚本的目录不存在相对关系,因此“cat.php”也无法找到。 - Xorifelse
@Sam 谢谢你的解释。那肯定有道理,但是你知道如果 Animal 没有扩展 Creature 为什么这会起作用吗? - Richard Pon
我不确定;我需要重新创建它才能看出发生了什么。 - Sam Dufel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接