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