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\FailedToParseJson;
10: use PHPStan\BetterReflection\SourceLocator\Type\Composer\Factory\Exception\InvalidProjectDirectory;
11: use PHPStan\BetterReflection\SourceLocator\Type\Composer\Factory\Exception\MissingComposerJson;
12: use PHPStan\BetterReflection\SourceLocator\Type\Composer\Psr\Psr0Mapping;
13: use PHPStan\BetterReflection\SourceLocator\Type\Composer\Psr\Psr4Mapping;
14: use PHPStan\BetterReflection\SourceLocator\Type\Composer\PsrAutoloaderLocator;
15: use PHPStan\BetterReflection\SourceLocator\Type\DirectoriesSourceLocator;
16: use PHPStan\BetterReflection\SourceLocator\Type\SingleFileSourceLocator;
17: use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator;
18:
19: use function array_filter;
20: use function array_map;
21: use function array_merge;
22: use function array_values;
23: use function assert;
24: use function file_get_contents;
25: use function is_array;
26: use function is_dir;
27: use function is_file;
28: use function is_string;
29: use function json_decode;
30: use function realpath;
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}|null $composer */
69: $composer = json_decode($composerJsonContent, true);
70:
71: if (! is_array($composer)) {
72: throw FailedToParseJson::inFile($composerJsonPath);
73: }
74:
75: $pathPrefix = $realInstallationPath . '/';
76: $classMapPaths = $this->prefixPaths($this->packageToClassMapPaths($composer), $pathPrefix);
77: $classMapFiles = array_filter($classMapPaths, 'is_file');
78: $classMapDirectories = array_values(array_filter($classMapPaths, 'is_dir'));
79: $filePaths = $this->prefixPaths($this->packageToFilePaths($composer), $pathPrefix);
80:
81: return new AggregateSourceLocator(array_merge([
82: new PsrAutoloaderLocator(Psr4Mapping::fromArrayMappings($this->prefixWithInstallationPath($this->packageToPsr4AutoloadNamespaces($composer), $pathPrefix)), $astLocator),
83: new PsrAutoloaderLocator(Psr0Mapping::fromArrayMappings($this->prefixWithInstallationPath($this->packageToPsr0AutoloadNamespaces($composer), $pathPrefix)), $astLocator),
84: new DirectoriesSourceLocator($classMapDirectories, $astLocator),
85: ], ...array_map(static function (string $file) use ($astLocator): array {
86: assert($file !== '');
87:
88: return [new SingleFileSourceLocator($file, $astLocator)];
89: }, array_merge($classMapFiles, $filePaths))));
90: }
91:
92: /**
93: * @param array{autoload: ComposerAutoload} $package
94: *
95: * @return array<string, list<string>>
96: */
97: private function packageToPsr4AutoloadNamespaces(array $package): array
98: {
99: return array_map(static function ($namespacePaths) : array {
100: return (array) $namespacePaths;
101: }, $package['autoload']['psr-4'] ?? []);
102: }
103:
104: /**
105: * @param array{autoload: ComposerAutoload} $package
106: *
107: * @return array<string, list<string>>
108: */
109: private function packageToPsr0AutoloadNamespaces(array $package): array
110: {
111: return array_map(static function ($namespacePaths) : array {
112: return (array) $namespacePaths;
113: }, $package['autoload']['psr-0'] ?? []);
114: }
115:
116: /**
117: * @param array{autoload: ComposerAutoload} $package
118: *
119: * @return list<string>
120: */
121: private function packageToClassMapPaths(array $package): array
122: {
123: return $package['autoload']['classmap'] ?? [];
124: }
125:
126: /**
127: * @param array{autoload: ComposerAutoload} $package
128: *
129: * @return list<string>
130: */
131: private function packageToFilePaths(array $package): array
132: {
133: return $package['autoload']['files'] ?? [];
134: }
135:
136: /**
137: * @param array<string, list<string>> $paths
138: *
139: * @return array<string, list<string>>
140: */
141: private function prefixWithInstallationPath(array $paths, string $trimmedInstallationPath): array
142: {
143: return array_map(function (array $paths) use ($trimmedInstallationPath) : array {
144: return $this->prefixPaths($paths, $trimmedInstallationPath);
145: }, $paths);
146: }
147:
148: /**
149: * @param list<string> $paths
150: *
151: * @return list<string>
152: */
153: private function prefixPaths(array $paths, string $prefix): array
154: {
155: return array_map(static function (string $path) use ($prefix) : string {
156: return $prefix . $path;
157: }, $paths);
158: }
159: }
160: