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_key_exists;
36: use function array_map;
37: use function array_merge;
38: use function array_unique;
39: use function count;
40: use function dirname;
41: use function extension_loaded;
42: use function getenv;
43: use function ini_get;
44: use function is_array;
45: use function is_file;
46: use function is_readable;
47: use function spl_object_id;
48: use function sprintf;
49: use function str_ends_with;
50: use function substr;
51:
52: /**
53: * @api
54: */
55: final class ContainerFactory
56: {
57:
58: private FileHelper $fileHelper;
59:
60: private string $rootDirectory;
61:
62: private string $configDirectory;
63:
64: private static ?int $lastInitializedContainerId = null;
65:
66: private bool $journalContainer = false;
67:
68: /** @api */
69: public function __construct(private string $currentWorkingDirectory)
70: {
71: $this->fileHelper = new FileHelper($currentWorkingDirectory);
72:
73: $rootDir = __DIR__ . '/../..';
74: $originalRootDir = $this->fileHelper->normalizePath($rootDir);
75: if (extension_loaded('phar')) {
76: $pharPath = Phar::running(false);
77: if ($pharPath !== '') {
78: $rootDir = dirname($pharPath);
79: }
80: }
81: $this->rootDirectory = $this->fileHelper->normalizePath($rootDir);
82: $this->configDirectory = $originalRootDir . '/conf';
83: }
84:
85: public function setJournalContainer(): void
86: {
87: $this->journalContainer = true;
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: ), $this->journalContainer);
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: ReflectionProviderStaticAccessor::registerInstance($container->getByType(ReflectionProvider::class));
188: PhpVersionStaticAccessor::registerInstance($container->getByType(PhpVersion::class));
189: ObjectType::resetCaches();
190: $container->getService('typeSpecifier');
191:
192: BleedingEdgeToggle::setBleedingEdge($container->getParameter('featureToggles')['bleedingEdge']);
193: }
194:
195: public function getCurrentWorkingDirectory(): string
196: {
197: return $this->currentWorkingDirectory;
198: }
199:
200: public function getRootDirectory(): string
201: {
202: return $this->rootDirectory;
203: }
204:
205: public function getConfigDirectory(): string
206: {
207: return $this->configDirectory;
208: }
209:
210: /**
211: * @param string[] $configFiles
212: * @param array<string, mixed> $loaderParameters
213: * @return array{list<string>, array<mixed>}
214: * @throws DuplicateIncludedFilesException
215: */
216: private function detectDuplicateIncludedFiles(
217: array $configFiles,
218: array $loaderParameters,
219: ): array
220: {
221: $neonAdapter = new NeonAdapter();
222: $phpAdapter = new PhpAdapter();
223: $allConfigFiles = [];
224: $configArray = [];
225: foreach ($configFiles as $configFile) {
226: [$tmpConfigFiles, $tmpConfigArray] = self::getConfigFiles($this->fileHelper, $neonAdapter, $phpAdapter, $configFile, $loaderParameters, null);
227: $allConfigFiles = array_merge($allConfigFiles, $tmpConfigFiles);
228:
229: /** @var array<mixed> $configArray */
230: $configArray = \Nette\Schema\Helpers::merge($tmpConfigArray, $configArray);
231: }
232:
233: $normalized = array_map(fn (string $file): string => $this->fileHelper->normalizePath($file), $allConfigFiles);
234:
235: $deduplicated = array_unique($normalized);
236: if (count($normalized) <= count($deduplicated)) {
237: return [$normalized, $configArray];
238: }
239:
240: $duplicateFiles = array_unique(array_diff_key($normalized, $deduplicated));
241:
242: throw new DuplicateIncludedFilesException($duplicateFiles);
243: }
244:
245: /**
246: * @param array<string, string> $loaderParameters
247: * @return array{list<string>, array<mixed>}
248: */
249: private static function getConfigFiles(
250: FileHelper $fileHelper,
251: NeonAdapter $neonAdapter,
252: PhpAdapter $phpAdapter,
253: string $configFile,
254: array $loaderParameters,
255: ?string $generateBaselineFile,
256: ): array
257: {
258: if ($generateBaselineFile === $fileHelper->normalizePath($configFile)) {
259: return [[], []];
260: }
261: if (!is_file($configFile) || !is_readable($configFile)) {
262: return [[], []];
263: }
264:
265: if (str_ends_with($configFile, '.php')) {
266: $data = $phpAdapter->load($configFile);
267: } else {
268: $data = $neonAdapter->load($configFile);
269: }
270: $allConfigFiles = [$configFile];
271: if (isset($data['includes'])) {
272: Validators::assert($data['includes'], 'list', sprintf("section 'includes' in file '%s'", $configFile));
273: $includes = Helpers::expand($data['includes'], $loaderParameters);
274: foreach ($includes as $include) {
275: $include = self::expandIncludedFile($include, $configFile);
276: [$tmpConfigFiles, $tmpConfigArray] = self::getConfigFiles($fileHelper, $neonAdapter, $phpAdapter, $include, $loaderParameters, $generateBaselineFile);
277: $allConfigFiles = array_merge($allConfigFiles, $tmpConfigFiles);
278:
279: /** @var array<mixed> $data */
280: $data = \Nette\Schema\Helpers::merge($tmpConfigArray, $data);
281: }
282: }
283:
284: return [$allConfigFiles, $data];
285: }
286:
287: private static function expandIncludedFile(string $includedFile, string $mainFile): string
288: {
289: return Strings::match($includedFile, '#([a-z]+:)?[/\\\\]#Ai') !== null // is absolute
290: ? $includedFile
291: : dirname($mainFile) . '/' . $includedFile;
292: }
293:
294: /**
295: * @param array<mixed> $parameters
296: * @param array<mixed> $parametersSchema
297: */
298: private function validateParameters(array $parameters, array $parametersSchema): void
299: {
300: if (!(bool) $parameters['__validate']) {
301: return;
302: }
303:
304: $schema = $this->processArgument(
305: new Statement('schema', [
306: new Statement('structure', [$parametersSchema]),
307: ]),
308: );
309: $processor = new Processor();
310: $processor->onNewContext[] = static function (SchemaContext $context): void {
311: $context->path = ['parameters'];
312: };
313: $processor->process($schema, $parameters);
314:
315: if (
316: !array_key_exists('phpVersion', $parameters)
317: || !is_array($parameters['phpVersion'])) {
318: return;
319: }
320:
321: $phpVersion = $parameters['phpVersion'];
322:
323: if ($phpVersion['max'] < $phpVersion['min']) {
324: throw new InvalidPhpVersionException('Invalid PHP version range: phpVersion.max should be greater or equal to phpVersion.min.');
325: }
326: }
327:
328: /**
329: * @param Statement[] $statements
330: */
331: private function processSchema(array $statements, bool $required = true): Schema
332: {
333: if (count($statements) === 0) {
334: throw new ShouldNotHappenException();
335: }
336:
337: $parameterSchema = null;
338: foreach ($statements as $statement) {
339: $processedArguments = array_map(fn ($argument) => $this->processArgument($argument), $statement->arguments);
340: if ($parameterSchema === null) {
341: /** @var Type|AnyOf|Structure $parameterSchema */
342: $parameterSchema = Expect::{$statement->getEntity()}(...$processedArguments);
343: } else {
344: $parameterSchema->{$statement->getEntity()}(...$processedArguments);
345: }
346: }
347:
348: if ($required) {
349: $parameterSchema->required();
350: }
351:
352: return $parameterSchema;
353: }
354:
355: /**
356: * @param mixed $argument
357: * @return mixed
358: */
359: private function processArgument($argument, bool $required = true)
360: {
361: if ($argument instanceof Statement) {
362: if ($argument->entity === 'schema') {
363: $arguments = [];
364: foreach ($argument->arguments as $schemaArgument) {
365: if (!$schemaArgument instanceof Statement) {
366: throw new ShouldNotHappenException('schema() should contain another statement().');
367: }
368:
369: $arguments[] = $schemaArgument;
370: }
371:
372: if (count($arguments) === 0) {
373: throw new ShouldNotHappenException('schema() should have at least one argument.');
374: }
375:
376: return $this->processSchema($arguments, $required);
377: }
378:
379: return $this->processSchema([$argument], $required);
380: } elseif (is_array($argument)) {
381: $processedArray = [];
382: foreach ($argument as $key => $val) {
383: $required = $key[0] !== '?';
384: $key = $required ? $key : substr($key, 1);
385: $processedArray[$key] = $this->processArgument($val, $required);
386: }
387:
388: return $processedArray;
389: }
390:
391: return $argument;
392: }
393:
394: }
395: