1: <?php declare(strict_types = 1);
2:
3: namespace ApiGen;
4:
5: use Composer\InstalledVersions;
6: use ErrorException;
7: use Nette\DI\Compiler;
8: use Nette\DI\Config\Loader;
9: use Nette\DI\Container;
10: use Nette\DI\ContainerLoader;
11: use Nette\DI\Extensions\ExtensionsExtension;
12: use Nette\DI\Helpers as DIHelpers;
13: use Nette\Schema\Expect;
14: use Nette\Schema\Helpers as SchemaHelpers;
15: use Nette\Schema\Processor;
16: use Nette\Utils\FileSystem;
17: use Symfony\Component\Console\Style\OutputStyle;
18:
19: use function array_keys;
20: use function array_map;
21: use function assert;
22: use function count;
23: use function dirname;
24: use function error_reporting;
25: use function getcwd;
26: use function ini_set;
27: use function is_array;
28: use function is_file;
29: use function is_int;
30: use function method_exists;
31: use function set_error_handler;
32: use function str_starts_with;
33: use function sys_get_temp_dir;
34:
35: use const E_ALL;
36: use const E_DEPRECATED;
37: use const E_USER_DEPRECATED;
38: use const PHP_RELEASE_VERSION;
39: use const PHP_VERSION_ID;
40:
41:
42: class Bootstrap
43: {
44: public static function configureErrorHandling(): void
45: {
46: error_reporting(E_ALL);
47: ini_set('display_errors', 'stderr');
48: ini_set('log_errors', '0');
49:
50: set_error_handler(function (int $severity, string $message, string $file, int $line): bool {
51: if (error_reporting() & $severity && $severity !== E_DEPRECATED && $severity !== E_USER_DEPRECATED) {
52: throw new ErrorException($message, 0, $severity, $file, $line);
53:
54: } else {
55: return false;
56: }
57: });
58: }
59:
60:
61: /**
62: * @param mixed[] $parameters indexed by [parameterName]
63: * @param string[] $configPaths indexed by []
64: */
65: public static function createApiGen(OutputStyle $output, array $parameters, array $configPaths): ApiGen
66: {
67: $workingDir = getcwd();
68: $tempDir = sys_get_temp_dir() . '/apigen';
69: $version = InstalledVersions::getPrettyVersion('apigen/apigen');
70:
71: if ($workingDir === false) {
72: throw new \RuntimeException('Unable to get current working directory.');
73: }
74:
75: $autoDiscoveryPath = "$workingDir/apigen.neon";
76: if (count($configPaths) === 0 && is_file($autoDiscoveryPath)) {
77: $output->text("Using configuration file $autoDiscoveryPath.\n");
78: $configPaths[] = $autoDiscoveryPath;
79: }
80:
81: $config = self::mergeConfigs(
82: ['parameters' => ['workingDir' => $workingDir, 'tempDir' => $tempDir, 'version' => $version]],
83: self::loadConfig(__DIR__ . '/../apigen.neon'),
84: ...array_map(self::loadConfig(...), $configPaths),
85: ...[['parameters' => self::resolvePaths($parameters, $workingDir)]],
86: );
87:
88: $parameters = $config['parameters'];
89: unset($config['parameters']);
90:
91: self::validateParameters($parameters);
92: $parameters = DIHelpers::expand($parameters, $parameters);
93: $containerLoader = new ContainerLoader($parameters['tempDir'], autoRebuild: true);
94:
95: $containerGenerator = function (Compiler $compiler) use ($config, $parameters): void {
96: $compiler->addExtension('extensions', new ExtensionsExtension);
97: $compiler->addConfig($config);
98: $compiler->setDynamicParameterNames(array_keys($parameters));
99: };
100:
101: $containerKey = [
102: $config,
103: PHP_VERSION_ID - PHP_RELEASE_VERSION,
104: ];
105:
106: /** @var class-string<Container> $containerClassName */
107: $containerClassName = $containerLoader->load($containerGenerator, $containerKey);
108:
109: $container = new $containerClassName($parameters);
110: $container->addService('symfonyConsole.output', $output);
111: $container->initialize();
112: ini_set('memory_limit', $container->getParameter('memoryLimit'));
113:
114: return $container->getByType(ApiGen::class);
115: }
116:
117:
118: /**
119: * @param mixed[] $parameters indexed by [parameterName]
120: */
121: protected static function validateParameters(array $parameters): void
122: {
123: $schema = Expect::structure([
124: // input
125: 'paths' => Expect::listOf('string')->min(1),
126: 'include' => Expect::listOf('string'),
127: 'exclude' => Expect::listOf('string'),
128:
129: // analysis
130: 'excludeProtected' => Expect::bool(),
131: 'excludePrivate' => Expect::bool(),
132: 'excludeTagged' => Expect::listOf('string'),
133:
134: // output
135: 'outputDir' => Expect::string(),
136: 'themeDir' => Expect::string()->nullable(),
137: 'title' => Expect::string(),
138: 'version' => Expect::string(),
139: 'baseUrl' => Expect::string(),
140:
141: // system
142: 'workingDir' => Expect::string(),
143: 'tempDir' => Expect::string(),
144: 'workerCount' => Expect::int()->min(1),
145: 'memoryLimit' => Expect::string(),
146: ]);
147:
148: (new Processor)->process($schema, $parameters);
149: }
150:
151:
152: /**
153: * @param mixed[][] $configs indexed by [][configKey]
154: * @return mixed[] indexed by [configKey]
155: */
156: protected static function mergeConfigs(array...$configs): array
157: {
158: $mergedConfig = [];
159:
160: foreach ($configs as $config) {
161: foreach ($config['parameters'] ?? [] as $key => $value) {
162: if (is_array($value)) {
163: $config['parameters'][$key][SchemaHelpers::PreventMerging] = true;
164: }
165: }
166:
167: $mergedConfig = SchemaHelpers::merge($config, $mergedConfig);
168: assert(is_array($mergedConfig));
169: }
170:
171: return $mergedConfig;
172: }
173:
174:
175: /**
176: * @return mixed[] indexed by [configKey]
177: */
178: protected static function loadConfig(string $path): array
179: {
180: $data = (new Loader)->load($path);
181: $data['parameters'] = self::resolvePaths($data['parameters'] ?? [], Helpers::realPath(dirname($path)));
182:
183: return $data;
184: }
185:
186:
187: /**
188: * @param mixed[] $parameters indexed by [parameterName]
189: * @return mixed[] indexed by [parameterName]
190: */
191: protected static function resolvePaths(array $parameters, string $base): array
192: {
193: foreach (['tempDir', 'workingDir', 'outputDir', 'themeDir'] as $parameterKey) {
194: if (isset($parameters[$parameterKey])) {
195: $parameters[$parameterKey] = self::resolvePath($parameters[$parameterKey], $base);
196: }
197: }
198:
199: foreach ($parameters['paths'] ?? [] as $i => $path) {
200: if (is_int($i)) {
201: $parameters['paths'][$i] = self::resolvePath($parameters['paths'][$i], $base);
202: }
203: }
204:
205: return $parameters;
206: }
207:
208:
209: protected static function resolvePath(string $path, string $base): string
210: {
211: return (FileSystem::isAbsolute($path) || str_starts_with($path, '%'))
212: ? $path
213: : FileSystem::joinPaths($base, $path);
214: }
215: }
216: