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\Factory\Exception\MissingInstalledJson;
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_merge_recursive;
23: use function array_values;
24: use function assert;
25: use function file_get_contents;
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: use function rtrim;
32:
33: use const JSON_THROW_ON_ERROR;
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 $composer */
60: $composer = json_decode($composerJsonContent, true, 512, JSON_THROW_ON_ERROR);
61: $vendorDir = rtrim($composer['config']['vendor-dir'] ?? 'vendor', '/');
62:
63: $installedJsonPath = $realInstallationPath . '/' . $vendorDir . '/composer/installed.json';
64:
65: if (! is_file($installedJsonPath)) {
66: throw MissingInstalledJson::inProjectPath($realInstallationPath . '/' . $vendorDir);
67: }
68:
69: $jsonContent = file_get_contents($installedJsonPath);
70: assert(is_string($jsonContent));
71:
72: /** @psalm-var array{packages?: list<ComposerPackage>}|list<ComposerPackage> $installedJson */
73: $installedJson = json_decode($jsonContent, true, 512, JSON_THROW_ON_ERROR);
74:
75: /** @psalm-var list<ComposerPackage> $installed */
76: $installed = $installedJson['packages'] ?? $installedJson;
77:
78: $classMapPaths = array_merge(
79: $this->prefixPaths($this->packageToClassMapPaths($composer), $realInstallationPath . '/'),
80: ...array_map(fn (array $package): array => $this->prefixPaths(
81: $this->packageToClassMapPaths($package),
82: $this->packagePrefixPath($realInstallationPath, $package, $vendorDir),
83: ), $installed),
84: );
85: $classMapFiles = array_filter($classMapPaths, 'is_file');
86: $classMapDirectories = array_values(array_filter($classMapPaths, 'is_dir'));
87: $filePaths = array_merge(
88: $this->prefixPaths($this->packageToFilePaths($composer), $realInstallationPath . '/'),
89: ...array_map(fn (array $package): array => $this->prefixPaths(
90: $this->packageToFilePaths($package),
91: $this->packagePrefixPath($realInstallationPath, $package, $vendorDir),
92: ), $installed),
93: );
94:
95: return new AggregateSourceLocator(array_merge(
96: [
97: new PsrAutoloaderLocator(
98: Psr4Mapping::fromArrayMappings(array_merge_recursive(
99: $this->prefixWithInstallationPath($this->packageToPsr4AutoloadNamespaces($composer), $realInstallationPath),
100: ...array_map(fn (array $package): array => $this->prefixWithPackagePath(
101: $this->packageToPsr4AutoloadNamespaces($package),
102: $realInstallationPath,
103: $package,
104: $vendorDir,
105: ), $installed),
106: )),
107: $astLocator,
108: ),
109: new PsrAutoloaderLocator(
110: Psr0Mapping::fromArrayMappings(array_merge_recursive(
111: $this->prefixWithInstallationPath($this->packageToPsr0AutoloadNamespaces($composer), $realInstallationPath),
112: ...array_map(fn (array $package): array => $this->prefixWithPackagePath(
113: $this->packageToPsr0AutoloadNamespaces($package),
114: $realInstallationPath,
115: $package,
116: $vendorDir,
117: ), $installed),
118: )),
119: $astLocator,
120: ),
121: new DirectoriesSourceLocator($classMapDirectories, $astLocator),
122: ],
123: ...array_map(
124: static function (string $file) use ($astLocator): array {
125: assert($file !== '');
126:
127: return [new SingleFileSourceLocator($file, $astLocator)];
128: },
129: array_merge($classMapFiles, $filePaths),
130: ),
131: ));
132: }
133:
134: /**
135: * @param ComposerPackage|Composer $package
136: *
137: * @return array<string, list<string>>
138: */
139: private function packageToPsr4AutoloadNamespaces(array $package): array
140: {
141: return array_map(static fn ($namespacePaths): array => (array) $namespacePaths, $package['autoload']['psr-4'] ?? []);
142: }
143:
144: /**
145: * @param ComposerPackage|Composer $package
146: *
147: * @return array<string, list<string>>
148: */
149: private function packageToPsr0AutoloadNamespaces(array $package): array
150: {
151: return array_map(static fn ($namespacePaths): array => (array) $namespacePaths, $package['autoload']['psr-0'] ?? []);
152: }
153:
154: /**
155: * @param ComposerPackage|Composer $package
156: *
157: * @return list<string>
158: */
159: private function packageToClassMapPaths(array $package): array
160: {
161: return $package['autoload']['classmap'] ?? [];
162: }
163:
164: /**
165: * @param ComposerPackage|Composer $package
166: *
167: * @return list<string>
168: */
169: private function packageToFilePaths(array $package): array
170: {
171: return $package['autoload']['files'] ?? [];
172: }
173:
174: /** @param ComposerPackage $package */
175: private function packagePrefixPath(string $trimmedInstallationPath, array $package, string $vendorDir): string
176: {
177: return $trimmedInstallationPath . '/' . $vendorDir . '/' . $package['name'] . '/';
178: }
179:
180: /**
181: * @param array<string, list<string>> $paths
182: * @param ComposerPackage $package
183: *
184: * @return array<string, list<string>>
185: */
186: private function prefixWithPackagePath(array $paths, string $trimmedInstallationPath, array $package, string $vendorDir): array
187: {
188: $prefix = $this->packagePrefixPath($trimmedInstallationPath, $package, $vendorDir);
189:
190: return array_map(fn (array $paths): array => $this->prefixPaths($paths, $prefix), $paths);
191: }
192:
193: /**
194: * @param array<string, list<string>> $paths
195: *
196: * @return array<string, list<string>>
197: */
198: private function prefixWithInstallationPath(array $paths, string $trimmedInstallationPath): array
199: {
200: return array_map(fn (array $paths): array => $this->prefixPaths($paths, $trimmedInstallationPath . '/'), $paths);
201: }
202:
203: /**
204: * @param list<string> $paths
205: *
206: * @return list<string>
207: */
208: private function prefixPaths(array $paths, string $prefix): array
209: {
210: return array_map(static fn (string $path): string => $prefix . $path, $paths);
211: }
212: }
213: