Symfony Finder CHANGELOG 深度解读:从 2.1 到 8.2 的演进脉络与 API 实战指南
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() 实现。