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: ): Container
90: {
91: $allConfigFiles = $this->detectDuplicateIncludedFiles(
92: $additionalConfigFiles,
93: [
94: 'rootDir' => $this->rootDirectory,
95: 'currentWorkingDirectory' => $this->currentWorkingDirectory,
96: ],
97: );
98:
99: $configurator = new Configurator(new LoaderFactory(
100: $this->fileHelper,
101: $this->rootDirectory,
102: $this->currentWorkingDirectory,
103: $generateBaselineFile,
104: ));
105: $configurator->defaultExtensions = [
106: 'php' => PhpExtension::class,
107: 'extensions' => ExtensionsExtension::class,
108: ];
109: $configurator->setDebugMode(true);
110: $configurator->setTempDirectory($tempDirectory);
111: $configurator->addParameters([
112: 'rootDir' => $this->rootDirectory,
113: 'currentWorkingDirectory' => $this->currentWorkingDirectory,
114: 'cliArgumentsVariablesRegistered' => ini_get('register_argc_argv') === '1',
115: 'tmpDir' => $tempDirectory,
116: 'additionalConfigFiles' => $additionalConfigFiles,
117: 'allConfigFiles' => $allConfigFiles,
118: 'composerAutoloaderProjectPaths' => $composerAutoloaderProjectPaths,
119: 'generateBaselineFile' => $generateBaselineFile,
120: 'usedLevel' => $usedLevel,
121: 'cliAutoloadFile' => $cliAutoloadFile,
122: 'fixerTmpDir' => sys_get_temp_dir() . '/phpstan-fixer',
123: ]);
124: $configurator->addDynamicParameters([
125: 'analysedPaths' => $analysedPaths,
126: 'analysedPathsFromConfig' => $analysedPathsFromConfig,
127: ]);
128: $configurator->addConfig($this->configDirectory . '/config.neon');
129: foreach ($additionalConfigFiles as $additionalConfigFile) {
130: $configurator->addConfig($additionalConfigFile);
131: }
132:
133: $configurator->setAllConfigFiles($allConfigFiles);
134:
135: $container = $configurator->createContainer()->getByType(Container::class);
136: self::postInitializeContainer($container);
137:
138: return $container;
139: }
140:
141: /** @internal */
142: public static function postInitializeContainer(Container $container): void
143: {
144: $containerId = spl_object_hash($container);
145: if ($containerId === self::$lastInitializedContainerId) {
146: return;
147: }
148:
149: self::$lastInitializedContainerId = $containerId;
150:
151: /** @var SourceLocator $sourceLocator */
152: $sourceLocator = $container->getService('betterReflectionSourceLocator');
153:
154: /** @var Reflector $reflector */
155: $reflector = $container->getService('betterReflectionReflector');
156:
157: /** @var Parser $phpParser */
158: $phpParser = $container->getService('phpParserDecorator');
159:
160: BetterReflection::populate(
161: $container->getByType(PhpVersion::class)->getVersionId(),
162: $sourceLocator,
163: $reflector,
164: $phpParser,
165: $container->getByType(PhpStormStubsSourceStubber::class),
166: );
167:
168: $broker = $container->getByType(Broker::class);
169: Broker::registerInstance($broker);
170: ReflectionProviderStaticAccessor::registerInstance($container->getByType(ReflectionProvider::class));
171: ObjectType::resetCaches();
172: $container->getService('typeSpecifier');
173:
174: BleedingEdgeToggle::setBleedingEdge($container->getParameter('featureToggles')['bleedingEdge']);
175: AccessoryArrayListType::setListTypeEnabled($container->getParameter('featureToggles')['listType']);
176: TemplateTypeVariance::setInvarianceCompositionEnabled($container->getParameter('featureToggles')['invarianceComposition']);
177: }
178:
179: public function clearOldContainers(string $tempDirectory): void
180: {
181: $configurator = new Configurator(new LoaderFactory(
182: $this->fileHelper,
183: $this->rootDirectory,
184: $this->currentWorkingDirectory,
185: null,
186: ));
187: $configurator->setDebugMode(true);
188: $configurator->setTempDirectory($tempDirectory);
189:
190: $containerDirectory = $configurator->getContainerCacheDirectory();
191: if (!is_dir($containerDirectory)) {
192: return;
193: }
194:
195: $finder = new Finder();
196: $finder->name('Container_*')->in($containerDirectory);
197: $twoDaysAgo = time() - 24 * 60 * 60 * 2;
198:
199: foreach ($finder as $containerFile) {
200: $path = $containerFile->getRealPath();
201: if ($path === false) {
202: continue;
203: }
204: if ($containerFile->getATime() > $twoDaysAgo) {
205: continue;
206: }
207: if ($containerFile->getCTime() > $twoDaysAgo) {
208: continue;
209: }
210:
211: @unlink($path);
212: }
213: }
214:
215: public function getCurrentWorkingDirectory(): string
216: {
217: return $this->currentWorkingDirectory;
218: }
219:
220: public function getRootDirectory(): string
221: {
222: return $this->rootDirectory;
223: }
224:
225: public function getConfigDirectory(): string
226: {
227: return $this->configDirectory;
228: }
229:
230: /**
231: * @param string[] $configFiles
232: * @param array<string, string> $loaderParameters
233: * @return string[]
234: * @throws DuplicateIncludedFilesException
235: */
236: private function detectDuplicateIncludedFiles(
237: array $configFiles,
238: array $loaderParameters,
239: ): array
240: {
241: $neonAdapter = new NeonAdapter();
242: $phpAdapter = new PhpAdapter();
243: $allConfigFiles = [];
244: foreach ($configFiles as $configFile) {
245: $allConfigFiles = array_merge($allConfigFiles, self::getConfigFiles($this->fileHelper, $neonAdapter, $phpAdapter, $configFile, $loaderParameters, null));
246: }
247:
248: $normalized = array_map(fn (string $file): string => $this->fileHelper->normalizePath($file), $allConfigFiles);
249:
250: $deduplicated = array_unique($normalized);
251: if (count($normalized) <= count($deduplicated)) {
252: return $normalized;
253: }
254:
255: if (!$this->checkDuplicateFiles) {
256: return $normalized;
257: }
258:
259: $duplicateFiles = array_unique(array_diff_key($normalized, $deduplicated));
260:
261: throw new DuplicateIncludedFilesException($duplicateFiles);
262: }
263:
264: /**
265: * @param array<string, string> $loaderParameters
266: * @return string[]
267: */
268: private static function getConfigFiles(
269: FileHelper $fileHelper,
270: NeonAdapter $neonAdapter,
271: PhpAdapter $phpAdapter,
272: string $configFile,
273: array $loaderParameters,
274: ?string $generateBaselineFile,
275: ): array
276: {
277: if ($generateBaselineFile === $fileHelper->normalizePath($configFile)) {
278: return [];
279: }
280: if (!is_file($configFile) || !is_readable($configFile)) {
281: return [];
282: }
283:
284: if (str_ends_with($configFile, '.php')) {
285: $data = $phpAdapter->load($configFile);
286: } else {
287: $data = $neonAdapter->load($configFile);
288: }
289: $allConfigFiles = [$configFile];
290: if (isset($data['includes'])) {
291: Validators::assert($data['includes'], 'list', sprintf("section 'includes' in file '%s'", $configFile));
292: $includes = Helpers::expand($data['includes'], $loaderParameters);
293: foreach ($includes as $include) {
294: $include = self::expandIncludedFile($include, $configFile);
295: $allConfigFiles = array_merge($allConfigFiles, self::getConfigFiles($fileHelper, $neonAdapter, $phpAdapter, $include, $loaderParameters, $generateBaselineFile));
296: }
297: }
298:
299: return $allConfigFiles;
300: }
301:
302: private static function expandIncludedFile(string $includedFile, string $mainFile): string
303: {
304: return Strings::match($includedFile, '#([a-z]+:)?[/\\\\]#Ai') !== null // is absolute
305: ? $includedFile
306: : dirname($mainFile) . '/' . $includedFile;
307: }
308:
309: }
310: