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