1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\DependencyInjection;
4:
5: use Nette\Bootstrap\Extensions\PhpExtension;
6: use Nette\DI\Config\Adapters\PhpAdapter;
7: use Nette\DI\Definitions\Statement;
8: use Nette\DI\Extensions\ExtensionsExtension;
9: use Nette\DI\Helpers;
10: use Nette\Schema\Context as SchemaContext;
11: use Nette\Schema\Elements\AnyOf;
12: use Nette\Schema\Elements\Structure;
13: use Nette\Schema\Elements\Type;
14: use Nette\Schema\Expect;
15: use Nette\Schema\Processor;
16: use Nette\Schema\Schema;
17: use Nette\Utils\Strings;
18: use Nette\Utils\Validators;
19: use Phar;
20: use PhpParser\Parser;
21: use PHPStan\BetterReflection\BetterReflection;
22: use PHPStan\BetterReflection\Reflector\Reflector;
23: use PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber;
24: use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator;
25: use PHPStan\Broker\Broker;
26: use PHPStan\Command\CommandHelper;
27: use PHPStan\File\FileHelper;
28: use PHPStan\Node\Printer\Printer;
29: use PHPStan\Php\PhpVersion;
30: use PHPStan\Reflection\PhpVersionStaticAccessor;
31: use PHPStan\Reflection\ReflectionProvider;
32: use PHPStan\Reflection\ReflectionProviderStaticAccessor;
33: use PHPStan\ShouldNotHappenException;
34: use PHPStan\Type\Accessory\AccessoryArrayListType;
35: use PHPStan\Type\Generic\TemplateTypeVariance;
36: use PHPStan\Type\ObjectType;
37: use Symfony\Component\Finder\Finder;
38: use function array_diff_key;
39: use function array_map;
40: use function array_merge;
41: use function array_unique;
42: use function count;
43: use function dirname;
44: use function extension_loaded;
45: use function getenv;
46: use function ini_get;
47: use function is_array;
48: use function is_dir;
49: use function is_file;
50: use function is_readable;
51: use function spl_object_id;
52: use function sprintf;
53: use function str_ends_with;
54: use function substr;
55: use function time;
56: use function unlink;
57:
58: /**
59: * @api
60: * @final
61: */
62: class ContainerFactory
63: {
64:
65: private FileHelper $fileHelper;
66:
67: private string $rootDirectory;
68:
69: private string $configDirectory;
70:
71: private static ?int $lastInitializedContainerId = null;
72:
73: /** @api */
74: public function __construct(private string $currentWorkingDirectory, private bool $checkDuplicateFiles = false)
75: {
76: $this->fileHelper = new FileHelper($currentWorkingDirectory);
77:
78: $rootDir = __DIR__ . '/../..';
79: $originalRootDir = $this->fileHelper->normalizePath($rootDir);
80: if (extension_loaded('phar')) {
81: $pharPath = Phar::running(false);
82: if ($pharPath !== '') {
83: $rootDir = dirname($pharPath);
84: }
85: }
86: $this->rootDirectory = $this->fileHelper->normalizePath($rootDir);
87: $this->configDirectory = $originalRootDir . '/conf';
88: }
89:
90: /**
91: * @param string[] $additionalConfigFiles
92: * @param string[] $analysedPaths
93: * @param string[] $composerAutoloaderProjectPaths
94: * @param string[] $analysedPathsFromConfig
95: */
96: public function create(
97: string $tempDirectory,
98: array $additionalConfigFiles,
99: array $analysedPaths,
100: array $composerAutoloaderProjectPaths = [],
101: array $analysedPathsFromConfig = [],
102: string $usedLevel = CommandHelper::DEFAULT_LEVEL,
103: ?string $generateBaselineFile = null,
104: ?string $cliAutoloadFile = null,
105: ): Container
106: {
107: [$allConfigFiles, $projectConfig] = $this->detectDuplicateIncludedFiles(
108: array_merge([__DIR__ . '/../../conf/parametersSchema.neon'], $additionalConfigFiles),
109: [
110: 'rootDir' => $this->rootDirectory,
111: 'currentWorkingDirectory' => $this->currentWorkingDirectory,
112: 'env' => getenv(),
113: ],
114: );
115:
116: $configurator = new Configurator(new LoaderFactory(
117: $this->fileHelper,
118: $this->rootDirectory,
119: $this->currentWorkingDirectory,
120: $generateBaselineFile,
121: ));
122: $configurator->defaultExtensions = [
123: 'php' => PhpExtension::class,
124: 'extensions' => ExtensionsExtension::class,
125: ];
126: $configurator->setDebugMode(true);
127: $configurator->setTempDirectory($tempDirectory);
128: $configurator->addParameters([
129: 'rootDir' => $this->rootDirectory,
130: 'currentWorkingDirectory' => $this->currentWorkingDirectory,
131: 'cliArgumentsVariablesRegistered' => ini_get('register_argc_argv') === '1',
132: 'tmpDir' => $tempDirectory,
133: 'additionalConfigFiles' => $additionalConfigFiles,
134: 'allConfigFiles' => $allConfigFiles,
135: 'composerAutoloaderProjectPaths' => $composerAutoloaderProjectPaths,
136: 'generateBaselineFile' => $generateBaselineFile,
137: 'usedLevel' => $usedLevel,
138: 'cliAutoloadFile' => $cliAutoloadFile,
139: ]);
140: $configurator->addDynamicParameters([
141: 'analysedPaths' => $analysedPaths,
142: 'analysedPathsFromConfig' => $analysedPathsFromConfig,
143: 'env' => getenv(),
144: ]);
145: $configurator->addConfig($this->configDirectory . '/config.neon');
146: foreach ($additionalConfigFiles as $additionalConfigFile) {
147: $configurator->addConfig($additionalConfigFile);
148: }
149:
150: $configurator->setAllConfigFiles($allConfigFiles);
151:
152: $container = $configurator->createContainer()->getByType(Container::class);
153: $this->validateParameters($container->getParameters(), $projectConfig['parametersSchema']);
154: self::postInitializeContainer($container);
155:
156: return $container;
157: }
158:
159: /** @internal */
160: public static function postInitializeContainer(Container $container): void
161: {
162: $containerId = spl_object_id($container);
163: if ($containerId === self::$lastInitializedContainerId) {
164: return;
165: }
166:
167: self::$lastInitializedContainerId = $containerId;
168:
169: /** @var SourceLocator $sourceLocator */
170: $sourceLocator = $container->getService('betterReflectionSourceLocator');
171:
172: /** @var Reflector $reflector */
173: $reflector = $container->getService('betterReflectionReflector');
174:
175: /** @var Parser $phpParser */
176: $phpParser = $container->getService('phpParserDecorator');
177:
178: BetterReflection::populate(
179: $container->getByType(PhpVersion::class)->getVersionId(),
180: $sourceLocator,
181: $reflector,
182: $phpParser,
183: $container->getByType(PhpStormStubsSourceStubber::class),
184: $container->getByType(Printer::class),
185: );
186:
187: $broker = $container->getByType(Broker::class);
188: Broker::registerInstance($broker);
189: ReflectionProviderStaticAccessor::registerInstance($container->getByType(ReflectionProvider::class));
190: PhpVersionStaticAccessor::registerInstance($container->getByType(PhpVersion::class));
191: ObjectType::resetCaches();
192: $container->getService('typeSpecifier');
193:
194: BleedingEdgeToggle::setBleedingEdge($container->getParameter('featureToggles')['bleedingEdge']);
195: AccessoryArrayListType::setListTypeEnabled($container->getParameter('featureToggles')['listType']);
196: TemplateTypeVariance::setInvarianceCompositionEnabled($container->getParameter('featureToggles')['invarianceComposition']);
197: }
198:
199: public function clearOldContainers(string $tempDirectory): void
200: {
201: $configurator = new Configurator(new LoaderFactory(
202: $this->fileHelper,
203: $this->rootDirectory,
204: $this->currentWorkingDirectory,
205: null,
206: ));
207: $configurator->setDebugMode(true);
208: $configurator->setTempDirectory($tempDirectory);
209:
210: $containerDirectory = $configurator->getContainerCacheDirectory();
211: if (!is_dir($containerDirectory)) {
212: return;
213: }
214:
215: $finder = new Finder();
216: $finder->name('Container_*')->in($containerDirectory);
217: $twoDaysAgo = time() - 24 * 60 * 60 * 2;
218:
219: foreach ($finder as $containerFile) {
220: $path = $containerFile->getRealPath();
221: if ($path === false) {
222: continue;
223: }
224: if ($containerFile->getATime() > $twoDaysAgo) {
225: continue;
226: }
227: if ($containerFile->getCTime() > $twoDaysAgo) {
228: continue;
229: }
230:
231: @unlink($path);
232: }
233: }
234:
235: public function getCurrentWorkingDirectory(): string
236: {
237: return $this->currentWorkingDirectory;
238: }
239:
240: public function getRootDirectory(): string
241: {
242: return $this->rootDirectory;
243: }
244:
245: public function getConfigDirectory(): string
246: {
247: return $this->configDirectory;
248: }
249:
250: /**
251: * @param string[] $configFiles
252: * @param array<string, mixed> $loaderParameters
253: * @return array{list<string>, array<mixed>}
254: * @throws DuplicateIncludedFilesException
255: */
256: private function detectDuplicateIncludedFiles(
257: array $configFiles,
258: array $loaderParameters,
259: ): array
260: {
261: $neonAdapter = new NeonAdapter();
262: $phpAdapter = new PhpAdapter();
263: $allConfigFiles = [];
264: $configArray = [];
265: foreach ($configFiles as $configFile) {
266: [$tmpConfigFiles, $tmpConfigArray] = self::getConfigFiles($this->fileHelper, $neonAdapter, $phpAdapter, $configFile, $loaderParameters, null);
267: $allConfigFiles = array_merge($allConfigFiles, $tmpConfigFiles);
268:
269: /** @var array<mixed> $configArray */
270: $configArray = \Nette\Schema\Helpers::merge($tmpConfigArray, $configArray);
271: }
272:
273: $normalized = array_map(fn (string $file): string => $this->fileHelper->normalizePath($file), $allConfigFiles);
274:
275: $deduplicated = array_unique($normalized);
276: if (count($normalized) <= count($deduplicated)) {
277: return [$normalized, $configArray];
278: }
279:
280: if (!$this->checkDuplicateFiles) {
281: return [$normalized, $configArray];
282: }
283:
284: $duplicateFiles = array_unique(array_diff_key($normalized, $deduplicated));
285:
286: throw new DuplicateIncludedFilesException($duplicateFiles);
287: }
288:
289: /**
290: * @param array<string, string> $loaderParameters
291: * @return array{list<string>, array<mixed>}
292: */
293: private static function getConfigFiles(
294: FileHelper $fileHelper,
295: NeonAdapter $neonAdapter,
296: PhpAdapter $phpAdapter,
297: string $configFile,
298: array $loaderParameters,
299: ?string $generateBaselineFile,
300: ): array
301: {
302: if ($generateBaselineFile === $fileHelper->normalizePath($configFile)) {
303: return [[], []];
304: }
305: if (!is_file($configFile) || !is_readable($configFile)) {
306: return [[], []];
307: }
308:
309: if (str_ends_with($configFile, '.php')) {
310: $data = $phpAdapter->load($configFile);
311: } else {
312: $data = $neonAdapter->load($configFile);
313: }
314: $allConfigFiles = [$configFile];
315: if (isset($data['includes'])) {
316: Validators::assert($data['includes'], 'list', sprintf("section 'includes' in file '%s'", $configFile));
317: $includes = Helpers::expand($data['includes'], $loaderParameters);
318: foreach ($includes as $include) {
319: $include = self::expandIncludedFile($include, $configFile);
320: [$tmpConfigFiles, $tmpConfigArray] = self::getConfigFiles($fileHelper, $neonAdapter, $phpAdapter, $include, $loaderParameters, $generateBaselineFile);
321: $allConfigFiles = array_merge($allConfigFiles, $tmpConfigFiles);
322:
323: /** @var array<mixed> $data */
324: $data = \Nette\Schema\Helpers::merge($tmpConfigArray, $data);
325: }
326: }
327:
328: return [$allConfigFiles, $data];
329: }
330:
331: private static function expandIncludedFile(string $includedFile, string $mainFile): string
332: {
333: return Strings::match($includedFile, '#([a-z]+:)?[/\\\\]#Ai') !== null // is absolute
334: ? $includedFile
335: : dirname($mainFile) . '/' . $includedFile;
336: }
337:
338: /**
339: * @param array<mixed> $parameters
340: * @param array<mixed> $parametersSchema
341: */
342: private function validateParameters(array $parameters, array $parametersSchema): void
343: {
344: if (!(bool) $parameters['__validate']) {
345: return;
346: }
347:
348: $schema = $this->processArgument(
349: new Statement('schema', [
350: new Statement('structure', [$parametersSchema]),
351: ]),
352: );
353: $processor = new Processor();
354: $processor->onNewContext[] = static function (SchemaContext $context): void {
355: $context->path = ['parameters'];
356: };
357: $processor->process($schema, $parameters);
358: }
359:
360: /**
361: * @param Statement[] $statements
362: */
363: private function processSchema(array $statements, bool $required = true): Schema
364: {
365: if (count($statements) === 0) {
366: throw new ShouldNotHappenException();
367: }
368:
369: $parameterSchema = null;
370: foreach ($statements as $statement) {
371: $processedArguments = array_map(fn ($argument) => $this->processArgument($argument), $statement->arguments);
372: if ($parameterSchema === null) {
373: /** @var Type|AnyOf|Structure $parameterSchema */
374: $parameterSchema = Expect::{$statement->getEntity()}(...$processedArguments);
375: } else {
376: $parameterSchema->{$statement->getEntity()}(...$processedArguments);
377: }
378: }
379:
380: if ($required) {
381: $parameterSchema->required();
382: }
383:
384: return $parameterSchema;
385: }
386:
387: /**
388: * @param mixed $argument
389: * @return mixed
390: */
391: private function processArgument($argument, bool $required = true)
392: {
393: if ($argument instanceof Statement) {
394: if ($argument->entity === 'schema') {
395: $arguments = [];
396: foreach ($argument->arguments as $schemaArgument) {
397: if (!$schemaArgument instanceof Statement) {
398: throw new ShouldNotHappenException('schema() should contain another statement().');
399: }
400:
401: $arguments[] = $schemaArgument;
402: }
403:
404: if (count($arguments) === 0) {
405: throw new ShouldNotHappenException('schema() should have at least one argument.');
406: }
407:
408: return $this->processSchema($arguments, $required);
409: }
410:
411: return $this->processSchema([$argument], $required);
412: } elseif (is_array($argument)) {
413: $processedArray = [];
414: foreach ($argument as $key => $val) {
415: $required = $key[0] !== '?';
416: $key = $required ? $key : substr($key, 1);
417: $processedArray[$key] = $this->processArgument($val, $required);
418: }
419:
420: return $processedArray;
421: }
422:
423: return $argument;
424: }
425:
426: }
427: