4

我正在更新我不久前编写的框架。我想采用新标准,例如命名空间和使用自动加载功能。

现在我的框架有一个非常基本但功能强大的自动加载功能,如下所示:

protected function runClasses()
{
    $itemHandler = opendir( V_APP_PATH );
    while( ( $item = readdir( $itemHandler ) ) !== false )
    {
        if ( substr( $item, 0, 1 ) != "." )
        {
            if( !is_dir( $item ) && substr( $item, -10 ) == ".class.php" )
            {
                if( !class_exists( $this->registry->$item ) )
                {
                    require_once( V_APP_PATH . $item );
                    $item  = str_replace( ".class.php", "", $item );
                    $this->registry->$item = new $item( $this->registry );
                }
            }
        }
    }   
}

正如您在代码中看到的那样,此函数仅限于单个文件夹,但它的优点是将类加载到我的注册表中,允许我通过执行类似于$this->registry->Class->somevar我的功能的操作来访问其他文件中的特定类需要。

我需要/想要完成的是使用自动加载器功能,但该功能不限于单个文件夹,而是能够浏览多个文件夹并实例化所需的类。

我只有一些测试文件,这是我当前的文件结构:

文件结构

对于 MyClass2 我有:

namespace model;
Class MyClass2 {
    function __construct() {
        echo "MyClass2 is now loaded!";
    }
}

对于 MyClass1 我有:

Class MyClass1 {
function __construct() {
         echo "MyClass1 is now loaded!<br />";
    }
}

对于自动加载,我有:

function __autoload( $className ) {
    $file = $className . ".php";
    printf( "%s <br />", $file );
    if(file_exists($file)) {
        require_once $file;
    }
}

$obj = new MyClass1();
$obj2 = new model\MyClass2(); 

我的问题是设置方式,它找不到 MyClass2 的文件,所以我想知道我做错了什么,其次,有没有像我的第一个“自动加载”函数一样不需要指定命名空间的方法在自动加载文件中并将其分配给我的注册表?

抱歉这么长的问题,但非常感谢任何帮助。

4

2 回答 2

7

我在这里看到两件事。

第一个使您的问题有点复杂。您想使用命名空间,但您当前的配置是通过文件系统进行的。到目前为止,类定义文件的文件名不包含命名空间。所以你不能像你实际做的那样继续。

第二个是您没有 PHP 自动加载所涵盖的内容,您只需加载一组定义的类并将其注册到注册表。

我不确定您是否需要在这里自动加载 PHP。当然,将两者结合在一起看起来很有希望。解决第一点可能会帮助你解决后面的问题,所以我建议先从它开始。

让我们让隐藏的依赖关系更加明显。在您当前的设计中,您拥有三样东西:

  1. 在注册表中注册对象的名称。
  2. 包含类定义的文件名。
  3. 类本身的名称。

2. 和 3. 的值合二为一,您从文件名中解析类本身的名称。正如所写,名称空间现在使这变得复杂。解决方案很简单,您可以从包含此信息的文件中读取,而不是从目录列表中读取。一个轻量级的配置文件格式是 json:

{
    "Service": {
        "file":  "test.class.php",
        "class": "Library\\Of\\Something\\ConcreteService"
    }
}

这现在包含三个所需的依赖项,以通过名称将类注册到注册表中,因为文件名也是已知的。

然后,您允许在注册表中注册类:

class Registry
{
    public function registerClass($name, $class) {
        $this->$name = new $class($this);
    }
}

并为 json 格式添加一个加载器类:

interface Register
{
    public function register(Registry $registry);
}

class JsonClassmapLoader implements Register
{
    private $file;

    public function __construct($file) {

        $this->file = $file;
    }

    public function register(Registry $registry) {

        $definitions = $this->loadDefinitionsFromFile();

        foreach ($definitions as $name => $definition) {
            $class = $definition->class;
            $path  = dirname($this->file) . '/' . $definition->file;

            $this->define($class, $path);

            $registry->registerClass($name, $class);
        }
    }

    protected function define($class, $path) {

        if (!class_exists($class)) {
            require($path);
        }
    }

    protected function loadDefinitionsFromFile() {

        $json = file_get_contents($this->file);
        return json_decode($json);
    }
}

这里没有什么神奇之处,json文件中的文件名是相对于它的目录的。如果尚未定义类(此处触发 PHP 自动加载),则需要该类的文件。完成后,该类将通过其名称注册:

$registry = new Registry();
$json     = new JsonClassmapLoader('path/registry.json');
$json->register($registry);

echo $registry->Service->invoke();  # Done.

这个例子也很简单,而且很有效。这样第一个问题就解决了。

第二个问题是自动加载。当前的变体和您以前的系统也确实隐藏了其他内容。有两件核心的事情要做。一种是实际加载类定义,另一种是实例化对象。

在您的原始示例中,从技术上讲,自动加载不是必需的,因为在注册表中注册对象时,它也是实例化的。您这样做也是为了将注册表分配给它。我不知道您是否只是因为这个而这样做,或者这是否只是以这种方式发生在您身上。你在你的问题中写下你需要那个。

因此,如果您想将自动加载带入您的注册表(或延迟加载),这会有所不同。由于您的设计已经搞砸了,让我们继续在上面添加更多魔法。您希望将注册表组​​件的实例化推迟到第一次使用它的那一刻。

在注册表中,组件的名称比它的实际类型更重要,这已经是非常动态的并且只是一个字符串。为了推迟组件创建,类不是在注册时创建,而是在访问时创建。这可以通过使用__get需要一种新型注册表的功能来实现:

class LazyRegistry extends Registry
{
    private $defines = [];

    public function registerClass($name, $class)
    {
        $this->defines[$name] = $class;
    }

    public function __get($name) {
        $class = $this->defines[$name];
        return $this->$name = new $class($this);
    }
}

用法示例再次完全相同,但是注册表的类型已更改:

$registry = new LazyRegistry();
$json     = new JsonClassmapLoader('path/registry.json');
$json->register($registry);

echo $registry->Service->invoke();  # Done.

所以现在具体服务对象的创建被推迟到第一次访问。然而,这仍然不是自动加载。类定义的加载已经在 json 加载器中完成。已经使事物变得充满活力和魔力不会因此而产生,但事实并非如此。我们需要一个自动加载器,每个类都应该在第一次访问对象时启动。例如,我们实际上希望能够在应用程序中包含可能永远不会被注意到的烂代码,因为我们不在乎它是否被使用。但是我们不想把它加载到内存中。

对于自动加载,您应该知道spl_autoload_register哪些允许您拥有多个自动加载功能。这通常有用的原因有很多(例如,假设您使用第三方软件包),但是这个名为Registry您的动态魔术盒,它只是完成这项工作的完美工具。一个直接的解决方案(并且不做任何过早的优化)是为我们在注册表定义中的每个类注册一个自动加载器函数。这需要一种新类型的加载器,而自动加载器函数只是两行代码左右:

class LazyJsonClassmapLoader extends JsonClassmapLoader
{
    protected function define($class, $path) {

        $autoloader = function ($classname) use ($class, $path) {

            if ($classname === $class) {
                require($path);
            }
        };

        spl_autoload_register($autoloader);
    }
}

用法示例也没有太大变化,只是加载器的类型:

$registry = new LazyRegistry();
$json     = new LazyJsonClassmapLoader('path/registry.json');
$json->register($registry);

echo $registry->Service->invoke(); # Done.

现在你可以偷懒了。这意味着,实际上要再次更改代码。因为您想要远程将这些文件实际放入该特定目录的必要性。啊等等,这就是你要的,所以我们把它留在这里。

否则,请考虑使用可在首次访问时返回实例的可调用对象来配置注册表。这通常会使事情变得更加灵活。自动加载 - 如图所示 - 独立于如果您实际上可以离开基于目录的方法,那么您不再关心代码被打包的具体位置(http://www.getcomposer.org/)。

完整的整个代码示例(不带registry.jsonand test.class.php):

class Registry
{
    public function registerClass($name, $class) {
        $this->$name = new $class($this);
    }
}

class LazyRegistry extends Registry
{
    private $defines = [];

    public function registerClass($name, $class) {
        $this->defines[$name] = $class;
    }

    public function __get($name) {
        $class = $this->defines[$name];
        return $this->$name = new $class($this);
    }
}

interface Register
{
    public function register(Registry $registry);
}

class JsonClassmapLoader implements Register
{
    private $file;

    public function __construct($file) {

        $this->file = $file;
    }

    public function register(Registry $registry) {

        $definitions = $this->loadDefinitionsFromFile();

        foreach ($definitions as $name => $definition) {
            $class = $definition->class;
            $path  = dirname($this->file) . '/' . $definition->file;

            $this->define($class, $path);

            $registry->registerClass($name, $class);
        }
    }

    protected function define($class, $path) {

        if (!class_exists($class)) {
            require($path);
        }
    }

    protected function loadDefinitionsFromFile() {

        $json = file_get_contents($this->file);
        return json_decode($json);
    }
}

class LazyJsonClassmapLoader extends JsonClassmapLoader
{
    protected function define($class, $path) {

        $autoloader = function ($classname) use ($class, $path) {

            if ($classname === $class) {
                require($path);
            }
        };

        spl_autoload_register($autoloader);
    }
}

$registry = new LazyRegistry();
$json     = new LazyJsonClassmapLoader('path/registry.json');
$json->register($registry);

echo $registry->Service->invoke(); # Done.

我希望这会有所帮助,但这主要是在沙盒中玩,你迟早会粉碎它。您真正想了解的是控制反转、依赖注入以及依赖注入容器。

您拥有的注册表是某种气味。这一切都充满了魔力和活力。您可能认为这对于开发或系统中的“插件”(它很容易扩展)很酷,但是您应该保持其中的对象数量较少。

Magic 可能很难调试,因此您可能需要先检查 json 文件的格式是否对您的情况有意义,以防止出现第一手配置问题。

还要考虑传递给每个构造函数的注册表对象不是一个参数,而是代表动态数量的参数。这迟早会开始产生副作用。如果您使用注册表过多,那么越快越好。这些副作用会让你花费很多维护成本,因为设计上这已经有缺陷了,所以你只能通过努力工作、对回归进行大量集成测试等来控制它。

不过,亲身经历,只是一些看法,不是你后来告诉我的,我没注意到。

于 2012-09-22T10:39:00.597 回答
2

对于您的第二个问题:不鼓励使用 __autoload,应将其替换为 spl_autoload_register。自动加载器应该做的是拆分命名空间和类:

function __autoload( $classname )
{    
  if( class_exists( $classname, false ))
    return true;

  $classparts = explode( '\\', $classname );
  $classfile = '/' . strtolower( array_pop( $classparts )) . '.php';
  $namespace = implode( '\\', $classparts );

  // at this point you have to decide how to process further

}

根据您的文件结构,我建议基于命名空间和类名构建绝对路径:

define('ROOT_PATH', __DIR__);

function __autoload( $classname )
{    
  if( class_exists( $classname, false ))
    return true;

  $classparts = explode( '\\', $classname );
  $classfile = '/' . strtolower( array_pop( $classparts )) . '.php';
  $namespace = implode( '\\', $classparts );

  $filename = ROOT_PATH . '/' . $namespace . $classfile;
  if( is_readble($filename))
    include_once $filename;
}

我采用了 PSR0 方法,其中命名空间是路径的一部分。

于 2012-09-22T09:25:32.180 回答