开放的编程资料库

当前位置:我爱分享网 > PHP教程 > 正文

将 FilterIterator 应用于目录迭代

我目前正在为ZendFramework2.0中的自动加载替代方案进行研究和原型设计。我正在研究的一种方法涉及创建显式类/文件映射;这些往往比使用include_path快得多,但确实需要一些额外的设置。

我生成地图的算法非常简单:

  • 扫描文件系统中的PHP文件
  • 如果文件不包含接口、类或抽象类,则跳过它。
  • 如果包含,则获取其声明的命名空间和类名

问题是使用什么实现方法。

我很了解RecursiveDirectoryIterator,并计划使用它。但是,我也听说过FilterIterator,想知道我是否可以以某种方式将其绑定。最后,我可以,但解决方案并不明显。

我认为我能做的事

FilterIterator是一个抽象类。扩展它时,必须定义一个accept()方法。

class FooFilter extends FilterIterator
{
    public function accept()
    {
    }
}

在该方法中,您通常会检查$this->current()返回的任何内容,然后返回布尔值truefalse,取决于你是否要保留它。

class FooFilter extends FilterIterator
{
    public function accept()
    {
        $item = $this->current();

        if ($someCriteriaIsMet) {
            return true;
        }

        return false;
    }
}

我稍后会详细介绍我的标准的机制;现在重要的是知道FilterIterator允许您限制迭代器返回的结果。

我最初认为我可以简单地将DirectoryIteratorRecursiveDirectoryIterator传递到我的过滤实例。这在前一种情况下有效,因为它只有一层深。然而,对于后者,它只会为所有匹配的类返回第一个目录级别——也就是说,如果我在Zend/Controller上运行它,我会为下的每个类找到一个匹配项Zend/Controller/Action/Helper/,但它会简单地返回Zend/Controller/Action作为匹配项。这当然没有用。

然后我发现了RecursiveFilterIterator,它看起来可以解决递归问题。但是,我发现出现了两个结果之一:如果至少有一个项目匹配,我将收到整个子树,或者如果找到的第一个项目不符合条件,它将跳过整个子树。没有中间立场。

解决方案

这个解决方案非常简单和优雅,我偶然发现了它:将我的RecursiveIteratorIterator实例传递给FilterIterator

$rdi      = new RecursiveDirectoryIterator($somePath);
$rii      = new RecursiveIteratorIterator($rdi);
$filtered = new FooFilter($rii);

真的。就是这么简单——但是,如前所述,并不明显。它还需要在我的过滤器中进行一些细微的更改——而不是使用current(),我需要先拉出“内部”迭代器实例:$this->getInnerIterator()->current()。当我检查过滤器实现时,我在下面展示了一个例子。

至于我的标准,我有几个选择。我可以require_once文件,并使用反射API检查类以确定它是接口、抽象类还是类,以及确定命名空间。然而,我不能100%确定该文件将包含一个类,所以这似乎有点过分了。由于使用了反射,而且性能极差。

下一个选项是简单地将文件内容放入变量中,并使用正则表达式。我喜欢正则表达式,但在这种情况下,感觉我可能会得到一些误报。此外,由于其中一些文件可能非常大,我再次担心性能影响—我不想永远等待生成这些地图。

我采用的解决方案是使用分词器检查文件。标记化速度非常快,而且分析标记也非常简单。

我决定将检测到的命名空间和类名存储为返回的SplFileInfo对象的公共属性;这使得遍历整个集合并利用该信息变得简单。另外,因为我有SplFileInfo对象,所以我已经有了我需要的路径。

我的实现是这样的:

/** @namespace */
namespace Zend\File;

// import SPL classes/interfaces into local scope
use DirectoryIterator,
    FilterIterator,
    RecursiveIterator,
    RecursiveDirectoryIterator,
    RecursiveIteratorIterator;

/**
 * Locate files containing PHP classes, interfaces, or abstracts
 * 
 * @package    Zend_File
 * @license    New BSD {@link http://framework.zend.com/license/new-bsd}
 */
class ClassFileLocater extends FilterIterator
{
    /**
     * Create an instance of the locater iterator
     * 
     * Expects either a directory, or a DirectoryIterator (or its recursive variant) 
     * instance.
     * 
     * @param  string|DirectoryIterator $dirOrIterator 
     * @return void
     */
    public function __construct($dirOrIterator = '.')
    {
        if (is_string($dirOrIterator)) {
            if (!is_dir($dirOrIterator)) {
                throw new InvalidArgumentException('Expected a valid directory name');
            }

            $dirOrIterator = new RecursiveDirectoryIterator($dirOrIterator);
        }
        if (!$dirOrIterator instanceof DirectoryIterator) {
            throw new InvalidArgumentException('Expected a DirectoryIterator');
        }

        if ($dirOrIterator instanceof RecursiveIterator) {
            $iterator = new RecursiveIteratorIterator($dirOrIterator);
        } else {
            $iterator = $dirOrIterator;
        }

        parent::__construct($iterator);
        $this->rewind();
    }

    /**
     * Filter for files containing PHP classes, interfaces, or abstracts
     * 
     * @return bool
     */
    public function accept()
    {
        $file = $this->getInnerIterator()->current();

        // If we somehow have something other than an SplFileInfo object, just 
        // return false
        if (!$file instanceof \SplFileInfo) {
            return false;
        }

        // If we have a directory, it's not a file, so return false
        if (!$file->isFile()) {
            return false;
        }

        // If not a PHP file, skip
        if ($file->getBasename('.php') == $file->getBasename()) {
            return false;
        }

        $contents = file_get_contents($file->getRealPath());
        $tokens   = token_get_all($contents);
        $count    = count($tokens);
        $i        = 0;
        while ($i < $count) {
            $token = $tokens[$i];

            if (!is_array($token)) {
                // single character token found; skip
                $i++;
                continue;
            }

            list($id, $content, $line) = $token;

            switch ($id) {
                case T_NAMESPACE:
                    // Namespace found; grab it for later
                    $namespace = '';
                    $done      = false;
                    do {
                        ++$i;
                        $token = $tokens[$i];
                        if (is_string($token)) {
                            if (';' === $token) {
                                $done = true;
                            }
                            continue;
                        }
                        list($type, $content, $line) = $token;
                        switch ($type) {
                            case T_STRING:
                            case T_NS_SEPARATOR:
                                $namespace .= $content;
                                break;
                        }
                    } while (!$done && $i < $count);

                    // Set the namespace of this file in the object
                    $file->namespace = $namespace;
                    break;
                case T_ABSTRACT:
                case T_CLASS:
                case T_INTERFACE:
                    // Abstract class, class, or interface found

                    // Get the classname
                    $class = '';
                    do {
                        ++$i;
                        $token = $tokens[$i];
                        if (is_string($token)) {
                            continue;
                        }
                        list($type, $content, $line) = $token;
                        switch ($type) {
                            case T_STRING:
                                $class = $content;
                                break;
                        }
                    } while (empty($class) && $i < $count);

                    // If a classname was found, set it in the object, and 
                    // return boolean true (found)
                    if (!empty($class)) {
                        $file->classname = $class;
                        return true;
                    }
                    break;
                default:
                    break;
            }
            ++$i;
        }

        // No class-type tokens found; return false
        return false;
    }
}

注意:此类中抛出的异常在同一命名空间中定义;我将如何实现它们由您自行想象。

迭代更快

我发现的下一个技巧是iterator_apply()的形式。通常,当我使用迭代器时,我会使用foreach,因为,嗯,这就是你所做的。但是在查看本练习的各种迭代器时,我偶然发现了这颗宝石。

基本上,您传递迭代器、回调和要传递给回调的参数。与FilterIterator一样,您不会获得迭代器返回的实际项目,因此在大多数用例中,您传递迭代器本身:

iterator_apply($it, $callback, array($it));

然后您可以从迭代器本身获取当前值和/或键:

public function process(Iterator $it)
{
    $value = $it->current();
    $key   = $it->key();
    // ...
}

虽然您可以使用任何有效的PHP回调,但我发现最有趣的解决方案是使用闭包,因为它允许您预先定义所有内容:

iterator_apply($it, function() use ($it) {
    $value = $it->current();
    $key   = $it->key();
    // ...
});

如果您通过use语句传入本地值,则可以进行一些聚合:

$map = new \stdClass;
iterator_apply($it, function() use ($it, $map) {
    $file = $it->current();
    $namespace = !empty($file->namespace) ? $file->namespace . '\' : '';
    $classname = $namespace . $file->classname;
    $map->{$classname} = $file->getPathname();
});

这不仅是一种很好、简洁的技术,而且速度也非常快——我发现它比使用传统的foreach循环快200%–300%。显然它不能在所有情况下使用,但如果您可以使用它,您可能应该使用它。

因此,如果您还没有准备好,请开始使用FilterIteratoriterator_apply()—这两者为您的应用程序提供了巨大的可能性和功能。

未经允许不得转载:我爱分享网 » 将 FilterIterator 应用于目录迭代

感觉很棒!可以赞赏支持我哟~

赞(0) 打赏