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