1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace PHPStan\BetterReflection\SourceLocator\Type\Composer\Factory;
6:
7: use PHPStan\BetterReflection\SourceLocator\Ast\Locator;
8: use PHPStan\BetterReflection\SourceLocator\Type\AggregateSourceLocator;
9: use PHPStan\BetterReflection\SourceLocator\Type\Composer\Factory\Exception\InvalidProjectDirectory;
10: use PHPStan\BetterReflection\SourceLocator\Type\Composer\Factory\Exception\MissingComposerJson;
11: use PHPStan\BetterReflection\SourceLocator\Type\Composer\Psr\Psr0Mapping;
12: use PHPStan\BetterReflection\SourceLocator\Type\Composer\Psr\Psr4Mapping;
13: use PHPStan\BetterReflection\SourceLocator\Type\Composer\PsrAutoloaderLocator;
14: use PHPStan\BetterReflection\SourceLocator\Type\DirectoriesSourceLocator;
15: use PHPStan\BetterReflection\SourceLocator\Type\SingleFileSourceLocator;
16: use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator;
17:
18: use function array_filter;
19: use function array_map;
20: use function array_merge;
21: use function array_values;
22: use function assert;
23: use function file_get_contents;
24: use function is_dir;
25: use function is_file;
26: use function is_string;
27: use function json_decode;
28: use function realpath;
29:
30: use const JSON_THROW_ON_ERROR;
31:
32: /**
33: * @psalm-type ComposerAutoload array{
34: * psr-0?: array<string, string|list<string>>,
35: * psr-4?: array<string, string|list<string>>,
36: * classmap?: list<string>,
37: * files?: list<string>,
38: * exclude-from-classmap?: list<string>
39: * }
40: * @psalm-type ComposerPackage array{
41: * name: string,
42: * autoload: ComposerAutoload
43: * }
44: * @psalm-type Composer array{
45: * autoload: ComposerAutoload,
46: * config?: array{vendor-dir?: string}
47: * }
48: */
49: final class MakeLocatorForComposerJson
50: {
51: public function __invoke(string $installationPath, Locator $astLocator): SourceLocator
52: {
53: $realInstallationPath = (string) realpath($installationPath);
54:
55: if (! is_dir($realInstallationPath)) {
56: throw InvalidProjectDirectory::atPath($installationPath);
57: }
58:
59: $composerJsonPath = $realInstallationPath . '/composer.json';
60:
61: if (! is_file($composerJsonPath)) {
62: throw MissingComposerJson::inProjectPath($installationPath);
63: }
64:
65: $composerJsonContent = file_get_contents($composerJsonPath);
66: assert(is_string($composerJsonContent));
67:
68: /** @psalm-var array{autoload: ComposerAutoload} $composer */
69: $composer = json_decode($composerJsonContent, true, 512, JSON_THROW_ON_ERROR);
70:
71: $pathPrefix = $realInstallationPath . '/';
72: $classMapPaths = $this->prefixPaths($this->packageToClassMapPaths($composer), $pathPrefix);
73: $classMapFiles = array_filter($classMapPaths, 'is_file');
74: $classMapDirectories = array_values(array_filter($classMapPaths, 'is_dir'));
75: $filePaths = $this->prefixPaths($this->packageToFilePaths($composer), $pathPrefix);
76:
77: return new AggregateSourceLocator(array_merge(
78: [
79: new PsrAutoloaderLocator(
80: Psr4Mapping::fromArrayMappings(
81: $this->prefixWithInstallationPath($this->packageToPsr4AutoloadNamespaces($composer), $pathPrefix),
82: ),
83: $astLocator,
84: ),
85: new PsrAutoloaderLocator(
86: Psr0Mapping::fromArrayMappings(
87: $this->prefixWithInstallationPath($this->packageToPsr0AutoloadNamespaces($composer), $pathPrefix),
88: ),
89: $astLocator,
90: ),
91: new DirectoriesSourceLocator($classMapDirectories, $astLocator),
92: ],
93: ...array_map(
94: static function (string $file) use ($astLocator): array {
95: assert($file !== '');
96:
97: return [new SingleFileSourceLocator($file, $astLocator)];
98: },
99: array_merge($classMapFiles, $filePaths),
100: ),
101: ));
102: }
103:
104: /**
105: * @param array{autoload: ComposerAutoload} $package
106: *
107: * @return array<string, list<string>>
108: */
109: private function packageToPsr4AutoloadNamespaces(array $package): array
110: {
111: return array_map(static fn ($namespacePaths): array => (array) $namespacePaths, $package['autoload']['psr-4'] ?? []);
112: }
113:
114: /**
115: * @param array{autoload: ComposerAutoload} $package
116: *
117: * @return array<string, list<string>>
118: */
119: private function packageToPsr0AutoloadNamespaces(array $package): array
120: {
121: return array_map(static fn ($namespacePaths): array => (array) $namespacePaths, $package['autoload']['psr-0'] ?? []);
122: }
123:
124: /**
125: * @param array{autoload: ComposerAutoload} $package
126: *
127: * @return list<string>
128: */
129: private function packageToClassMapPaths(array $package): array
130: {
131: return $package['autoload']['classmap'] ?? [];
132: }
133:
134: /**
135: * @param array{autoload: ComposerAutoload} $package
136: *
137: * @return list<string>
138: */
139: private function packageToFilePaths(array $package): array
140: {
141: return $package['autoload']['files'] ?? [];
142: }
143:
144: /**
145: * @param array<string, list<string>> $paths
146: *
147: * @return array<string, list<string>>
148: */
149: private function prefixWithInstallationPath(array $paths, string $trimmedInstallationPath): array
150: {
151: return array_map(fn (array $paths): array => $this->prefixPaths($paths, $trimmedInstallationPath), $paths);
152: }
153:
154: /**
155: * @param list<string> $paths
156: *
157: * @return list<string>
158: */
159: private function prefixPaths(array $paths, string $prefix): array
160: {
161: return array_map(static fn (string $path): string => $prefix . $path, $paths);
162: }
163: }
164: