Skip to content

Commit 1d9b835

Browse files
authored
chore(core): improve discovery performance by excluding specific directories (#1331)
1 parent ea417ae commit 1d9b835

File tree

4 files changed

+86
-58
lines changed

4 files changed

+86
-58
lines changed

packages/core/src/Kernel/LoadDiscoveryClasses.php

Lines changed: 73 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,6 @@
44

55
namespace Tempest\Core\Kernel;
66

7-
use FilesystemIterator;
8-
use RecursiveDirectoryIterator;
9-
use RecursiveIteratorIterator;
10-
use SplFileInfo;
117
use Tempest\Container\Container;
128
use Tempest\Core\DiscoveryCache;
139
use Tempest\Core\DiscoveryCacheStrategy;
@@ -92,69 +88,85 @@ private function buildDiscovery(string $discoveryClass): Discovery
9288
}
9389

9490
foreach ($this->kernel->discoveryLocations as $location) {
95-
if ($this->shouldSkipLocation($location)) {
96-
continue;
97-
}
91+
$this->discoverPath($discovery, $location, $location->path);
92+
}
9893

99-
$directories = new RecursiveDirectoryIterator($location->path, FilesystemIterator::UNIX_PATHS | FilesystemIterator::SKIP_DOTS);
100-
$files = new RecursiveIteratorIterator($directories);
94+
return $discovery;
95+
}
10196

