Skip to content

Commit 3ce7701

Browse files
committed
Issue 139: Move classes into class-files-iterator package.
1 parent c29c64e commit 3ce7701

19 files changed

+1726
-0
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
11
# Class files iterator
22

33
This package provides IteratorAggregate implementations to iterate through class files.
4+
5+
Main concepts:
6+
- [`ClassFilesIA*`](src/ClassFilesIA/ClassFilesIAInterface.php):\
7+
IteratorAggregate that lists class names keyed by their file names.
8+
- [`NamespaceDirectory`](src/NamespaceDirectory.php):\
9+
Main implementation representing a PSR-4 class files directory.\
10+
It provides additional methods to navigate to parent or child directories.
11+
- [`ClassNamesIA*`](src/ClassNamesIA/ClassNamesIAInterface.php)\
12+
IteratorAggregate that lists class names by numeric keys.\
13+
This can be used if the file path is not relevant.

src/ClassFilesIA/ClassFilesIA.php

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<?php
2+
3+
namespace Ock\ClassFilesIterator\ClassFilesIA;
4+
5+
use Ock\ClassFilesIterator\NamespaceDirectory;
6+
use Ock\ClassFilesIterator\NsDirUtil;
7+
8+
/**
9+
* Static factories for ClassFilesIAInterface objects.
10+
*/
11+
class ClassFilesIA {
12+
13+
/**
14+
* @param string $dir
15+
* @param string $namespace
16+
*
17+
* @return \Ock\ClassFilesIterator\ClassFilesIA\ClassFilesIAInterface
18+
*/
19+
public static function psr4(string $dir, string $namespace): ClassFilesIAInterface {
20+
return new ClassFilesIA_NamespaceDirectoryPsr4(
21+
$dir,
22+
NsDirUtil::terminateNamespace($namespace),
23+
);
24+
}
25+
26+
/**
27+
* @param string $dir
28+
* @param string $namespace
29+
* @param int $nLevelsUp
30+
*
31+
* @return \Ock\ClassFilesIterator\ClassFilesIA\ClassFilesIAInterface
32+
*/
33+
public static function psr4Up(string $dir, string $namespace, int $nLevelsUp = 0): ClassFilesIAInterface {
34+
return NamespaceDirectory::create($dir, $namespace)
35+
->requireParentN($nLevelsUp);
36+
}
37+
38+
/**
39+
* @param string $class
40+
* @param int $nLevelsUp
41+
*
42+
* @return \Ock\ClassFilesIterator\NamespaceDirectory
43+
*
44+
* @throws \ReflectionException
45+
* Class does not exist.
46+
*/
47+
public static function psr4FromClass(string $class, int $nLevelsUp = 0): NamespaceDirectory {
48+
$result = NamespaceDirectory::createFromClass($class);
49+
if ($nLevelsUp !== 0) {
50+
$result = $result->requireParentN($nLevelsUp);
51+
}
52+
return $result;
53+
}
54+
55+
/**
56+
* @param class-string $class
57+
* @param int $nLevelsUp
58+
*
59+
* @return \Ock\ClassFilesIterator\NamespaceDirectory
60+
*/
61+
public static function psr4FromKnownClass(string $class, int $nLevelsUp = 0): NamespaceDirectory {
62+
$result = NamespaceDirectory::fromKnownClass($class);
63+
if ($nLevelsUp !== 0) {
64+
$result = $result->requireParentN($nLevelsUp);
65+
}
66+
return $result;
67+
}
68+
69+
/**
70+
* @param \Ock\ClassFilesIterator\ClassFilesIA\ClassFilesIAInterface[] $classFilesIAs
71+
*
72+
* @return \Ock\ClassFilesIterator\ClassFilesIA\ClassFilesIAInterface
73+
*/
74+
public static function multiple(array $classFilesIAs): ClassFilesIAInterface {
75+
return new ClassFilesIA_Concat($classFilesIAs);
76+
}
77+
78+
/**
79+
* @param class-string[] $classes
80+
*
81+
* @return \Ock\ClassFilesIterator\ClassFilesIA\ClassFilesIAInterface
82+
*
83+
* @throws \ReflectionException
84+
* One of the classes does not exist.
85+
*/
86+
public static function psr4FromClasses(array $classes): ClassFilesIAInterface {
87+
return self::multiple(array_map(
88+
self::psr4FromClass(...),
89+
$classes,
90+
));
91+
}
92+
93+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace Ock\ClassFilesIterator\ClassFilesIA;
4+
5+
use Ock\ClassFilesIterator\ClassNamesIA\ClassNamesIAInterface;
6+
7+
/**
8+
* @template-extends ClassNamesIAInterface<string>
9+
*/
10+
interface ClassFilesIAInterface extends ClassNamesIAInterface {
11+
12+
/**
13+
* Iterates through class files.
14+
*
15+
* @return \Iterator<string, class-string>
16+
* Format: $[$file] = $class
17+
*/
18+
public function getIterator(): \Iterator;
19+
20+
/**
21+
* Gets a version where all base paths are sent through ->realpath().
22+
*
23+
* This is useful when comparing the path to \ReflectionClass::getFileName().
24+
*
25+
* Implementations might use an optimization where they only send the base
26+
* path through realpath(), assuming that the subdirectories do not contain
27+
* symlinks.
28+
*
29+
* @return static
30+
*/
31+
public function withRealpathRoot(): static;
32+
33+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace Ock\ClassFilesIterator\ClassFilesIA;
4+
5+
class ClassFilesIA_Concat implements ClassFilesIAInterface {
6+
7+
/**
8+
* @param \Ock\ClassFilesIterator\ClassFilesIA\ClassFilesIAInterface[] $classFilesIAs
9+
*/
10+
public function __construct(
11+
private array $classFilesIAs,
12+
) {}
13+
14+
/**
15+
* {@inheritdoc}
16+
*/
17+
public function getIterator(): \Iterator {
18+
foreach ($this->classFilesIAs as $classFilesIA) {
19+
yield from $classFilesIA;
20+
}
21+
}
22+
23+
/**
24+
* {@inheritdoc}
25+
*/
26+
public function withRealpathRoot(): static {
27+
$clone = clone $this;
28+
foreach ($clone->classFilesIAs as $i => $classFilesIA) {
29+
$clone->classFilesIAs[$i] = $classFilesIA->withRealpathRoot();
30+
}
31+
return $clone;
32+
}
33+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace Ock\ClassFilesIterator\ClassFilesIA;
4+
5+
class ClassFilesIA_Empty implements ClassFilesIAInterface {
6+
7+
use RealpathRootThisTrait;
8+
9+
/**
10+
* {@inheritdoc}
11+
*/
12+
public function getIterator(): \Iterator {
13+
return new \EmptyIterator();
14+
}
15+
16+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<?php
2+
3+
namespace Ock\ClassFilesIterator\ClassFilesIA;
4+
5+
use Ock\ClassFilesIterator\NamespaceDirectory;
6+
use Ock\ClassFilesIterator\NsDirUtil;
7+
8+
class ClassFilesIA_NamespaceDirectoryPsr4 implements ClassFilesIAInterface {
9+
10+
/**
11+
* See http://php.net/manual/en/language.oop5.basic.php
12+
*/
13+
const CLASS_NAME_REGEX = /** @lang RegExp */ '/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/';
14+
15+
/**
16+
* @param string $dir
17+
* @param string $namespace
18+
*
19+
* @return self
20+
*/
21+
public static function create(string $dir, string $namespace): self {
22+
return new self(
23+
$dir,
24+
NsDirUtil::terminateNamespace($namespace),
25+
);
26+
}
27+
28+
/**
29+
* @param class-string $class
30+
* @param int $nLevelsUp
31+
*
32+
* @return \Ock\ClassFilesIterator\ClassFilesIA\ClassFilesIAInterface
33+
* @throws \ReflectionException
34+
*/
35+
public static function createFromClass(string $class, int $nLevelsUp = 0): ClassFilesIAInterface {
36+
$nsDir = NamespaceDirectory::createFromClass($class)
37+
->requireParentN($nLevelsUp);
38+
return self::createFromNsdirObject($nsDir);
39+
}
40+
41+
/**
42+
* @param \Ock\ClassFilesIterator\NamespaceDirectory $nsdir
43+
*
44+
* @return \Ock\ClassFilesIterator\ClassFilesIA\ClassFilesIAInterface
45+
*/
46+
public static function createFromNsdirObject(NamespaceDirectory $nsdir): ClassFilesIAInterface {
47+
if (!is_dir($nsdir->getDirectory())) {
48+
return new ClassFilesIA_Empty();
49+
}
50+
return new self(
51+
$nsdir->getDirectory(),
52+
$nsdir->getTerminatedNamespace(),
53+
);
54+
}
55+
56+
/**
57+
* @param string $directory
58+
* @param string $terminatedNamespace
59+
*/
60+
public function __construct(
61+
private string $directory,
62+
private readonly string $terminatedNamespace,
63+
) {}
64+
65+
/**
66+
* {@inheritdoc}
67+
*/
68+
public function withRealpathRoot(): static {
69+
$clone = clone $this;
70+
$realpath = realpath($this->directory);
71+
// @todo Properly handle this case.
72+
assert($realpath !== false, "Cannot get realpath for '$this->directory'. Perhaps the directory does not exist.");
73+
$clone->directory = $realpath;
74+
return $clone;
75+
}
76+
77+
/**
78+
* {@inheritdoc}
79+
*/
80+
public function getIterator(): \Iterator {
81+
return self::scan($this->directory, $this->terminatedNamespace);
82+
}
83+
84+
/**
85+
* @param string $dir
86+
* @param string $terminatedNamespace
87+
*
88+
* @return \Iterator<string, class-string>
89+
* Format: $[$file] = $class
90+
*/
91+
private static function scan(string $dir, string $terminatedNamespace): \Iterator {
92+
$candidates = \scandir($dir, \SCANDIR_SORT_ASCENDING);
93+
if ($candidates === false) {
94+
throw new \RuntimeException("Failed to scandir('$dir').");
95+
}
96+
foreach ($candidates as $candidate) {
97+
if ('.' === $candidate[0]) {
98+
continue;
99+
}
100+
$path = $dir . '/' . $candidate;
101+
if (str_ends_with($candidate, '.php')) {
102+
if (!is_file($path)) {
103+
continue;
104+
}
105+
$name = substr($candidate, 0, -4);
106+
if (!preg_match(self::CLASS_NAME_REGEX, $name)) {
107+
continue;
108+
}
109+
// The value is a class-string, but PhpStan does not know.
110+
// @phpstan-ignore generator.valueType
111+
yield $path => $terminatedNamespace . $name;
112+
}
113+
else {
114+
if (!is_dir($path)) {
115+
continue;
116+
}
117+
if (!preg_match(self::CLASS_NAME_REGEX, $candidate)) {
118+
continue;
119+
}
120+
// @todo Make PHP 7 version with "yield from".
121+
yield from self::scan($path, $terminatedNamespace . $candidate . '\\');
122+
}
123+
}
124+
}
125+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Ock\ClassFilesIterator\ClassFilesIA;
6+
7+
/**
8+
* @internal
9+
*/
10+
trait RealpathRootThisTrait {
11+
12+
/**
13+
* {@inheritdoc}
14+
*/
15+
public function withRealpathRoot(): static {
16+
return $this;
17+
}
18+
19+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ock\ClassFilesIterator\ClassNamesIA;
6+
7+
/**
8+
* @template-covariant TKey
9+
*
10+
* @template-extends \IteratorAggregate<TKey, class-string>
11+
*/
12+
interface ClassNamesIAInterface extends \IteratorAggregate {
13+
14+
/**
15+
* Iterates through class names, with arbitrary keys.
16+
*
17+
* @return \Iterator<TKey, class-string>
18+
* Format: $[*] = $class
19+
*/
20+
public function getIterator(): \Iterator;
21+
22+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ock\ClassFilesIterator\ClassNamesIA;
6+
7+
/**
8+
* @template TKey
9+
*
10+
* @template-implements ClassNamesIAInterface<TKey>
11+
*/
12+
class ClassNamesIA_Array implements ClassNamesIAInterface {
13+
14+
/**
15+
* Constructor.
16+
*
17+
* @param array<TKey, class-string> $values
18+
*/
19+
public function __construct(
20+
private readonly array $values,
21+
) {
22+
// Validate input if assertions are enabled.
23+
assert(!array_filter(
24+
$values,
25+
fn ($value) => !is_string($value),
26+
), 'Array values must be class names.');
27+
}
28+
29+
/**
30+
* {@inheritdoc}
31+
*/
32+
public function getIterator(): \Iterator {
33+
yield from $this->values;
34+
}
35+
36+
}

0 commit comments

Comments
 (0)