Skip to content

Commit 60272cb

Browse files
committed
Extract static methods out of NamespaceDirectory.
1 parent edef7ca commit 60272cb

File tree

3 files changed

+136
-65
lines changed

3 files changed

+136
-65
lines changed

src/NamespaceDirectory.php

Lines changed: 2 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -525,7 +525,7 @@ public function getRelativeTerminatedPath(string $subdir = 'src/', int $level =
525525
* Format: $[$file] = $class
526526
*/
527527
public function getIterator(): \Iterator {
528-
return self::scan($this->directory, $this->terminatedNamespace);
528+
return NsDirUtil::iterate($this->directory, $this->terminatedNamespace);
529529
}
530530

531531
/**
@@ -596,45 +596,6 @@ public function getSubdirsHere(): \Iterator {
596596
}
597597
}
598598

599-
/**
600-
* @param string $dir
601-
* @param string $terminatedNamespace
602-
*
603-
* @return \Iterator<string, class-string>
604-
* Format: $[$file] = $class
605-
*/
606-
private static function scan(string $dir, string $terminatedNamespace): \Iterator {
607-
foreach (self::scanKnownDir($dir) as $candidate) {
608-
if ('.' === $candidate[0]) {
609-
continue;
610-
}
611-
$path = $dir . '/' . $candidate;
612-
if (\str_ends_with($candidate, '.php')) {
613-
if (!is_file($path)) {
614-
continue;
615-
}
616-
$name = substr($candidate, 0, -4);
617-
if (!preg_match(self::CLASS_NAME_REGEX, $name)) {
618-
continue;
619-
}
620-
// @phpstan-ignore generator.valueType
621-
yield $path => $terminatedNamespace . $name;
622-
}
623-
else {
624-
if (!is_dir($path)) {
625-
continue;
626-
}
627-
if (!preg_match(self::CLASS_NAME_REGEX, $candidate)) {
628-
continue;
629-
}
630-
yield from self::scan(
631-
$path,
632-
$terminatedNamespace . $candidate . '\\',
633-
);
634-
}
635-
}
636-
}
637-
638599
/**
639600
* Runs scandir() on this namespace directory.
640601
*
@@ -647,31 +608,7 @@ private static function scan(string $dir, string $terminatedNamespace): \Iterato
647608
* This also includes '.' and '..'.
648609
*/
649610
private function scanThisDir(): array {
650-
return self::scanKnownDir($this->directory);
651-
}
652-
653-
/**
654-
* Runs scandir() on a known directory.
655-
*
656-
* Throws a runtime exception on failure, instead of returning false.
657-
* This is considered an unhandled exception, because it is assumed that the
658-
* given directory always exists.
659-
*
660-
* @param string $dir
661-
* Known directory to scan.
662-
*
663-
* @return list<string>
664-
* Items in the directory in alphabetic order.
665-
* This also includes '.' and '..'.
666-
* Calling code already does filtering with regex or other means, so it
667-
* would be redundant to do additional filtering here.
668-
*/
669-
private static function scanKnownDir(string $dir): array {
670-
$candidates = @\scandir($dir, \SCANDIR_SORT_ASCENDING);
671-
if ($candidates === false) {
672-
throw new \RuntimeException("Failed to scandir('$dir').");
673-
}
674-
return $candidates;
611+
return NsDirUtil::scanKnownDir($this->directory);
675612
}
676613

677614
}

src/NsDirUtil.php

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44

55
class NsDirUtil {
66

7+
/**
8+
* See http://php.net/manual/en/language.oop5.basic.php
9+
*/
10+
const CLASS_NAME_REGEX = /** @lang RegExp */ '/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/';
11+
712
/**
813
* @param string $namespace
914
*
@@ -23,4 +28,89 @@ public static function terminateNamespace(string $namespace): string {
2328
}
2429
return $namespace . '\\';
2530
}
31+
32+
/**
33+
* Recursively iterates over class files.
34+
*
35+
* @param string $dir
36+
* Directory without trailing slash.
37+
* @param string $terminatedNamespace
38+
* Namespace with trailing separator.
39+
*
40+
* @return \Iterator<string, class-string>
41+
* Format: $[$file] = $class
42+
*/
43+
public static function iterate(string $dir, string $terminatedNamespace): \Iterator {
44+
assert(!str_ends_with($dir, '/'));
45+
assert(str_ends_with($terminatedNamespace, '\\') || $terminatedNamespace === '');
46+
return self::doIterateRecursively($dir, $terminatedNamespace);
47+
}
48+
49+
/**
50+
* Recursively iterates over class files.
51+
*
52+
* @param string $dir
53+
* Directory without trailing slash.
54+
* @param string $terminatedNamespace
55+
* Namespace with trailing separator.
56+
*
57+
* @return \Iterator<string, class-string>
58+
* Format: $[$file] = $class
59+
*/
60+
private static function doIterateRecursively(string $dir, string $terminatedNamespace): \Iterator {
61+
foreach (self::scanKnownDir($dir) as $candidate) {
62+
if ('.' === $candidate[0]) {
63+
continue;
64+
}
65+
$path = $dir . '/' . $candidate;
66+
if (\str_ends_with($candidate, '.php')) {
67+
if (!is_file($path)) {
68+
continue;
69+
}
70+
$name = substr($candidate, 0, -4);
71+
if (!preg_match(self::CLASS_NAME_REGEX, $name)) {
72+
continue;
73+
}
74+
// @phpstan-ignore generator.valueType
75+
yield $path => $terminatedNamespace . $name;
76+
}
77+
else {
78+
if (!is_dir($path)) {
79+
continue;
80+
}
81+
if (!preg_match(self::CLASS_NAME_REGEX, $candidate)) {
82+
continue;
83+
}
84+
yield from self::doIterateRecursively(
85+
$path,
86+
$terminatedNamespace . $candidate . '\\',
87+
);
88+
}
89+
}
90+
}
91+
92+
/**
93+
* Runs scandir() on a known directory.
94+
*
95+
* Throws a runtime exception on failure, instead of returning false.
96+
* This is considered an unhandled exception, because it is assumed that the
97+
* given directory always exists.
98+
*
99+
* @param string $dir
100+
* Known directory to scan.
101+
*
102+
* @return list<string>
103+
* Items in the directory in alphabetic order.
104+
* This also includes '.' and '..'.
105+
* Calling code already does filtering with regex or other means, so it
106+
* would be redundant to do additional filtering here.
107+
*/
108+
public static function scanKnownDir(string $dir): array {
109+
$candidates = @\scandir($dir, \SCANDIR_SORT_ASCENDING);
110+
if ($candidates === false) {
111+
throw new \RuntimeException("Failed to scandir('$dir').");
112+
}
113+
return $candidates;
114+
}
115+
26116
}

tests/src/NsDirUtilTest.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,18 @@
44

55
namespace Ock\ClassFilesIterator\Tests;
66

7+
use Ock\ClassFilesIterator\NamespaceDirectory;
78
use Ock\ClassFilesIterator\NsDirUtil;
9+
use Ock\ClassFilesIterator\Tests\Fixtures\Acme\Plant\PlantInterface;
10+
use Ock\ClassFilesIterator\Tests\Fixtures\Acme\Plant\Tree\Fig;
11+
use Ock\ClassFilesIterator\Tests\Fixtures\Acme\Plant\VenusFlyTrap;
812
use Ock\ClassFilesIterator\Tests\Traits\ExceptionTestTrait;
913
use PHPUnit\Framework\Attributes\CoversClass;
14+
use PHPUnit\Framework\Attributes\UsesClass;
1015
use PHPUnit\Framework\TestCase;
1116

1217
#[CoversClass(NsDirUtil::class)]
18+
#[UsesClass(NamespaceDirectory::class)]
1319
class NsDirUtilTest extends TestCase {
1420

1521
use ExceptionTestTrait;
@@ -23,4 +29,42 @@ public function testTerminateNamespace(): void {
2329
$this->callAndAssertException(\InvalidArgumentException::class, fn () => $f('\\Acme\\Zoo\\Animal'));
2430
}
2531

32+
public function testIterate(): void {
33+
$nsdir = NamespaceDirectory::fromKnownClass(PlantInterface::class);
34+
$this->assertSame(
35+
[
36+
__DIR__ . '/Fixtures/Acme/Plant/PlantInterface.php' => PlantInterface::class,
37+
__DIR__ . '/Fixtures/Acme/Plant/Tree/Fig.php' => Fig::class,
38+
__DIR__ . '/Fixtures/Acme/Plant/VenusFlyTrap.php' => VenusFlyTrap::class,
39+
],
40+
iterator_to_array(NsDirUtil::iterate($nsdir->getDirectory(), $nsdir->getTerminatedNamespace())),
41+
);
42+
43+
$assert_exception = fn (string $path) => $this->callAndAssertException(\RuntimeException::class, NsDirUtil::iterate($path, '')->valid(...));
44+
45+
$assert_exception(__DIR__ . '/NonExistingSubdir');
46+
$assert_exception(__FILE__);
47+
48+
mkdir($dir = sys_get_temp_dir() . '/test-dir-' . uniqid());
49+
$perms = fileperms($dir) & 0777;
50+
file_put_contents($file = $dir . '/Test.php', "<?php return 'file contents';");
51+
$this->assertSame($file, NsDirUtil::iterate($dir, '')->key());
52+
53+
// Write and "execute" permissions on the directory.
54+
chmod($dir, $perms & 0333);
55+
$assert_exception($dir);
56+
$this->assertSame('file contents', include $file);
57+
58+
// Only read permissions, no execute bit.
59+
// The directory will appear as empty.
60+
chmod($dir, $perms & 0444);
61+
$this->assertNull(NsDirUtil::iterate($dir, '')->key());
62+
$this->assertFalse(@include $file);
63+
}
64+
65+
public function testScanKnownDir(): void {
66+
$f = NsDirUtil::scanKnownDir(...);
67+
$this->callAndAssertException(\RuntimeException::class, fn () => $f(__DIR__ . '/NonExistingDir'));
68+
}
69+
2670
}

0 commit comments

Comments
 (0)