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