Symfony Finder CHANGELOG 深度解读:从 2.1 到 8.2 的演进脉络与 API 实战指南

原创2026-10-13 19:34:341,655 阅读
文章标签:后端开发工具

Symfony Finder CHANGELOG 深度解读:从 2.1 到 8.2 的演进脉络与 API 实战指南

本文以 Symfony Finder 组件官方 CHANGELOG.md 为脉络,逐版本梳理其 API 演进历史,并结合当前仓库源码(Finder.php、各 Iterator 实现与测试用例)解读每个里程碑背后的实现原理。读者将掌握 exclude() 根相对路径、useUnixPaths()、sortBy*() 系列排序、filter() 早期剪枝、.gitignore 支持等关键特性的使用方式、语义细节与适用场景。

说明:本仓库即 Symfony Finder 组件的镜像源码(含 Comparator、Iterator、Tests 等目录)。CHANGELOG 按版本倒序排列,最新特性位于顶部;本文沿时间顺序(2.1 → 8.2)正向梳理,便于理解演进因果。

一、最新版本特性:8.2 与 8.1

8.2:exclude() 支持根相对路径(root-relative)

CHANGELOG 记录 8.2 引入"通过以 / 前缀路径,在 Finder::exclude() 中支持根相对排除"。源码中的注释给出了完整语义(Finder.php):

$finder->in(__DIR__)->exclude('ruby');    // 排除任意深度下所有名为 "ruby" 的目录
$finder->in(__DIR__)->exclude('/ruby');   // 仅排除 __DIR__/ruby(搜索根下的 ruby)
$finder->in(__DIR__)->exclude('/foo/bar'); // 仅排除 __DIR__/foo/bar

其实现位于 Iterator/ExcludeDirectoryFilterIterator.php:构造函数遍历传入目录列表,以 / 开头的条目被归入 $rootPatterns 并生成锚定正则 #^(?:...)(?:/|$)#,只匹配搜索根路径;而不带 / 的普通名称走 $excludedDirs 或通配正则 #(?:^|/)(?:...)(?:/|$)#,匹配任意深度。测试 Tests/FinderTest.php 中的 testExcludeRootRelativePath 验证:exclude('/foo/bar') 仅剔除根级 foo/bar,而 x/nested/foo/bar 仍保留。

8.1:useUnixPaths() 强制正斜杠

8.1 新增 Finder::useUnixPaths(),强制返回路径使用 / 作为目录分隔符,主要面向分隔符为 \ 的 Windows 平台(Finder.php)。实现上它向 RecursiveDirectoryIterator 传递 UNIX_PATHS 标志(Finder.php),同时自定义迭代器内部会用 / 拼装相对路径(Iterator/RecursiveDirectoryIterator.php)。

测试 Tests/FinderTest.php 断言开启后 getPathname()、getPath()、getRelativePath()、getRelativePathname() 均不含 \,且子目录条目仍能正常产出。

二、过滤与剪枝演进:6.4 与 4.3

6.4:filter() 的早期目录剪枝

6.4 为 Finder::filter() 增加"早期目录剪枝"。filter() 的闭包签名与 $prune 参数如下(Finder.php):

public function filter(\Closure $closure, bool $prune = false): static
  • 闭包接收一个 SplFileInfo,返回 false 即剔除该条目;
  • 当 $prune = true 时,闭包同时进入 $this->pruneFilters(Finder.php),在目录遍历阶段提前终止对不满足条件目录的递归下探。

searchInDirectory() 将 pruneFilters 并入 $exclude 列表(Finder.php),由 Iterator/ExcludeDirectoryFilterIterator.php 在 hasChildren() 为真时对目录逐一执行剪枝回调。测试 Tests/FinderTest.php 通过 VFS 日志证明:被剪枝的目录 x/d 从未被打开遍历,而嵌套的 x/x/d 不受影响;testFilterPruneNestedDirectory(Tests/FinderTest.php)进一步验证了嵌套场景。这一特性对"排除 node_modules 之类大目录"的场景有显著性能收益。

4.3:ignoreVCSIgnored() 遵循 .gitignore

4.3 引入 Finder::ignoreVCSIgnored(),根据 .gitignore 规则忽略文件。默认关闭(Finder.php),开启后设置 IGNORE_VCS_IGNORED_FILES 位并在迭代链末尾接入 Iterator/VcsIgnoredFilterIterator.php(Finder.php)。核心匹配逻辑由 Gitignore.php 提供:

  • Gitignore::toRegex() 将 .gitignore 内容编译为等价正则(Gitignore.php);
  • Gitignore::toRegexMatchingNegatedPatterns() 处理以 ! 开头的反向规则(Gitignore.php);
  • 正则编译支持 *、**、?、字符类 <a href="https://link.gitcode.com/i/ddfa0d86f53e3928cd0b35c53dc41baf" target="_blank">...]、# 注释、尾随空格剥离、锚定路径 /foo 与目录后缀 /([Gitignore.php)。

Tests/GitignoreTest.php 提供了数百个匹配/不匹配用例(含 CRLF 换行归一化、! 反向规则嵌套层级、超过正则嵌套上限的 300 行模式压力测试 Tests/GitignoreTest.php),Tests/Fixtures/gitignore 目录则提供了真实目录结构测试样例。仓库另设有独立的 Tests/GitignoreTest.php 与 Tests/FinderTest.php 中的 gitignore 相关用例覆盖端到端行为。

5.4:递归 .gitignore 支持

5.4 记录"Add recursive .gitignore files support",即支持搜索过程中递归读取各级子目录中的 .gitignore,与 VCS 忽略过滤配合使用(见 Iterator/VcsIgnoredFilterIterator.php)。

三、排序 API 的成型:4.2 与 5.0、6.2

4.2:sortByName() 自然排序与 reverseSorting()

4.2 为 sortByName() 增加 $useNaturalSort 参数,并新增 reverseSorting()。当前签名(Finder.php):

public function sortByName(bool $useNaturalSort = false): static
  • false(默认):strcmp 字典序;
  • true:strnatcmp 自然排序,使 qux_2_0.php 排在 qux_10_2.php 之前。

5.0 将"不传 $useNaturalSort 即视为弃用"落地为正式参数(CHANGELOG 5.0.0)。reverseSorting()(Finder.php)使排序结果整体倒序,实现上通过向 SortableIterator 传入 $reverseOrder 并将比较结果乘以 -1(Iterator/SortableIterator.php)。测试 Tests/FinderTest.php 验证了 sortByName(false) 的字典序输出,Zephire.php 因大写字母排在最前。

6.2:sortByExtension() 与 sortBySize()

6.2 新增两个便捷排序:

$finder->sortByExtension(); // 按扩展名自然排序(SortableIterator::SORT_BY_EXTENSION)
$finder->sortBySize();      // 按文件字节大小升序(SortableIterator::SORT_BY_SIZE)

对应实现为 strnatcmp($a->getExtension(), $b->getExtension()) 与 $a->getSize() - $b->getSize()(Iterator/SortableIterator.php)。完整排序种类共 10 种常量(Iterator/SortableIterator.php),另含 sortByCaseInsensitiveName()(见下)、sortByType()(目录在前)、sortByAccessedTime() / sortByChangedTime() / sortByModifiedTime() 以及任意 sort(Closure) 回调。

注意:排序类方法会先收集全部匹配条目再做比较(Finder.php、Finder.php),对超大文件集内存开销较大。

6.2(同版本):sortByCaseInsensitiveName()

public function sortByCaseInsensitiveName(bool $useNaturalSort = false): static

提供大小写不敏感排序:false 用 strcasecmp,true 用 strnatcasecmp(Iterator/SortableIterator.php)。测试 Tests/FinderTest.php 分别验证了自然不敏感排序(qux_2_0.php 先于 qux_10_2.php,且 Zephire.php 排在 zebulon.php 之后)与普通不敏感排序。

四、比较器(Comparator)的演进:2.1 → 5.4 → 6.0

2.1:!= 运算符与比较器诞生

2.1 为比较器增加了 != 支持。当前 Comparator/Comparator.php 构造函数接受的合法运算符为 >、<、>=、<=、==、!=,非法运算符抛出 InvalidArgumentException;test() 方法用 match 完成比较(Comparator/Comparator.php)。

5.4 与 6.0:构造器取代 setter

  • 5.4 弃用 Comparator::setTarget() 与 Comparator::setOperator(),引入可同时设置 target 与 operator 的构造函数;
  • 6.0 正式移除这两个 setter。

因此现在的推荐用法是直接构造(等价于旧版两段式 setter 调用):

$comparator = new Comparator('10K', '>');
$comparator->test(15_000); // true

两个派生比较器解析了更友好的表达式:

  • Comparator/NumberComparator.php:支持 k(1000)、ki(1024)、m、mi、g、gi 等单位后缀,例如 '> 10K'、'<= 1Ki'、'> 10K';
  • Comparator/DateComparator.php:支持 after/since(映射为 >)与 before/until(映射为 <),日期交给 DateTimeImmutable 解析,等价于 strtotime() 可解析的字符串。

Finder::size() 与 Finder::date()、Finder::depth() 正是基于这两个比较器构建过滤条件(Finder.php、Finder.php、Finder.php)。

五、深度过滤、Glob 与路径匹配:3.4、3.3、2.x

3.4:hasResults() 快速判断

3.4 新增 Finder::hasResults(),用于在遍历前判断是否存在匹配结果:

if ($finder->files()->name('*.php')->in(__DIR__)->hasResults()) {
    // 有匹配
}

实现是惰性迭代——只取第一个元素即返回 true,不会消费整个结果集(Finder.php)。Finder 同时实现 \Countable,count() 使用 iterator_count()(Finder.php)。

3.3:Glob 双星匹配

3.3 为 Glob::toRegex() 增加双星(**)匹配能力。当前实现支持:

  • ** 跨目录通配(Glob.php);
  • * 匹配单段、? 匹配单字符、{a,b} 花括号选择、反斜杠转义(Glob.php);
  • strictLeadingDot 默认开启:通配符不匹配以 . 开头的隐藏文件(Glob.php)。

测试见 Tests/GlobTest.php。

2.x:路径过滤与 in() 增强

  • 2.1:新增基于文件内容的 contains() / notContains()(Finder.php),以及 [BC BREAK] 说明:被 * 或 ? 包裹的过滤表达式不再视为正则而是 glob 模式;同版本还加入按访问/变更/修改时间排序与 Countable;
  • 2.2:新增 path() / notPath()(Finder.php),并让 Finder::in() 支持通配符与 glob 模式;2.5 进一步支持 GLOB_BRACE。当前 in() 对每个目录先做 is_dir 检查,否则尝试 glob()(含 GLOB_ONLYDIR),全部失败则抛 DirectoryNotFoundException(Finder.php);
  • 2.3:新增 ignoreUnreadableDirs() 忽略不可读目录(默认扫描到不可读目录会抛 AccessDeniedException,见 Iterator/RecursiveDirectoryIterator.php);并统一不可执行子目录的处理为抛 AccessDeniedException;
  • 2.8:弃用 adapters 及相关类,3.0 移除全部废弃类。

六、总结:能力全景

版本 特性 核心入口
8.2 exclude() 根相对路径 /foo Finder::exclude() / ExcludeDirectoryFilterIterator
8.1 useUnixPaths() 正斜杠路径 Finder::useUnixPaths()
6.4 filter() 目录早期剪枝 Finder::filter($closure, true)
6.2 sortByExtension() / sortBySize() / sortByCaseInsensitiveName() Finder::sortBy*()
6.0 移除 Comparator setter Comparator 构造器
5.4 Comparator 构造器;SplFileInfo 内类型;递归 .gitignore Comparator::__construct() / Gitignore
5.0 sortByName($useNaturalSort) 正式参数 Finder::sortByName()
4.3 ignoreVCSIgnored() Finder::ignoreVCSIgnored()
4.2 自然排序参数 + reverseSorting() Finder::sortByName() / reverseSorting()
4.0 移除 ExceptionInterface 与 FilterIterator —
3.4 hasResults() Finder::hasResults()
3.3 Glob ** 双星 Glob::toRegex()
2.8/3.0 adapters 弃用并移除 —
2.5 in() 支持 GLOB_BRACE Finder::in()
2.3 ignoreUnreadableDirs()、统一 AccessDeniedException Finder::ignoreUnreadableDirs()
2.2 path() / notPath()、in() 通配符 Finder::path()
2.1 contains() / notContains()、时间排序、Countable、!=、glob 语义 Finder::contains()

从 2.1 到 8.2,Finder 的演进主线清晰可见:过滤能力(内容、路径、日期、大小、深度、自定义回调)逐步完善,性能(不可读目录处理、目录剪枝、惰性迭代、hasResults())持续优化,跨平台一致性(Unix 路径、Windows 变更时间语义)被专门对待,与生态协同(.gitignore 双向解析、VCS 目录忽略)不断加深。若要深入某一特性的实现细节,可直接阅读 Finder.php 中 searchInDirectory()(Finder.php)的迭代器链组装逻辑,以及 Iterator 目录下各过滤迭代器的 accept() 实现。

登录后查看全文
finder