-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathComposerNamespaceResolver.php
More file actions
152 lines (124 loc) · 4.34 KB
/
ComposerNamespaceResolver.php
File metadata and controls
152 lines (124 loc) · 4.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
<?php
declare(strict_types=1);
namespace KaririCode\ClassDiscovery\Scanner;
use KaririCode\ClassDiscovery\Contract\NamespaceResolver;
/**
* PSR-4 namespace resolver using composer.json autoload mappings.
*
* Parses composer.json from the project root to build namespace prefix
* to directory mappings. Supports both autoload and autoload-dev sections.
*
* @package KaririCode\ClassDiscovery\Scanner
* @author Walmir Silva <walmir.silva@kariricode.org>
* @license MIT
* @since 3.0.0
*/
final class ComposerNamespaceResolver implements NamespaceResolver
{
/** @var array<string, string> Prefix => directory */
private array $mappings = [];
/**
* @param string|null $composerJsonPath Path to composer.json (auto-detected if null)
* @param bool $includeDevAutoload Include autoload-dev mappings
*/
public function __construct(
?string $composerJsonPath = null,
bool $includeDevAutoload = false,
) {
$composerJsonPath ??= $this->detectComposerJson();
if ($composerJsonPath !== null && is_file($composerJsonPath)) {
$this->loadFromComposerJson($composerJsonPath, $includeDevAutoload);
}
}
#[\Override]
public function resolve(string $filePath): ?string
{
$realPath = realpath($filePath);
if ($realPath === false) {
return null;
}
// Normalize path separators
$normalizedPath = str_replace('\\', '/', $realPath);
foreach ($this->mappings as $prefix => $directory) {
$normalizedDir = str_replace('\\', '/', rtrim($directory, '/')) . '/';
if (! str_starts_with($normalizedPath, $normalizedDir)) {
continue;
}
$relativePath = substr($normalizedPath, \strlen($normalizedDir));
// Remove .php extension
$relativePath = preg_replace('/\.php$/i', '', $relativePath);
if ($relativePath === null) {
continue;
}
// Convert path separators to namespace separators
$className = str_replace('/', '\\', $relativePath);
return $prefix . '\\' . $className;
}
return null;
}
#[\Override]
public function addMapping(string $prefix, string $directory): self
{
$resolved = realpath($directory);
if ($resolved !== false) {
$this->mappings[rtrim($prefix, '\\')] = $resolved;
}
return $this;
}
/**
* Load PSR-4 mappings from composer.json.
*/
private function loadFromComposerJson(string $path, bool $includeDevAutoload): void
{
$content = @file_get_contents($path);
if ($content === false) {
return;
}
$data = json_decode($content, true);
if (! \is_array($data)) {
return;
}
$baseDir = \dirname(realpath($path) ?: $path);
$this->extractPsr4Mappings($data['autoload']['psr-4'] ?? [], $baseDir); // @phpstan-ignore offsetAccess.nonOffsetAccessible, argument.type
if ($includeDevAutoload) {
$this->extractPsr4Mappings($data['autoload-dev']['psr-4'] ?? [], $baseDir);
}
}
/**
* Extract and register PSR-4 prefix-to-directory mappings.
*
* @param array<string, string|array<string>> $psr4 Composer PSR-4 autoload config
* @param string $baseDir Project root directory
*/
private function extractPsr4Mappings(array $psr4, string $baseDir): void
{
foreach ($psr4 as $prefix => $paths) {
$paths = \is_array($paths) ? $paths : [$paths];
foreach ($paths as $dir) {
$absoluteDir = $baseDir . '/' . rtrim($dir, '/');
$resolved = realpath($absoluteDir);
if ($resolved !== false) {
$this->mappings[rtrim($prefix, '\\')] = $resolved;
}
}
}
}
/**
* Auto-detect composer.json by walking up from CWD.
*/
private function detectComposerJson(): ?string
{
$dir = getcwd();
if ($dir === false) {
return null;
}
while ($dir !== \dirname($dir)) {
$candidate = $dir . '/composer.json';
if (is_file($candidate)) {
return $candidate;
}
$dir = \dirname($dir);
}
return null;
}
}