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([], ...array_map(function (array $package) use ($realInstallationPath, $vendorDir) : array {
79: return $this->prefixPaths($this->packageToClassMapPaths($package), $this->packagePrefixPath($realInstallationPath, $package, $vendorDir));
80: }, $installed));
81: $classMapFiles = array_filter($classMapPaths, 'is_file');
82: $classMapDirectories = array_values(array_filter($classMapPaths, 'is_dir'));
83: $filePaths = array_merge([], ...array_map(function (array $package) use ($realInstallationPath, $vendorDir) : array {
84: return $this->prefixPaths($this->packageToFilePaths($package), $this->packagePrefixPath($realInstallationPath, $package, $vendorDir));
85: }, $installed));
86:
87: return new AggregateSourceLocator(array_merge([
88: new PsrAutoloaderLocator(Psr4Mapping::fromArrayMappings(array_merge_recursive([], ...array_map(function (array $package) use ($realInstallationPath, $vendorDir) : array {
89: return $this->prefixWithPackagePath($this->packageToPsr4AutoloadNamespaces($package), $realInstallationPath, $package, $vendorDir);
90: }, $installed))), $astLocator),
91: new PsrAutoloaderLocator(Psr0Mapping::fromArrayMappings(array_merge_recursive([], ...array_map(function (array $package) use ($realInstallationPath, $vendorDir) : array {
92: return $this->prefixWithPackagePath($this->packageToPsr0AutoloadNamespaces($package), $realInstallationPath, $package, $vendorDir);
93: }, $installed))), $astLocator),
94: new DirectoriesSourceLocator($classMapDirectories, $astLocator),
95: ], ...array_map(static function (string $file) use ($astLocator): array {
96: assert($file !== '');
97:
98: return [new SingleFileSourceLocator($file, $astLocator)];
99: }, array_merge($classMapFiles, $filePaths))));
100: }
101:
102: /**
103: * @param ComposerPackage $package
104: *
105: * @return array<string, list<string>>
106: */
107: private function packageToPsr4AutoloadNamespaces(array $package): array
108: {
109: return array_map(static function ($namespacePaths) : array {
110: return (array) $namespacePaths;
111: }, $package['autoload']['psr-4'] ?? []);
112: }
113:
114: /**
115: * @param ComposerPackage $package
116: *
117: * @return array<string, list<string>>
118: */
119: private function packageToPsr0AutoloadNamespaces(array $package): array
120: {
121: return array_map(static function ($namespacePaths) : array {
122: return (array) $namespacePaths;
123: }, $package['autoload']['psr-0'] ?? []);
124: }
125:
126: /**
127: * @param ComposerPackage $package
128: *
129: * @return list<string>
130: */
131: private function packageToClassMapPaths(array $package): array
132: {
133: return $package['autoload']['classmap'] ?? [];
134: }
135:
136: /**
137: * @param ComposerPackage $package
138: *
139: * @return list<string>
140: */
141: private function packageToFilePaths(array $package): array
142: {
143: return $package['autoload']['files'] ?? [];
144: }
145:
146: /** @param ComposerPackage $package */
147: private function packagePrefixPath(string $trimmedInstallationPath, array $package, string $vendorDir): string
148: {
149: return $trimmedInstallationPath . '/' . $vendorDir . '/' . $package['name'] . '/';
150: }
151:
152: /**
153: * @param array<int|string, array<string>> $paths
154: * @param ComposerPackage $package
155: *
156: * @return array<int|string, string|array<string>>
157: */
158: private function prefixWithPackagePath(array $paths, string $trimmedInstallationPath, array $package, string $vendorDir): array
159: {
160: $prefix = $this->packagePrefixPath($trimmedInstallationPath, $package, $vendorDir);
161:
162: return array_map(function (array $paths) use ($prefix) : array {
163: return $this->prefixPaths($paths, $prefix);
164: }, $paths);
165: }
166:
167: /**
168: * @param array<int|string, string> $paths
169: *
170: * @return array<int|string, string>
171: */
172: private function prefixPaths(array $paths, string $prefix): array
173: {
174: return array_map(static function (string $path) use ($prefix) : string {
175: return $prefix . $path;
176: }, $paths);
177: }
178: }
179: