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