Skip to content

Commit 3418ad2

Browse files
committed
Introduce class DirectoryContents.
1 parent 4b91c3b commit 3418ad2

File tree

7 files changed

+269
-87
lines changed

7 files changed

+269
-87
lines changed

src/DirectoryContents.php

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Ock\ClassFilesIterator;
6+
7+
/**
8+
* Value object with the result of a directory scan.
9+
*/
10+
final class DirectoryContents {
11+
12+
/**
13+
* See http://php.net/manual/en/language.oop5.basic.php
14+
*/
15+
private const CLASS_NAME_PATTERN = /** @lang RegExp */ '/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/';
16+
17+
private const CLASS_FILE_PATTERN = /** @lang RegExp */ '/^([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\.php$/';
18+
19+
/**
20+
* Constructor.
21+
*
22+
* @param list<string> $subdirNames
23+
* @param list<string> $fileNames
24+
*/
25+
public function __construct(
26+
public readonly array $subdirNames,
27+
public readonly array $fileNames,
28+
) {}
29+
30+
/**
31+
* Static factory which loads a directory.
32+
*
33+
* @param string $dir
34+
* Directory path without trailing slash.
35+
*
36+
* @return static
37+
*/
38+
public static function load(string $dir): static {
39+
assert(!str_ends_with($dir, '/'));
40+
$iterator = new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS|\FilesystemIterator::KEY_AS_FILENAME|\FilesystemIterator::CURRENT_AS_SELF);
41+
$subdir_names = [];
42+
$file_names = [];
43+
foreach ($iterator as $name => $iterator_self) {
44+
if ($iterator->hasChildren()) {
45+
$subdir_names[] = $name;
46+
}
47+
else {
48+
$file_names[] = $name;
49+
}
50+
}
51+
sort($subdir_names);
52+
sort($file_names);
53+
return new static($subdir_names, $file_names);
54+
}
55+
56+
/**
57+
* Gets file names and subdir names as a mixed sorted array.
58+
*
59+
* @return array<string, bool>
60+
*/
61+
public function getFilesAndDirectoriesMap(): array {
62+
$map = array_fill_keys($this->subdirNames, true)
63+
+ array_fill_keys($this->fileNames, false);
64+
ksort($map);
65+
return $map;
66+
}
67+
68+
/**
69+
* Gets file names and subdir names as a mixed sorted array.
70+
*
71+
* @return \Iterator<string, bool>
72+
*/
73+
public function iterateClassAndNamespaceMap(): \Iterator {
74+
foreach ($this->getFilesAndDirectoriesMap() as $candidate => $is_dir) {
75+
if ($is_dir) {
76+
if (preg_match(self::CLASS_NAME_PATTERN, $candidate)) {
77+
yield $candidate => TRUE;
78+
}
79+
}
80+
else {
81+
if (preg_match(self::CLASS_FILE_PATTERN, $candidate, $matches)) {
82+
yield $matches[1] => FALSE;
83+
}
84+
}
85+
}
86+
}
87+
88+
/**
89+
* Gets class shortnames based on *.php file names.
90+
*
91+
* @return list<string>
92+
* Class shortnames.
93+
*/
94+
public function getClassNames(): array {
95+
$names = [];
96+
foreach ($this->fileNames as $file_name) {
97+
if (preg_match(self::CLASS_FILE_PATTERN, $file_name, $matches)) {
98+
$names[] = $matches[1];
99+
}
100+
}
101+
return $names;
102+
}
103+
104+
/**
105+
* Gets namespace shortnames based on subdirectory names.
106+
*
107+
* @return list<string>
108+
* Namespace shortnames.
109+
*/
110+
public function getNamespaceNames(): array {
111+
$names = preg_grep(self::CLASS_NAME_PATTERN, $this->subdirNames);
112+
assert($names !== false);
113+
return $names;
114+
}
115+
116+
}

src/NamespaceDirectory.php

Lines changed: 10 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,6 @@
99
*/
1010
final class NamespaceDirectory implements ClassFilesIAInterface {
1111

12-
/**
13-
* See http://php.net/manual/en/language.oop5.basic.php
14-
*/
15-
const CLASS_NAME_REGEX = /** @lang RegExp */ '/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/';
16-
17-
const CANDIDATE_REGEX = /** @lang RegExp */ '/^([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)(\.php|)$/';
18-
1912
/**
2013
* Creates a new instance.
2114
*
@@ -534,24 +527,12 @@ public function getIterator(): \Iterator {
534527
public function getElements(): array {
535528
$classes = [];
536529
$subdirs = [];
537-
foreach (NsDirUtil::getDirContents($this->directory) as $candidate => $is_dir) {
538-
if (!$is_dir) {
539-
if (!\str_ends_with($candidate, '.php')) {
540-
continue;
541-
}
542-
$path = $this->directory . '/' . $candidate;
543-
$name = \substr($candidate, 0, -4);
544-
if (!\preg_match(self::CLASS_NAME_REGEX, $name)) {
545-
continue;
546-
}
547-
$classes[$path] = $this->terminatedNamespace . $name;
548-
}
549-
else {
550-
if (!\preg_match(self::CLASS_NAME_REGEX, $candidate)) {
551-
continue;
552-
}
553-
$subdirs[$candidate] = $this->subdir($candidate);
554-
}
530+
$contents = DirectoryContents::load($this->directory);
531+
foreach ($contents->getClassNames() as $name) {
532+
$classes[$this->directory . '/' . $name . '.php'] = $this->terminatedNamespace . $name;
533+
}
534+
foreach ($contents->getNamespaceNames() as $name) {
535+
$subdirs[$name] = $this->subdir($name);
555536
}
556537
/** @var array<string, class-string> $classes */
557538
return [$classes, $subdirs];
@@ -562,15 +543,8 @@ public function getElements(): array {
562543
*/
563544
public function getClassFilesHere(): array {
564545
$classFiles = [];
565-
foreach (NsDirUtil::getDirContents($this->directory) as $candidate => $is_dir) {
566-
if ($is_dir || !\str_ends_with($candidate, '.php')) {
567-
continue;
568-
}
569-
$path = $this->directory . '/' . $candidate;
570-
$name = \substr($candidate, 0, -4);
571-
if (!\preg_match(self::CLASS_NAME_REGEX, $name)) {
572-
continue;
573-
}
546+
foreach (DirectoryContents::load($this->directory)->getClassNames() as $name) {
547+
$path = $this->directory . '/' . $name . '.php';
574548
$classFiles[$path] = $this->terminatedNamespace . $name;
575549
}
576550
/** @var array<string, class-string> $classFiles */
@@ -581,11 +555,8 @@ public function getClassFilesHere(): array {
581555
* @return \Iterator<string, static>
582556
*/
583557
public function getSubdirsHere(): \Iterator {
584-
foreach (NsDirUtil::getDirContents($this->directory) as $candidate => $is_dir) {
585-
if (!$is_dir || !preg_match(self::CLASS_NAME_REGEX, $candidate)) {
586-
continue;
587-
}
588-
yield $candidate => $this->subdir($candidate);
558+
foreach (DirectoryContents::load($this->directory)->getNamespaceNames() as $name) {
559+
yield $name => $this->subdir($name);
589560
}
590561
}
591562

src/NsDirUtil.php

Lines changed: 6 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,6 @@
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-
127
/**
138
* @param string $namespace
149
*
@@ -77,51 +72,19 @@ public static function assertReadableDirectory(string $dir): void {
7772
* Format: $[$file] = $class
7873
*/
7974
private static function doIterateRecursively(string $dir, string $terminatedNamespace): \Iterator {
80-
foreach (self::getDirContents($dir) as $candidate => $is_dir) {
81-
$path = $dir . '/' . $candidate;
82-
if (!$is_dir) {
83-
if (!\str_ends_with($candidate, '.php')) {
84-
continue;
85-
}
86-
$name = substr($candidate, 0, -4);
87-
if (!preg_match(self::CLASS_NAME_REGEX, $name)) {
88-
continue;
89-
}
75+
$contents = DirectoryContents::load($dir);
76+
foreach ($contents->iterateClassAndNamespaceMap() as $name => $is_namespace) {
77+
if (!$is_namespace) {
9078
// @phpstan-ignore generator.valueType
91-
yield $path => $terminatedNamespace . $name;
79+
yield $dir . '/' . $name . '.php' => $terminatedNamespace . $name;
9280
}
9381
else {
94-
if (!preg_match(self::CLASS_NAME_REGEX, $candidate)) {
95-
continue;
96-
}
9782
yield from self::doIterateRecursively(
98-
$path,
99-
$terminatedNamespace . $candidate . '\\',
83+
$dir . '/' . $name,
84+
$terminatedNamespace . $name . '\\',
10085
);
10186
}
10287
}
10388
}
10489

105-
/**
106-
* Gets names of files and subdirectories in a directory.
107-
*
108-
* @param string $dir
109-
* Parent directory to scan.
110-
*
111-
* @return array<string, bool>
112-
* Array where the keys are file or directory names, and the values indicate
113-
* whether the entry is a directory.
114-
*
115-
* @internal
116-
*/
117-
public static function getDirContents(string $dir): array {
118-
$iterator = new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS|\FilesystemIterator::KEY_AS_FILENAME|\FilesystemIterator::CURRENT_AS_SELF);
119-
$contents = [];
120-
foreach ($iterator as $name => $iterator_self) {
121-
$contents[$name] = $iterator->hasChildren();
122-
}
123-
ksort($contents);
124-
return $contents;
125-
}
126-
12790
}

tests/src/ClassFilesIATest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Ock\ClassFilesIterator\ClassFilesIA\ClassFilesIA_Concat;
77
use Ock\ClassFilesIterator\ClassFilesIA\ClassFilesIA_Empty;
88
use Ock\ClassFilesIterator\ClassFilesIA\ClassFilesIA_NamespaceDirectoryPsr4;
9+
use Ock\ClassFilesIterator\DirectoryContents;
910
use Ock\ClassFilesIterator\NamespaceDirectory;
1011
use Ock\ClassFilesIterator\NsDirUtil;
1112
use Ock\ClassFilesIterator\Tests\Fixtures\Acme\Animal\GreySquirrel;
@@ -24,6 +25,7 @@
2425
#[CoversClass(ClassFilesIA_NamespaceDirectoryPsr4::class)]
2526
#[UsesClass(NamespaceDirectory::class)]
2627
#[UsesClass(NsDirUtil::class)]
28+
#[UsesClass(DirectoryContents::class)]
2729
class ClassFilesIATest extends TestCase {
2830

2931
use ExceptionTestTrait;

0 commit comments

Comments
 (0)