102-
/** @var SplFileInfo $file */
103-
foreach ($files as $file) {
104-
$fileName = $file->getFilename();
97+
private function discoverPath(Discovery $discovery, DiscoveryLocation $location, string $path): void
98+
{
99+
if ($this->shouldSkipLocation($location)) {
100+
return;
101+
}
105102

106-
if ($fileName === '') {
107-
continue;
108-
}
103+
$input = realpath($path);
109104

110-
if ($fileName === '.') {
111-
continue;
112-
}
105+
if ($input === false) {
106+
return;
107+
}
113108

114-
if ($fileName === '..') {
115-
continue;
116-
}
109+
// Make sure the path is not marked for skipping
110+
if ($this->shouldSkipBasedOnConfig($input)) {
111+
return;
112+
}
117113

118-
$input = $file->getRealPath();
114+
// Directories are scanned recursively
115+
if (is_dir($input)) {
116+
if ($this->shouldSkipDirectory($input)) {
117+
return;
118+
}
119119

120-
if ($this->shouldSkipBasedOnConfig($input)) {
120+
foreach (scandir($input) as $subPath) {
121+
if ($subPath === '.' || $subPath === '..') {
121122
continue;
122123
}
123124

124-
// We assume that any PHP file that starts with an uppercase letter will be a class
125-
if ($file->getExtension() === 'php' && ucfirst($fileName) === $fileName) {
126-
$className = $location->toClassName($file->getPathname());
127-
128-
// Discovery errors (syntax errors, missing imports, etc.)
129-
// are ignored when they happen in vendor files,
130-
// but they are allowed to be thrown in project code
131-
if ($location->isVendor()) {
132-
try {
133-
$input = new ClassReflector($className);
134-
} catch (Throwable) { // @mago-expect best-practices/no-empty-catch-clause
135-
}
136-
} elseif (class_exists($className)) {
137-
$input = new ClassReflector($className);
138-
}
139-
}
125+
$this->discoverPath($discovery, $location, "{$input}/{$subPath}");
126+
}
140127

141-
if ($this->shouldSkipBasedOnConfig($input)) {
142-
continue;
143-
}
128+
return;
129+
}
144130

145-
if ($input instanceof ClassReflector) {
146-
// If the input is a class, we'll call `discover`
147-
if (! $this->shouldSkipDiscoveryForClass($discovery, $input)) {
148-
$discovery->discover($location, $input);
149-
}
150-
} elseif ($discovery instanceof DiscoversPath) {
151-
// If the input is NOT a class, AND the discovery class can discover paths, we'll call `discoverPath`
152-
$discovery->discoverPath($location, $input);
131+
$pathInfo = pathinfo($input);
132+
$extension = $pathInfo['extension'] ?? null;
133+
$fileName = $pathInfo['filename'] ?: null;
134+
135+
// We assume that any PHP file that starts with an uppercase letter will be a class
136+
if ($extension === 'php' && ucfirst($fileName) === $fileName) {
137+
$className = $location->toClassName($input);
138+
139+
// Discovery errors (syntax errors, missing imports, etc.)
140+
// are ignored when they happen in vendor files,
141+
// but they are allowed to be thrown in project code
142+
if ($location->isVendor()) {
143+
try {
144+
$input = new ClassReflector($className);
145+
} catch (Throwable $e) { // @mago-expect best-practices/no-empty-catch-clause
153146
}
147+
} elseif (class_exists($className)) {
148+
$input = new ClassReflector($className);
154149
}
155150
}
156151

157-
return $discovery;
152+
// If the input is a class, we'll try to discover it
153+
if ($input instanceof ClassReflector) {
154+
// Check whether the class should be skipped
155+
if ($this->shouldSkipBasedOnConfig($input)) {
156+
return;
157+
}
158+
159+
// Check whether this class is marked with `#[SkipDiscovery]`
160+
if ($this->shouldSkipDiscoveryForClass($discovery, $input)) {
161+
return;
162+
}
163+
164+
$discovery->discover($location, $input);
165+
} elseif ($discovery instanceof DiscoversPath) {
166+
// If the input is NOT a class, AND the discovery class can discover paths, we'll call `discoverPath`
167+
// Note that we've already checked whether the path was marked for skipping earlier in this method
168+
$discovery->discoverPath($location, $input);
169+
}
158170
}
159171

160172
/**
@@ -212,4 +224,14 @@ private function shouldSkipLocation(DiscoveryLocation $location): bool
212224
DiscoveryCacheStrategy::PARTIAL => $location->isVendor(),
213225
};
214226
}
227+
228+
/**
229+
* Check whether a given directory should be skipped
230+
*/
231+
private function shouldSkipDirectory(string $path): bool
232+
{
233+
$directory = pathinfo($path, PATHINFO_BASENAME);
234+
235+
return $directory === 'node_modules';
236+
}
215237
}

packages/discovery/src/DiscoveryLocation.php

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,16 @@
66

77
final readonly class DiscoveryLocation
88
{
9+
public string $namespace;
10+
public string $path;
11+
912
public function __construct(
10-
public string $namespace,
11-
public string $path,
12-
) {}
13+
string $namespace,
14+
string $path,
15+
) {
16+
$this->namespace = $namespace;
17+
$this->path = realpath(rtrim($path, '\\/'));
18+
}
1319

1420
public function isVendor(): bool
1521
{
@@ -18,12 +24,10 @@ public function isVendor(): bool
1824

1925
public function toClassName(string $path): string
2026
{
21-
$pathWithoutSlashes = rtrim($this->path, '\\/');
22-
2327
// Try to create a PSR-compliant class name from the path
2428
return str_replace(
2529
[
26-
$pathWithoutSlashes,
30+
$this->path,
2731
'/',
2832
'\\\\',
2933
'.php',

packages/mapper/src/Mappers/JsonToArrayMapper.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
{
1111
public function canMap(mixed $from, mixed $to): bool
1212
{
13-
return is_string($from) && json_validate($from);
13+
return false;
1414
}
1515

1616
public function map(mixed $from, mixed $to): array

tests/Integration/Mapper/MapperTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
use Tempest\DateTime\DateTime;
99
use Tempest\DateTime\DateTimeInterface;
1010
use Tempest\Mapper\Exceptions\MappingValuesWereMissing;
11+
use Tempest\Mapper\Mappers\ArrayToObjectMapper;
12+
use Tempest\Mapper\Mappers\JsonToObjectMapper;
1113
use Tempest\Mapper\Mappers\ObjectToArrayMapper;
1214
use Tests\Tempest\Fixtures\Modules\Books\Models\Author;
1315
use Tests\Tempest\Fixtures\Modules\Books\Models\AuthorType;

0 commit comments

Comments
 (0)