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: /** @api */
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: /** @api */
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: * @param string[] $additionalConfigFiles
76: * @param string[] $analysedPaths
77: * @param string[] $composerAutoloaderProjectPaths
78: * @param string[] $analysedPathsFromConfig
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: /** @internal */
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: /** @var SourceLocator $sourceLocator */
155: $sourceLocator = $container->getService('betterReflectionSourceLocator');
156:
157: /** @var Reflector $reflector */
158: $reflector = $container->getService('betterReflectionReflector');
159:
160: /** @var Parser $phpParser */
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: /** @var Broker $broker */
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: * @param string[] $configFiles
236: * @param array<string, string> $loaderParameters
237: * @return string[]
238: * @throws DuplicateIncludedFilesException
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: * @param array<string, string> $loaderParameters
270: * @return string[]
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 // is absolute
309: ? $includedFile
310: : dirname($mainFile) . '/' . $includedFile;
311: }
312:
313: }
314: