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 MakeLocatorForInstalledJson
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: /** @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: [],
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: [],
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: /** @phpstan-ignore argument.type */
99: Psr4Mapping::fromArrayMappings(array_merge_recursive(
100: [],
101: ...array_map(fn (array $package): array => $this->prefixWithPackagePath(
102: $this->packageToPsr4AutoloadNamespaces($package),
103: $realInstallationPath,
104: $package,
105: $vendorDir,
106: ), $installed),
107: )),
108: $astLocator,
109: ),
110: new PsrAutoloaderLocator(
111: /** @phpstan-ignore argument.type */
112: Psr0Mapping::fromArrayMappings(array_merge_recursive(
113: [],
114: ...array_map(fn (array $package): array => $this->prefixWithPackagePath(
115: $this->packageToPsr0AutoloadNamespaces($package),
116: $realInstallationPath,
117: $package,
118: $vendorDir,
119: ), $installed),
120: )),
121: $astLocator,
122: ),
123: new DirectoriesSourceLocator($classMapDirectories, $astLocator),
124: ],
125: ...array_map(
126: static function (string $file) use ($astLocator): array {
127: assert($file !== '');
128:
129: return [new SingleFileSourceLocator($file, $astLocator)];
130: },
131: array_merge($classMapFiles, $filePaths),
132: ),
133: ));
134: }
135:
136: /**
137: * @param ComposerPackage $package
138: *
139: * @return array<string, list<string>>
140: */
141: private function packageToPsr4AutoloadNamespaces(array $package): array
142: {
143: return array_map(static fn ($namespacePaths): array => (array) $namespacePaths, $package['autoload']['psr-4'] ?? []);
144: }
145:
146: /**
147: * @param ComposerPackage $package
148: *
149: * @return array<string, list<string>>
150: */
151: private function packageToPsr0AutoloadNamespaces(array $package): array
152: {
153: return array_map(static fn ($namespacePaths): array => (array) $namespacePaths, $package['autoload']['psr-0'] ?? []);
154: }
155:
156: /**
157: * @param ComposerPackage $package
158: *
159: * @return list<string>
160: */
161: private function packageToClassMapPaths(array $package): array
162: {
163: return $package['autoload']['classmap'] ?? [];
164: }
165:
166: /**
167: * @param ComposerPackage $package
168: *
169: * @return list<string>
170: */
171: private function packageToFilePaths(array $package): array
172: {
173: return $package['autoload']['files'] ?? [];
174: }
175:
176: /** @param ComposerPackage $package */
177: private function packagePrefixPath(string $trimmedInstallationPath, array $package, string $vendorDir): string
178: {
179: return $trimmedInstallationPath . '/' . $vendorDir . '/' . $package['name'] . '/';
180: }
181:
182: /**
183: * @param array<int|string, array<string>> $paths
184: * @param ComposerPackage $package
185: *
186: * @return array<int|string, array<string>>
187: */
188: private function prefixWithPackagePath(array $paths, string $trimmedInstallationPath, array $package, string $vendorDir): array
189: {
190: $prefix = $this->packagePrefixPath($trimmedInstallationPath, $package, $vendorDir);
191:
192: return array_map(fn (array $paths): array => $this->prefixPaths($paths, $prefix), $paths);
193: }
194:
195: /**
196: * @param array<int|string, string> $paths
197: *
198: * @return array<int|string, string>
199: */
200: private function prefixPaths(array $paths, string $prefix): array
201: {
202: return array_map(static fn (string $path): string => $prefix . $path, $paths);
203: }
204: }
205: