Skip to content

Commit eb22b4c

Browse files
Merge branch '6.4' into 7.0
* 6.4: (47 commits) Sync .github/expected-missing-return-types.diff [FrameworkBundle] Add void return-type to ErrorLoggerCompilerPass [DoctrineBridge] Fix cross-versions compat [HttpKernel] Handle nullable callback of StreamedResponse [Mailer] Capitalize sender header for Mailgun [FrameworkBundle] Configure `logger` as error logger if the Monolog Bundle is not registered DX: PHP CS Fixer - drop explicit nullable_type_declaration_for_default_null_value config, as it's part of ruleset anyway DX: PHP CS Fixer - drop explicit no_superfluous_phpdoc_tags config, as it's part of ruleset already [DI] Simplify using DI attributes with `ServiceLocator/Iterator`'s [FrameworkBundle] Fix registering workflow.registry Revert "Add keyword `dev` to leverage composer hint" [Validator] Add missing Ukrainian translations #51960 [Validator] Add missing translations for Indonesian (id) [Validator] Add missing translations for Vietnamese (VI) Add missing Validator translations - Croatian (hr) [HttpFoundation]  Improve PHPDoc of Cache attribute [Validator] Add missing Spanish (es) translations #51956 [Serializer] Add `XmlEncoder::CDATA_WRAPPING` context option [Finder] Add early directory prunning filter support Add missing dutch translations ...
2 parents 0d89af5 + c0b454c commit eb22b4c

File tree

5 files changed

+285
-3
lines changed

5 files changed

+285
-3
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
6.4
5+
---
6+
7+
* Add early directory prunning to `Finder::filter()`
8+
49
6.2
510
---
611

Finder.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ class Finder implements \IteratorAggregate, \Countable
5050
private array $notNames = [];
5151
private array $exclude = [];
5252
private array $filters = [];
53+
private array $pruneFilters = [];
5354
private array $depths = [];
5455
private array $sizes = [];
5556
private bool $followLinks = false;
@@ -578,14 +579,22 @@ public function sortByModifiedTime(): static
578579
* The anonymous function receives a \SplFileInfo and must return false
579580
* to remove files.
580581
*
582+
* @param \Closure(SplFileInfo): bool $closure
583+
* @param bool $prune Whether to skip traversing directories further
584+
*
581585
* @return $this
582586
*
583587
* @see CustomFilterIterator
584588
*/
585-
public function filter(\Closure $closure): static
589+
public function filter(\Closure $closure /* , bool $prune = false */): static
586590
{
591+
$prune = 1 < \func_num_args() ? func_get_arg(1) : false;
587592
$this->filters[] = $closure;
588593

594+
if ($prune) {
595+
$this->pruneFilters[] = $closure;
596+
}
597+
589598
return $this;
590599
}
591600

@@ -739,6 +748,10 @@ private function searchInDirectory(string $dir): \Iterator
739748
$exclude = $this->exclude;
740749
$notPaths = $this->notPaths;
741750

751+
if ($this->pruneFilters) {
752+
$exclude = array_merge($exclude, $this->pruneFilters);
753+
}
754+
742755
if (static::IGNORE_VCS_FILES === (static::IGNORE_VCS_FILES & $this->ignore)) {
743756
$exclude = array_merge($exclude, self::$vcsPatterns);
744757
}

Iterator/ExcludeDirectoryFilterIterator.php

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,32 @@ class ExcludeDirectoryFilterIterator extends \FilterIterator implements \Recursi
2727
/** @var \Iterator<string, SplFileInfo> */
2828
private \Iterator $iterator;
2929
private bool $isRecursive;
30+
/** @var array<string, true> */
3031
private array $excludedDirs = [];
3132
private ?string $excludedPattern = null;
33+
/** @var list<callable(SplFileInfo):bool> */
34+
private array $pruneFilters = [];
3235

3336
/**
34-
* @param \Iterator<string, SplFileInfo> $iterator The Iterator to filter
35-
* @param string[] $directories An array of directories to exclude
37+
* @param \Iterator<string, SplFileInfo> $iterator The Iterator to filter
38+
* @param list<string|callable(SplFileInfo):bool> $directories An array of directories to exclude
3639
*/
3740
public function __construct(\Iterator $iterator, array $directories)
3841
{
3942
$this->iterator = $iterator;
4043
$this->isRecursive = $iterator instanceof \RecursiveIterator;
4144
$patterns = [];
4245
foreach ($directories as $directory) {
46+
if (!\is_string($directory)) {
47+
if (!\is_callable($directory)) {
48+
throw new \InvalidArgumentException('Invalid PHP callback.');
49+
}
50+
51+
$this->pruneFilters[] = $directory;
52+
53+
continue;
54+
}
55+
4356
$directory = rtrim($directory, '/');
4457
if (!$this->isRecursive || str_contains($directory, '/')) {
4558
$patterns[] = preg_quote($directory, '#');
@@ -70,6 +83,14 @@ public function accept(): bool
7083
return !preg_match($this->excludedPattern, $path);
7184
}
7285

86+
if ($this->pruneFilters && $this->hasChildren()) {
87+
foreach ($this->pruneFilters as $pruneFilter) {
88+
if (!$pruneFilter($this->current())) {
89+
return false;
90+
}
91+
}
92+
}
93+
7394
return true;
7495
}
7596

Tests/FinderTest.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
class FinderTest extends Iterator\RealIteratorTestCase
1818
{
19+
use Iterator\VfsIteratorTestTrait;
20+
1921
public function testCreate()
2022
{
2123
$this->assertInstanceOf(Finder::class, Finder::create());
@@ -989,6 +991,72 @@ public function testFilter()
989991
$this->assertIterator($this->toAbsolute(['test.php', 'test.py']), $finder->in(self::$tmpDir)->getIterator());
990992
}
991993

994+
public function testFilterPrune()
995+
{
996+
$this->setupVfsProvider([
997+
'x' => [
998+
'a.php' => '',
999+
'b.php' => '',
1000+
'd' => [
1001+
'u.php' => '',
1002+
],
1003+
'x' => [
1004+
'd' => [
1005+
'u2.php' => '',
1006+
],
1007+
],
1008+
],
1009+
'y' => [
1010+
'c.php' => '',
1011+
],
1012+
]);
1013+
1014+
$finder = $this->buildFinder();
1015+
$finder
1016+
->in($this->vfsScheme.'://x')
1017+
->filter(fn (): bool => true, true) // does nothing
1018+
->filter(function (\SplFileInfo $file): bool {
1019+
$path = $this->stripSchemeFromVfsPath($file->getPathname());
1020+
1021+
$res = 'x/d' !== $path;
1022+
1023+
$this->vfsLog[] = [$path, 'exclude_filter', $res];
1024+
1025+
return $res;
1026+
}, true)
1027+
->filter(fn (): bool => true, true); // does nothing
1028+
1029+
$this->assertSameVfsIterator([
1030+
'x/a.php',
1031+
'x/b.php',
1032+
'x/x',
1033+
'x/x/d',
1034+
'x/x/d/u2.php',
1035+
], $finder->getIterator());
1036+
1037+
// "x/d" directory must be pruned early
1038+
// "x/x/d" directory must not be pruned
1039+
$this->assertSame([
1040+
['x', 'is_dir', true],
1041+
['x', 'list_dir_open', ['a.php', 'b.php', 'd', 'x']],
1042+
['x/a.php', 'is_dir', false],
1043+
['x/a.php', 'exclude_filter', true],
1044+
['x/b.php', 'is_dir', false],
1045+
['x/b.php', 'exclude_filter', true],
1046+
['x/d', 'is_dir', true],
1047+
['x/d', 'exclude_filter', false],
1048+
['x/x', 'is_dir', true],
1049+
['x/x', 'exclude_filter', true], // from ExcludeDirectoryFilterIterator::accept() (prune directory filter)
1050+
['x/x', 'exclude_filter', true], // from CustomFilterIterator::accept() (regular filter)
1051+
['x/x', 'list_dir_open', ['d']],
1052+
['x/x/d', 'is_dir', true],
1053+
['x/x/d', 'exclude_filter', true],
1054+
['x/x/d', 'list_dir_open', ['u2.php']],
1055+
['x/x/d/u2.php', 'is_dir', false],
1056+
['x/x/d/u2.php', 'exclude_filter', true],
1057+
], $this->vfsLog);
1058+
}
1059+
9921060
public function testFollowLinks()
9931061
{
9941062
if ('\\' == \DIRECTORY_SEPARATOR) {
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Finder\Tests\Iterator;
13+
14+
trait VfsIteratorTestTrait
15+
{
16+
private static int $vfsNextSchemeIndex = 0;
17+
18+
/** @var array<string, \Closure(string, 'list_dir_open'|'list_dir_rewind'|'is_dir'): (list<string>|bool)> */
19+
public static array $vfsProviders;
20+
21+
protected string $vfsScheme;
22+
23+
/** @var list<array{string, string, mixed}> */
24+
protected array $vfsLog = [];
25+
26+
protected function setUp(): void
27+
{
28+
parent::setUp();
29+
30+
$this->vfsScheme = 'symfony-finder-vfs-test-'.++self::$vfsNextSchemeIndex;
31+
32+
$vfsWrapperClass = \get_class(new class() {
33+
/** @var array<string, \Closure(string, 'list_dir_open'|'list_dir_rewind'|'is_dir'): (list<string>|bool)> */
34+
public static array $vfsProviders = [];
35+
36+
/** @var resource */
37+
public $context;
38+
39+
private string $scheme;
40+
41+
private string $dirPath;
42+
43+
/** @var list<string> */
44+
private array $dirData;
45+
46+
private function parsePathAndSetScheme(string $url): string
47+
{
48+
$urlArr = parse_url($url);
49+
\assert(\is_array($urlArr));
50+
\assert(isset($urlArr['scheme']));
51+
\assert(isset($urlArr['host']));
52+
53+
$this->scheme = $urlArr['scheme'];
54+
55+
return str_replace(\DIRECTORY_SEPARATOR, '/', $urlArr['host'].($urlArr['path'] ?? ''));
56+
}
57+
58+
public function processListDir(bool $fromRewind): bool
59+
{
60+
$providerFx = self::$vfsProviders[$this->scheme];
61+
$data = $providerFx($this->dirPath, 'list_dir'.($fromRewind ? '_rewind' : '_open'));
62+
\assert(\is_array($data));
63+
$this->dirData = $data;
64+
65+
return true;
66+
}
67+
68+
public function dir_opendir(string $url): bool
69+
{
70+
$this->dirPath = $this->parsePathAndSetScheme($url);
71+
72+
return $this->processListDir(false);
73+
}
74+
75+
public function dir_readdir(): string|false
76+
{
77+
return array_shift($this->dirData) ?? false;
78+
}
79+
80+
public function dir_closedir(): bool
81+
{
82+
unset($this->dirPath);
83+
unset($this->dirData);
84+
85+
return true;
86+
}
87+
88+
public function dir_rewinddir(): bool
89+
{
90+
return $this->processListDir(true);
91+
}
92+
93+
/**
94+
* @return array<string, mixed>
95+
*/
96+
public function stream_stat(): array
97+
{
98+
return [];
99+
}
100+
101+
/**
102+
* @return array<string, mixed>
103+
*/
104+
public function url_stat(string $url): array
105+
{
106+
$path = $this->parsePathAndSetScheme($url);
107+
$providerFx = self::$vfsProviders[$this->scheme];
108+
$isDir = $providerFx($path, 'is_dir');
109+
\assert(\is_bool($isDir));
110+
111+
return ['mode' => $isDir ? 0040755 : 0100644];
112+
}
113+
});
114+
self::$vfsProviders = &$vfsWrapperClass::$vfsProviders;
115+
116+
stream_wrapper_register($this->vfsScheme, $vfsWrapperClass);
117+
}
118+
119+
protected function tearDown(): void
120+
{
121+
stream_wrapper_unregister($this->vfsScheme);
122+
123+
parent::tearDown();
124+
}
125+
126+
/**
127+
* @param array<string, mixed> $data
128+
*/
129+
protected function setupVfsProvider(array $data): void
130+
{
131+
self::$vfsProviders[$this->vfsScheme] = function (string $path, string $op) use ($data) {
132+
$pathArr = explode('/', $path);
133+
$fileEntry = $data;
134+
while (($name = array_shift($pathArr)) !== null) {
135+
if (!isset($fileEntry[$name])) {
136+
$fileEntry = false;
137+
138+
break;
139+
}
140+
141+
$fileEntry = $fileEntry[$name];
142+
}
143+
144+
if ('list_dir_open' === $op || 'list_dir_rewind' === $op) {
145+
/** @var list<string> $res */
146+
$res = array_keys($fileEntry);
147+
} elseif ('is_dir' === $op) {
148+
$res = \is_array($fileEntry);
149+
} else {
150+
throw new \Exception('Unexpected operation type');
151+
}
152+
153+
$this->vfsLog[] = [$path, $op, $res];
154+
155+
return $res;
156+
};
157+
}
158+
159+
protected function stripSchemeFromVfsPath(string $url): string
160+
{
161+
$urlArr = parse_url($url);
162+
\assert(\is_array($urlArr));
163+
\assert($urlArr['scheme'] === $this->vfsScheme);
164+
\assert(isset($urlArr['host']));
165+
166+
return str_replace(\DIRECTORY_SEPARATOR, '/', $urlArr['host'].($urlArr['path'] ?? ''));
167+
}
168+
169+
protected function assertSameVfsIterator(array $expected, \Traversable $iterator)
170+
{
171+
$values = array_map(fn (\SplFileInfo $fileinfo) => $this->stripSchemeFromVfsPath($fileinfo->getPathname()), iterator_to_array($iterator));
172+
173+
$this->assertEquals($expected, array_values($values));
174+
}
175+
}

0 commit comments

Comments
 (0)