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\Psr\Psr0Mapping;
12: use PHPStan\BetterReflection\SourceLocator\Type\Composer\Psr\Psr4Mapping;
13: use PHPStan\BetterReflection\SourceLocator\Type\Composer\PsrAutoloaderLocator;
14: use PHPStan\BetterReflection\SourceLocator\Type\DirectoriesSourceLocator;
15: use PHPStan\BetterReflection\SourceLocator\Type\SingleFileSourceLocator;
16: use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator;
17:
18: use function array_filter;
19: use function array_map;
20: use function array_merge;
21: use function array_values;
22: use function assert;
23: use function file_get_contents;
24: use function is_dir;
25: use function is_file;
26: use function is_string;
27: use function json_decode;
28: use function realpath;
29:
30: use const JSON_THROW_ON_ERROR;
31:
32: /**
33: * @psalm-type ComposerAutoload array{
34: * psr-0?: array<string, string|list<string>>,
35: * psr-4?: array<string, string|list<string>>,
36: * classmap?: list<string>,
37: * files?: list<string>,
38: * exclude-from-classmap?: list<string>
39: * }
40: * @psalm-type ComposerPackage array{
41: * name: string,
42: * autoload: ComposerAutoload
43: * }
44: * @psalm-type Composer array{
45: * autoload: ComposerAutoload,
46: * config?: array{vendor-dir?: string}
47: * }
48: */
49: final class MakeLocatorForComposerJson
50: {
51: public function __invoke(string $installationPath, Locator $astLocator): SourceLocator
52: {
53: $realInstallationPath = (string) realpath($installationPath);
54:
55: if (! is_dir($realInstallationPath)) {
56: throw InvalidProjectDirectory::atPath($installationPath);
57: }
58:
59: $composerJsonPath = $realInstallationPath . '/composer.json';
60:
61: if (! is_file($composerJsonPath)) {
62: throw MissingComposerJson::inProjectPath($installationPath);
63: }
64:
65: $composerJsonContent = file_get_contents($composerJsonPath);
66: assert(is_string($composerJsonContent));
67:
68: /** @psalm-var array{autoload: ComposerAutoload} $composer */
69: $composer = json_decode($composerJsonContent, true, 512, JSON_THROW_ON_ERROR);
70:
71: $pathPrefix = $realInstallationPath . '/';
72: $classMapPaths = $this->prefixPaths($this->packageToClassMapPaths($composer), $pathPrefix);
73: $classMapFiles = array_filter($classMapPaths, 'is_file');
74: $classMapDirectories = array_values(array_filter($classMapPaths, 'is_dir'));
75: $filePaths = $this->prefixPaths($this->packageToFilePaths($composer), $pathPrefix);
76:
77: return new AggregateSourceLocator(array_merge([
78: new PsrAutoloaderLocator(Psr4Mapping::fromArrayMappings($this->prefixWithInstallationPath($this->packageToPsr4AutoloadNamespaces($composer), $pathPrefix)), $astLocator),
79: new PsrAutoloaderLocator(Psr0Mapping::fromArrayMappings($this->prefixWithInstallationPath($this->packageToPsr0AutoloadNamespaces($composer), $pathPrefix)), $astLocator),
80: new DirectoriesSourceLocator($classMapDirectories, $astLocator),
81: ], ...array_map(static function (string $file) use ($astLocator): array {
82: assert($file !== '');
83:
84: return [new SingleFileSourceLocator($file, $astLocator)];
85: }, array_merge($classMapFiles, $filePaths))));
86: }
87:
88: /**
89: * @param array{autoload: ComposerAutoload} $package
90: *
91: * @return array<string, list<string>>
92: */
93: private function packageToPsr4AutoloadNamespaces(array $package): array
94: {
95: return array_map(static function ($namespacePaths) : array {
96: return (array) $namespacePaths;
97: }, $package['autoload']['psr-4'] ?? []);
98: }
99:
100: /**
101: * @param array{autoload: ComposerAutoload} $package
102: *
103: * @return array<string, list<string>>
104: */
105: private function packageToPsr0AutoloadNamespaces(array $package): array
106: {
107: return array_map(static function ($namespacePaths) : array {
108: return (array) $namespacePaths;
109: }, $package['autoload']['psr-0'] ?? []);
110: }
111:
112: /**
113: * @param array{autoload: ComposerAutoload} $package
114: *
115: * @return list<string>
116: */
117: private function packageToClassMapPaths(array $package): array
118: {
119: return $package['autoload']['classmap'] ?? [];
120: }
121:
122: /**
123: * @param array{autoload: ComposerAutoload} $package
124: *
125: * @return list<string>
126: */
127: private function packageToFilePaths(array $package): array
128: {
129: return $package['autoload']['files'] ?? [];
130: }
131:
132: /**
133: * @param array<string, list<string>> $paths
134: *
135: * @return array<string, list<string>>
136: */
137: private function prefixWithInstallationPath(array $paths, string $trimmedInstallationPath): array
138: {
139: return array_map(function (array $paths) use ($trimmedInstallationPath) : array {
140: return $this->prefixPaths($paths, $trimmedInstallationPath);
141: }, $paths);
142: }
143:
144: /**
145: * @param list<string> $paths
146: *
147: * @return list<string>
148: */
149: private function prefixPaths(array $paths, string $prefix): array
150: {
151: return array_map(static function (string $path) use ($prefix) : string {
152: return $prefix . $path;
153: }, $paths);
154: }
155: }
156: