Skip to content

Commit a800669

Browse files
authored
Migrate bin/openapi to Symfony Console (#1977)
1 parent 7f6f6ab commit a800669

7 files changed

Lines changed: 220 additions & 361 deletions

File tree

bin/openapi

Lines changed: 28 additions & 252 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
#!/usr/bin/env php
22
<?php
33

4-
use OpenApi\Analysers\AttributeAnnotationFactory;
5-
use OpenApi\Analysers\DocBlockAnnotationFactory;
6-
use OpenApi\Analysers\ReflectionAnalyser;
7-
use OpenApi\Annotations\OpenApi;
4+
use OpenApi\Console\GenerateCommand;
85
use OpenApi\Generator;
9-
use OpenApi\SourceFinder;
10-
use OpenApi\Loggers\ConsoleLogger;
6+
use Symfony\Component\Console\Application;
7+
use Symfony\Component\Console\Input\ArgvInput;
8+
use Symfony\Component\Console\Logger\ConsoleLogger;
9+
use Symfony\Component\Console\Output\ConsoleOutput;
1110

1211
if (class_exists(Generator::class) === false) {
1312
if (file_exists(__DIR__.'/../vendor/autoload.php')) { // cloned / dev environment?
@@ -17,254 +16,31 @@ if (class_exists(Generator::class) === false) {
1716
}
1817
}
1918

20-
error_reporting(E_ALL);
21-
22-
// Possible options and their default values.
23-
$options = [
24-
'config' => [],
25-
'defaults' => false,
26-
'output' => false,
27-
'format' => 'auto',
28-
'exclude' => [],
29-
'pattern' => '*.php',
30-
'bootstrap' => [],
31-
'help' => false,
32-
'debug' => false,
33-
'add-processor' => [],
34-
'remove-processor' => [],
35-
'version' => null,
36-
];
37-
$aliases = [
38-
'c' => 'config',
39-
'D' => 'defaults',
40-
'o' => 'output',
41-
'e' => 'exclude',
42-
'n' => 'pattern',
43-
'b' => 'bootstrap',
44-
'h' => 'help',
45-
'd' => 'debug',
46-
'a' => 'add-processor',
47-
'r' => 'remove-processor',
48-
'f' => 'format'
49-
];
50-
$needsArgument = [
51-
'config',
52-
'output',
53-
'format',
54-
'exclude',
55-
'pattern',
56-
'bootstrap',
57-
'add-processor',
58-
'remove-processor',
59-
'version',
60-
];
61-
$paths = [];
62-
$error = false;
63-
64-
try {
65-
// Parse cli arguments
66-
for ($i = 1; $i < $argc; $i++) {
67-
$arg = $argv[$i];
68-
69-
if (substr($arg, 0, 2) === '--') {
70-
// longopt
71-
$option = substr($arg, 2);
72-
} elseif ($arg[0] === '-') {
73-
// shortopt
74-
if (array_key_exists(substr($arg, 1), $aliases)) {
75-
$option = $aliases[$arg[1]];
76-
} else {
77-
throw new Exception('Unknown option: "' . $arg . '"');
78-
}
79-
} else {
80-
$paths[] = $arg;
81-
continue;
82-
}
83-
84-
if (false === array_key_exists($option, $options)) {
85-
throw new Exception('Unknown option: "' . $arg . '"');
86-
}
87-
88-
if (in_array($option, $needsArgument)) {
89-
if (empty($argv[$i + 1]) || $argv[$i + 1][0] === '-') {
90-
throw new Exception('Missing argument for "' . $arg . '"');
91-
}
92-
if (is_array($options[$option])) {
93-
$options[$option][] = $argv[$i + 1];
94-
} else {
95-
$options[$option] = $argv[$i + 1];
96-
}
97-
$i++;
98-
} else {
99-
$options[$option] = true;
100-
}
101-
}
102-
} catch (\Exception $e) {
103-
$error = $e->getMessage();
104-
}
105-
106-
$logger = new ConsoleLogger($options['debug']);
107-
108-
if (!$error && $options['bootstrap']) {
109-
foreach ($options['bootstrap'] as $bootstrap) {
110-
$filenames = glob($bootstrap);
111-
if (false === $filenames) {
112-
$error = 'Invalid `--bootstrap` value: "' . $bootstrap . '"';
113-
break;
19+
$input = new class extends ArgvInput
20+
{
21+
public function hasParameterOption(array|string $values, bool $onlyParams = false): bool
22+
{
23+
// Skip the built-in version option check
24+
// thus the command can use it for its own purpose
25+
if (['--version', '-V'] === $values) {
26+
return false;
11427
}
115-
foreach ($filenames as $filename) {
116-
if ($options['debug']) {
117-
$logger->debug('Bootstrapping: ' . $filename);
118-
}
119-
require_once($filename);
120-
}
121-
}
122-
}
123-
124-
if ($options['defaults']) {
125-
$logger->info('Default config');
126-
$logger->info(json_encode((new Generator())->getDefaultConfig(), JSON_PRETTY_PRINT));
127-
exit(1);
128-
}
129-
130-
if (count($paths) === 0) {
131-
$error = 'Specify at least one path.';
132-
}
133-
134-
if ($options['help'] === false && $error) {
135-
$logger->error('', ['prefix' => '']);
136-
$logger->error($error);
137-
// Show help
138-
$options['help'] = true;
139-
}
140-
$defaultVersion = OpenApi::DEFAULT_VERSION;
141-
if ($options['help']) {
142-
$help = <<<EOF
143-
144-
Usage: openapi [--option value] [/path/to/project ...]
145-
146-
Options:
147-
--config (-c) Generator config.
148-
ex: -c operationId.hash=false
149-
--defaults (-D) Show default config.
150-
--output (-o) Path to store the generated documentation.
151-
ex: --output openapi.yaml
152-
--exclude (-e) Exclude path(s).
153-
ex: --exclude vendor,library/Zend
154-
--pattern (-n) Pattern of files to scan.
155-
ex: --pattern "*.php" or --pattern "/\.(phps|php)$/"
156-
--bootstrap (-b) Bootstrap php file(s) for defining constants, etc.
157-
ex: --bootstrap config/constants.php
158-
--add-processor (-a) Register an additional processor (allows multiple).
159-
--remove-processor (-r) Remove an existing processor (allows multiple).
160-
--format (-f) Force yaml or json.
161-
--debug (-d) Show additional error information.
162-
--version The OpenAPI version; defaults to {$defaultVersion}.
163-
--help (-h) Display this help message.
16428

165-
166-
EOF;
167-
$logger->info($help);
168-
exit(1);
169-
}
170-
171-
$errorTypes = [
172-
E_ERROR => 'Error',
173-
E_WARNING => 'Warning',
174-
E_PARSE => 'Parser error',
175-
E_NOTICE => 'Notice',
176-
E_DEPRECATED => 'Deprecated',
177-
E_CORE_ERROR => 'Error(Core)',
178-
E_CORE_WARNING => 'Warning(Core)',
179-
E_COMPILE_ERROR => 'Error(compile)',
180-
E_COMPILE_WARNING => 'Warning(Compile)',
181-
E_RECOVERABLE_ERROR => 'Error(Recoverable)',
182-
E_USER_ERROR => 'Error',
183-
E_USER_WARNING => 'Warning',
184-
E_USER_NOTICE => 'Notice',
185-
E_USER_DEPRECATED => 'Deprecated',
186-
];
187-
set_error_handler(function ($errno, $errstr, $file, $line) use ($errorTypes, $options, $logger) {
188-
if (!(error_reporting() & $errno)) {
189-
// This error code is not included in error_reporting
190-
return;
191-
}
192-
$type = array_key_exists($errno, $errorTypes) ? $errorTypes[$errno] : 'Error';
193-
if ($type === 'Deprecated') {
194-
$logger->info($errstr, ['prefix' => $type . ': ']);
195-
} else {
196-
$logger->error($errstr, ['prefix' => $type . ': ']);
197-
}
198-
199-
if ($options['debug']) {
200-
$logger->info(' in '.$file.' on line '.$line);
201-
}
202-
if (substr($type, 0, 5) === 'Error') {
203-
exit($errno);
29+
return parent::hasParameterOption($values, $onlyParams);
20430
}
205-
});
206-
207-
set_exception_handler(function ($exception) use ($logger) {
208-
$logger->error($exception);
209-
exit($exception->getCode() ?: 1);
210-
});
31+
};
32+
$output = new ConsoleOutput();
33+
$logger = new ConsoleLogger($output);
34+
$app = new Application();
21135

212-
$exclude = null;
213-
if ($options['exclude']) {
214-
$exclude = $options['exclude'];
215-
if (strpos($exclude[0], ',') !== false) {
216-
$exploded = explode(',', $exclude[0]);
217-
$logger->error('Comma-separated exclude paths are deprecated, use multiple --exclude statements: --exclude '.$exploded[0].' --exclude '.$exploded[1]);
218-
$exclude[0] = array_shift($exploded);
219-
$exclude = array_merge($exclude, $exploded);
220-
}
221-
}
36+
// Remove Symfony's built-in options that conflict with our command options:
37+
// --version (-V): conflicts with our --version (VALUE_REQUIRED for OpenAPI spec version)
38+
// --no-interaction (-n): conflicts with our --pattern (-n)
39+
$definition = $app->getDefinition();
40+
$options = $definition->getOptions();
41+
unset($options['version'], $options['no-interaction']);
42+
$definition->setOptions($options);
22243

223-
$pattern = "*.php";
224-
if ($options['pattern']) {
225-
$pattern = $options['pattern'];
226-
}
227-
228-
$generator = new Generator($logger);
229-
foreach ($options["add-processor"] as $processor) {
230-
$class = '\OpenApi\Processors\\'.ucfirst($processor);
231-
if (class_exists($class)) {
232-
$processor = new $class();
233-
} elseif (class_exists($processor)) {
234-
$processor = new $processor();
235-
}
236-
$generator->getProcessorPipeline()->add($processor);
237-
}
238-
foreach ($options["remove-processor"] as $processor) {
239-
$class = class_exists($processor)
240-
? $class
241-
: '\OpenApi\Processors\\'.ucfirst($processor);
242-
$generator->getProcessorPipeline()->remove($class);
243-
}
244-
245-
$analyser = new ReflectionAnalyser([
246-
new AttributeAnnotationFactory(),
247-
new DocBlockAnnotationFactory(),
248-
]);
249-
$analyser->setGenerator($generator);
250-
251-
$openapi = $generator
252-
->setVersion($options['version'])
253-
->setConfig($options['config'])
254-
->setAnalyser($analyser)
255-
->generate(new SourceFinder($paths, $exclude, $pattern));
256-
257-
if ($options['output'] === false) {
258-
if (strtolower($options['format']) === 'json') {
259-
echo $openapi->toJson();
260-
} else {
261-
echo $openapi->toYaml();
262-
}
263-
echo "\n";
264-
} else {
265-
if (is_dir($options['output'])) {
266-
$options['output'] .= '/openapi.yaml';
267-
}
268-
$openapi->saveAs($options['output'], $options['format']);
269-
}
270-
exit($logger->loggedMessageAboveNotice() ? 1 : 0);
44+
$app->addCommand(new GenerateCommand($logger));
45+
$app->setDefaultCommand('openapi', true);
46+
$app->run($input, $output);

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"phpstan/phpdoc-parser": "^2.0",
5050
"psr/log": "^1.1 || ^2.0 || ^3.0",
5151
"radebatz/type-info-extras": "^1.0.2",
52+
"symfony/console": "^7.4 || ^8.0",
5253
"symfony/deprecation-contracts": "^2 || ^3",
5354
"symfony/finder": "^5.0 || ^6.0 || ^7.0 || ^8.0",
5455
"symfony/yaml": "^5.4 || ^6.0 || ^7.0 || ^8.0"

src/Console/GenerateCommand.php

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php declare(strict_types=1);
2+
3+
/**
4+
* @license Apache 2.0
5+
*/
6+
7+
namespace OpenApi\Console;
8+
9+
use OpenApi\Annotations as OA;
10+
use OpenApi\Generator;
11+
use OpenApi\SourceFinder;
12+
use Symfony\Component\Console\Attribute\AsCommand;
13+
use Symfony\Component\Console\Attribute\MapInput;
14+
use Symfony\Component\Console\Logger\ConsoleLogger;
15+
use Symfony\Component\Console\Output\OutputInterface;
16+
use Symfony\Component\Console\Style\SymfonyStyle;
17+
18+
#[AsCommand(
19+
name: 'openapi',
20+
description: 'Generate OpenAPI documentation',
21+
)]
22+
class GenerateCommand
23+
{
24+
public function __construct(
25+
private ConsoleLogger $logger,
26+
) {
27+
}
28+
29+
public function __invoke(#[MapInput] GenerateInput $input, SymfonyStyle $io): int
30+
{
31+
$io->setVerbosity($input->debug ? OutputInterface::VERBOSITY_DEBUG : $io->getVerbosity());
32+
33+
foreach ($input->getBootstrapFilenames() as $filename) {
34+
if ($io->isVerbose()) {
35+
$io->info('Bootstrapping: ' . $filename);
36+
}
37+
38+
require_once($filename);
39+
}
40+
41+
if ($input->defaults) {
42+
$io->title('Default config');
43+
$io->writeln(json_encode((new Generator())->getDefaultConfig(), JSON_PRETTY_PRINT));
44+
45+
return 0;
46+
}
47+
48+
$openapi = $this->generate($input);
49+
50+
if (!$input->output) {
51+
if ($input->format->isJson()) {
52+
echo $openapi->toJson();
53+
} else {
54+
echo $openapi->toYaml();
55+
}
56+
echo "\n";
57+
} else {
58+
$outputPath = $input->output;
59+
if (is_dir($outputPath)) {
60+
$outputPath .= '/openapi.yaml';
61+
}
62+
$openapi->saveAs($outputPath, $input->format->value);
63+
}
64+
65+
return $this->logger->hasErrored() ? 1 : 0;
66+
}
67+
68+
private function generate(GenerateInput $input): OA\OpenApi
69+
{
70+
$generator = new Generator($this->logger);
71+
72+
foreach ($input->addProcessor as $processor) {
73+
$class = '\OpenApi\Processors\\' . ucfirst((string) $processor);
74+
if (class_exists($class)) {
75+
$processor = new $class();
76+
} elseif (class_exists($processor)) {
77+
$processor = new $processor();
78+
}
79+
$generator->getProcessorPipeline()->add($processor);
80+
}
81+
82+
foreach ($input->removeProcessor as $processor) {
83+
$class = class_exists($processor)
84+
? $processor
85+
: '\OpenApi\Processors\\' . ucfirst((string) $processor);
86+
$generator->getProcessorPipeline()->remove($class);
87+
}
88+
89+
return $generator
90+
->setVersion($input->version)
91+
->setConfig($input->config)
92+
->generate(new SourceFinder($input->paths, $input->exclude, $input->pattern));
93+
}
94+
}

0 commit comments

Comments
 (0)