1: | <?php declare(strict_types = 1); |
2: | |
3: | namespace PHPStan\DependencyInjection; |
4: | |
5: | use Nette\DI\Config\Adapters\PhpAdapter; |
6: | use Nette\DI\Extensions\ExtensionsExtension; |
7: | use Nette\DI\Extensions\PhpExtension; |
8: | use Nette\DI\Helpers; |
9: | use Nette\Utils\Strings; |
10: | use Nette\Utils\Validators; |
11: | use Phar; |
12: | use PhpParser\Parser; |
13: | use PHPStan\BetterReflection\BetterReflection; |
14: | use PHPStan\BetterReflection\Reflector\Reflector; |
15: | use PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber; |
16: | use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator; |
17: | use PHPStan\Broker\Broker; |
18: | use PHPStan\Command\CommandHelper; |
19: | use PHPStan\File\FileHelper; |
20: | use PHPStan\Php\PhpVersion; |
21: | use PHPStan\Reflection\ReflectionProvider; |
22: | use PHPStan\Reflection\ReflectionProviderStaticAccessor; |
23: | use PHPStan\Type\Accessory\AccessoryArrayListType; |
24: | use PHPStan\Type\Generic\TemplateTypeVariance; |
25: | use PHPStan\Type\ObjectType; |
26: | use Symfony\Component\Finder\Finder; |
27: | use function array_diff_key; |
28: | use function array_map; |
29: | use function array_merge; |
30: | use function array_unique; |
31: | use function count; |
32: | use function dirname; |
33: | use function extension_loaded; |
34: | use function ini_get; |
35: | use function is_dir; |
36: | use function is_file; |
37: | use function is_readable; |
38: | use function spl_object_hash; |
39: | use function sprintf; |
40: | use function str_ends_with; |
41: | use function sys_get_temp_dir; |
42: | use function time; |
43: | use function unlink; |
44: | |
45: | |
46: | class ContainerFactory |
47: | { |
48: | |
49: | private FileHelper $fileHelper; |
50: | |
51: | private string $rootDirectory; |
52: | |
53: | private string $configDirectory; |
54: | |
55: | private static ?string $lastInitializedContainerId = null; |
56: | |
57: | |
58: | public function __construct(private string $currentWorkingDirectory, private bool $checkDuplicateFiles = false) |
59: | { |
60: | $this->fileHelper = new FileHelper($currentWorkingDirectory); |
61: | |
62: | $rootDir = __DIR__ . '/../..'; |
63: | $originalRootDir = $this->fileHelper->normalizePath($rootDir); |
64: | if (extension_loaded('phar')) { |
65: | $pharPath = Phar::running(false); |
66: | if ($pharPath !== '') { |
67: | $rootDir = dirname($pharPath); |
68: | } |
69: | } |
70: | $this->rootDirectory = $this->fileHelper->normalizePath($rootDir); |
71: | $this->configDirectory = $originalRootDir . '/conf'; |
72: | } |
73: | |
74: | |
75: | |
76: | |
77: | |
78: | |
79: | |
80: | public function create( |
81: | string $tempDirectory, |
82: | array $additionalConfigFiles, |
83: | array $analysedPaths, |
84: | array $composerAutoloaderProjectPaths = [], |
85: | array $analysedPathsFromConfig = [], |
86: | string $usedLevel = CommandHelper::DEFAULT_LEVEL, |
87: | ?string $generateBaselineFile = null, |
88: | ?string $cliAutoloadFile = null, |
89: | ?string $singleReflectionFile = null, |
90: | ?string $singleReflectionInsteadOfFile = null, |
91: | ): Container |
92: | { |
93: | $allConfigFiles = $this->detectDuplicateIncludedFiles( |
94: | $additionalConfigFiles, |
95: | [ |
96: | 'rootDir' => $this->rootDirectory, |
97: | 'currentWorkingDirectory' => $this->currentWorkingDirectory, |
98: | ], |
99: | ); |
100: | |
101: | $configurator = new Configurator(new LoaderFactory( |
102: | $this->fileHelper, |
103: | $this->rootDirectory, |
104: | $this->currentWorkingDirectory, |
105: | $generateBaselineFile, |
106: | )); |
107: | $configurator->defaultExtensions = [ |
108: | 'php' => PhpExtension::class, |
109: | 'extensions' => ExtensionsExtension::class, |
110: | ]; |
111: | $configurator->setDebugMode(true); |
112: | $configurator->setTempDirectory($tempDirectory); |
113: | $configurator->addParameters([ |
114: | 'rootDir' => $this->rootDirectory, |
115: | 'currentWorkingDirectory' => $this->currentWorkingDirectory, |
116: | 'cliArgumentsVariablesRegistered' => ini_get('register_argc_argv') === '1', |
117: | 'tmpDir' => $tempDirectory, |
118: | 'additionalConfigFiles' => $additionalConfigFiles, |
119: | 'composerAutoloaderProjectPaths' => $composerAutoloaderProjectPaths, |
120: | 'generateBaselineFile' => $generateBaselineFile, |
121: | 'usedLevel' => $usedLevel, |
122: | 'cliAutoloadFile' => $cliAutoloadFile, |
123: | 'fixerTmpDir' => sys_get_temp_dir() . '/phpstan-fixer', |
124: | ]); |
125: | $configurator->addDynamicParameters([ |
126: | 'singleReflectionFile' => $singleReflectionFile, |
127: | 'singleReflectionInsteadOfFile' => $singleReflectionInsteadOfFile, |
128: | 'analysedPaths' => $analysedPaths, |
129: | 'analysedPathsFromConfig' => $analysedPathsFromConfig, |
130: | ]); |
131: | $configurator->addConfig($this->configDirectory . '/config.neon'); |
132: | foreach ($additionalConfigFiles as $additionalConfigFile) { |
133: | $configurator->addConfig($additionalConfigFile); |
134: | } |
135: | |
136: | $configurator->setAllConfigFiles($allConfigFiles); |
137: | |
138: | $container = $configurator->createContainer()->getByType(Container::class); |
139: | self::postInitializeContainer($container); |
140: | |
141: | return $container; |
142: | } |
143: | |
144: | |
145: | public static function postInitializeContainer(Container $container): void |
146: | { |
147: | $containerId = spl_object_hash($container); |
148: | if ($containerId === self::$lastInitializedContainerId) { |
149: | return; |
150: | } |
151: | |
152: | self::$lastInitializedContainerId = $containerId; |
153: | |
154: | |
155: | $sourceLocator = $container->getService('betterReflectionSourceLocator'); |
156: | |
157: | |
158: | $reflector = $container->getService('betterReflectionReflector'); |
159: | |
160: | |
161: | $phpParser = $container->getService('phpParserDecorator'); |
162: | |
163: | BetterReflection::populate( |
164: | $container->getByType(PhpVersion::class)->getVersionId(), |
165: | $sourceLocator, |
166: | $reflector, |
167: | $phpParser, |
168: | $container->getByType(PhpStormStubsSourceStubber::class), |
169: | ); |
170: | |
171: | |
172: | $broker = $container->getByType(Broker::class); |
173: | Broker::registerInstance($broker); |
174: | ReflectionProviderStaticAccessor::registerInstance($container->getByType(ReflectionProvider::class)); |
175: | ObjectType::resetCaches(); |
176: | $container->getService('typeSpecifier'); |
177: | |
178: | BleedingEdgeToggle::setBleedingEdge($container->getParameter('featureToggles')['bleedingEdge']); |
179: | AccessoryArrayListType::setListTypeEnabled($container->getParameter('featureToggles')['listType']); |
180: | TemplateTypeVariance::setInvarianceCompositionEnabled($container->getParameter('featureToggles')['invarianceComposition']); |
181: | } |
182: | |
183: | public function clearOldContainers(string $tempDirectory): void |
184: | { |
185: | $configurator = new Configurator(new LoaderFactory( |
186: | $this->fileHelper, |
187: | $this->rootDirectory, |
188: | $this->currentWorkingDirectory, |
189: | null, |
190: | )); |
191: | $configurator->setDebugMode(true); |
192: | $configurator->setTempDirectory($tempDirectory); |
193: | |
194: | $containerDirectory = $configurator->getContainerCacheDirectory(); |
195: | if (!is_dir($containerDirectory)) { |
196: | return; |
197: | } |
198: | |
199: | $finder = new Finder(); |
200: | $finder->name('Container_*')->in($containerDirectory); |
201: | $twoDaysAgo = time() - 24 * 60 * 60 * 2; |
202: | |
203: | foreach ($finder as $containerFile) { |
204: | $path = $containerFile->getRealPath(); |
205: | if ($path === false) { |
206: | continue; |
207: | } |
208: | if ($containerFile->getATime() > $twoDaysAgo) { |
209: | continue; |
210: | } |
211: | if ($containerFile->getCTime() > $twoDaysAgo) { |
212: | continue; |
213: | } |
214: | |
215: | @unlink($path); |
216: | } |
217: | } |
218: | |
219: | public function getCurrentWorkingDirectory(): string |
220: | { |
221: | return $this->currentWorkingDirectory; |
222: | } |
223: | |
224: | public function getRootDirectory(): string |
225: | { |
226: | return $this->rootDirectory; |
227: | } |
228: | |
229: | public function getConfigDirectory(): string |
230: | { |
231: | return $this->configDirectory; |
232: | } |
233: | |
234: | |
235: | |
236: | |
237: | |
238: | |
239: | |
240: | private function detectDuplicateIncludedFiles( |
241: | array $configFiles, |
242: | array $loaderParameters, |
243: | ): array |
244: | { |
245: | $neonAdapter = new NeonAdapter(); |
246: | $phpAdapter = new PhpAdapter(); |
247: | $allConfigFiles = []; |
248: | foreach ($configFiles as $configFile) { |
249: | $allConfigFiles = array_merge($allConfigFiles, self::getConfigFiles($this->fileHelper, $neonAdapter, $phpAdapter, $configFile, $loaderParameters, null)); |
250: | } |
251: | |
252: | $normalized = array_map(fn (string $file): string => $this->fileHelper->normalizePath($file), $allConfigFiles); |
253: | |
254: | $deduplicated = array_unique($normalized); |
255: | if (count($normalized) <= count($deduplicated)) { |
256: | return $normalized; |
257: | } |
258: | |
259: | if (!$this->checkDuplicateFiles) { |
260: | return $normalized; |
261: | } |
262: | |
263: | $duplicateFiles = array_unique(array_diff_key($normalized, $deduplicated)); |
264: | |
265: | throw new DuplicateIncludedFilesException($duplicateFiles); |
266: | } |
267: | |
268: | |
269: | |
270: | |
271: | |
272: | private static function getConfigFiles( |
273: | FileHelper $fileHelper, |
274: | NeonAdapter $neonAdapter, |
275: | PhpAdapter $phpAdapter, |
276: | string $configFile, |
277: | array $loaderParameters, |
278: | ?string $generateBaselineFile, |
279: | ): array |
280: | { |
281: | if ($generateBaselineFile === $fileHelper->normalizePath($configFile)) { |
282: | return []; |
283: | } |
284: | if (!is_file($configFile) || !is_readable($configFile)) { |
285: | return []; |
286: | } |
287: | |
288: | if (str_ends_with($configFile, '.php')) { |
289: | $data = $phpAdapter->load($configFile); |
290: | } else { |
291: | $data = $neonAdapter->load($configFile); |
292: | } |
293: | $allConfigFiles = [$configFile]; |
294: | if (isset($data['includes'])) { |
295: | Validators::assert($data['includes'], 'list', sprintf("section 'includes' in file '%s'", $configFile)); |
296: | $includes = Helpers::expand($data['includes'], $loaderParameters); |
297: | foreach ($includes as $include) { |
298: | $include = self::expandIncludedFile($include, $configFile); |
299: | $allConfigFiles = array_merge($allConfigFiles, self::getConfigFiles($fileHelper, $neonAdapter, $phpAdapter, $include, $loaderParameters, $generateBaselineFile)); |
300: | } |
301: | } |
302: | |
303: | return $allConfigFiles; |
304: | } |
305: | |
306: | private static function expandIncludedFile(string $includedFile, string $mainFile): string |
307: | { |
308: | return Strings::match($includedFile, '#([a-z]+:)?[/\\\\]#Ai') !== null |
309: | ? $includedFile |
310: | : dirname($mainFile) . '/' . $includedFile; |
311: | } |
312: | |
313: | } |
314: | |