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