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\Factory\Exception\MissingInstalledJson;
13: use PHPStan\BetterReflection\SourceLocator\Type\Composer\Psr\Psr0Mapping;
14: use PHPStan\BetterReflection\SourceLocator\Type\Composer\Psr\Psr4Mapping;
15: use PHPStan\BetterReflection\SourceLocator\Type\Composer\PsrAutoloaderLocator;
16: use PHPStan\BetterReflection\SourceLocator\Type\DirectoriesSourceLocator;
17: use PHPStan\BetterReflection\SourceLocator\Type\SingleFileSourceLocator;
18: use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator;
19:
20: use function array_filter;
21: use function array_map;
22: use function array_merge;
23: use function array_merge_recursive;
24: use function array_values;
25: use function assert;
26: use function file_get_contents;
27: use function is_array;
28: use function is_dir;
29: use function is_file;
30: use function is_string;
31: use function json_decode;
32: use function realpath;
33: use function rtrim;
34:
35: /**
36: * @psalm-import-type ComposerAutoload from MakeLocatorForComposerJson
37: * @psalm-import-type ComposerPackage from MakeLocatorForComposerJson
38: * @psalm-import-type Composer from MakeLocatorForComposerJson
39: */
40: final class MakeLocatorForComposerJsonAndInstalledJson
41: {
42: public function __invoke(string $installationPath, Locator $astLocator): SourceLocator
43: {
44: $realInstallationPath = (string) realpath($installationPath);
45:
46: if (! is_dir($realInstallationPath)) {
47: throw InvalidProjectDirectory::atPath($installationPath);
48: }
49:
50: $composerJsonPath = $realInstallationPath . '/composer.json';
51:
52: if (! is_file($composerJsonPath)) {
53: throw MissingComposerJson::inProjectPath($installationPath);
54: }
55:
56: $composerJsonContent = file_get_contents($composerJsonPath);
57: assert(is_string($composerJsonContent));
58:
59: /** @psalm-var Composer|null $composer */
60: $composer = json_decode($composerJsonContent, true);
61: $vendorDir = $composer['config']['vendor-dir'] ?? 'vendor';
62: $vendorDir = rtrim($vendorDir, '/');
63:
64: $installedJsonPath = $realInstallationPath . '/' . $vendorDir . '/composer/installed.json';
65:
66: if (! is_file($installedJsonPath)) {
67: throw MissingInstalledJson::inProjectPath($realInstallationPath . '/' . $vendorDir);
68: }
69:
70: $jsonContent = file_get_contents($installedJsonPath);
71: assert(is_string($jsonContent));
72:
73: /** @psalm-var array{packages: list<mixed[]>}|list<mixed[]>|null $installedJson */
74: $installedJson = json_decode($jsonContent, true);
75:
76: if (! is_array($composer)) {
77: throw FailedToParseJson::inFile($composerJsonPath);
78: }
79:
80: if (! is_array($installedJson)) {
81: throw FailedToParseJson::inFile($installedJsonPath);
82: }
83:
84: /** @psalm-var list<ComposerPackage> $installed */
85: $installed = $installedJson['packages'] ?? $installedJson;
86:
87: $classMapPaths = array_merge($this->prefixPaths($this->packageToClassMapPaths($composer), $realInstallationPath . '/'), ...array_map(function (array $package) use ($realInstallationPath, $vendorDir) : array {
88: return $this->prefixPaths($this->packageToClassMapPaths($package), $this->packagePrefixPath($realInstallationPath, $package, $vendorDir));
89: }, $installed));
90: $classMapFiles = array_filter($classMapPaths, 'is_file');
91: $classMapDirectories = array_values(array_filter($classMapPaths, 'is_dir'));
92: $filePaths = array_merge($this->prefixPaths($this->packageToFilePaths($composer), $realInstallationPath . '/'), ...array_map(function (array $package) use ($realInstallationPath, $vendorDir) : array {
93: return $this->prefixPaths($this->packageToFilePaths($package), $this->packagePrefixPath($realInstallationPath, $package, $vendorDir));
94: }, $installed));
95:
96: return new AggregateSourceLocator(array_merge([
97: new PsrAutoloaderLocator(Psr4Mapping::fromArrayMappings(array_merge_recursive($this->prefixWithInstallationPath($this->packageToPsr4AutoloadNamespaces($composer), $realInstallationPath), ...array_map(function (array $package) use ($realInstallationPath, $vendorDir) : array {
98: return $this->prefixWithPackagePath($this->packageToPsr4AutoloadNamespaces($package), $realInstallationPath, $package, $vendorDir);
99: }, $installed))), $astLocator),
100: new PsrAutoloaderLocator(Psr0Mapping::fromArrayMappings(array_merge_recursive($this->prefixWithInstallationPath($this->packageToPsr0AutoloadNamespaces($composer), $realInstallationPath), ...array_map(function (array $package) use ($realInstallationPath, $vendorDir) : array {
101: return $this->prefixWithPackagePath($this->packageToPsr0AutoloadNamespaces($package), $realInstallationPath, $package, $vendorDir);
102: }, $installed))), $astLocator),
103: new DirectoriesSourceLocator($classMapDirectories, $astLocator),
104: ], ...array_map(static function (string $file) use ($astLocator): array {
105: assert($file !== '');
106:
107: return [new SingleFileSourceLocator($file, $astLocator)];
108: }, array_merge($classMapFiles, $filePaths))));
109: }
110:
111: /**
112: * @param ComposerPackage|Composer $package
113: *
114: * @return array<string, list<string>>
115: */
116: private function packageToPsr4AutoloadNamespaces(array $package): array
117: {
118: return array_map(static function ($namespacePaths) : array {
119: return (array) $namespacePaths;
120: }, $package['autoload']['psr-4'] ?? []);
121: }
122:
123: /**
124: * @param ComposerPackage|Composer $package
125: *
126: * @return array<string, list<string>>
127: */
128: private function packageToPsr0AutoloadNamespaces(array $package): array
129: {
130: return array_map(static function ($namespacePaths) : array {
131: return (array) $namespacePaths;
132: }, $package['autoload']['psr-0'] ?? []);
133: }
134:
135: /**
136: * @param ComposerPackage|Composer $package
137: *
138: * @return list<string>
139: */
140: private function packageToClassMapPaths(array $package): array
141: {
142: return $package['autoload']['classmap'] ?? [];
143: }
144:
145: /**
146: * @param ComposerPackage|Composer $package
147: *
148: * @return list<string>
149: */
150: private function packageToFilePaths(array $package): array
151: {
152: return $package['autoload']['files'] ?? [];
153: }
154:
155: /** @param ComposerPackage $package */
156: private function packagePrefixPath(string $trimmedInstallationPath, array $package, string $vendorDir): string
157: {
158: return $trimmedInstallationPath . '/' . $vendorDir . '/' . $package['name'] . '/';
159: }
160:
161: /**
162: * @param array<string, list<string>> $paths
163: * @param ComposerPackage $package $package
164: *
165: * @return array<string, list<string>>
166: */
167: private function prefixWithPackagePath(array $paths, string $trimmedInstallationPath, array $package, string $vendorDir): array
168: {
169: $prefix = $this->packagePrefixPath($trimmedInstallationPath, $package, $vendorDir);
170:
171: return array_map(function (array $paths) use ($prefix) : array {
172: return $this->prefixPaths($paths, $prefix);
173: }, $paths);
174: }
175:
176: /**
177: * @param array<string, list<string>> $paths
178: *
179: * @return array<string, list<string>>
180: */
181: private function prefixWithInstallationPath(array $paths, string $trimmedInstallationPath): array
182: {
183: return array_map(function (array $paths) use ($trimmedInstallationPath) : array {
184: return $this->prefixPaths($paths, $trimmedInstallationPath . '/');
185: }, $paths);
186: }
187:
188: /**
189: * @param list<string> $paths
190: *
191: * @return list<string>
192: */
193: private function prefixPaths(array $paths, string $prefix): array
194: {
195: return array_map(static function (string $path) use ($prefix) : string {
196: return $prefix . $path;
197: }, $paths);
198: }
199: }
200: