diff --git a/CHANGELOG-6.x.md b/CHANGELOG-6.x.md index 0c4e00bc..1b8bc2e4 100644 --- a/CHANGELOG-6.x.md +++ b/CHANGELOG-6.x.md @@ -7,6 +7,10 @@ using the [Keep a CHANGELOG](http://keepachangelog.com) principles. ## [Unreleased] +### Added + +- improves `output` option by introducing Reporter extension (see [documentation](docs/01_Components/04_Extensions/Reporter.md)) + ## [6.0.1] - 2021-12-13 ### Fixed diff --git a/docs/01_Components/04_Extensions/Reporter.md b/docs/01_Components/04_Extensions/Reporter.md new file mode 100644 index 00000000..7adfe306 --- /dev/null +++ b/docs/01_Components/04_Extensions/Reporter.md @@ -0,0 +1,43 @@ + +# Output format + +**Since version 6.1.0**, PHP CompatInfo supports different output formats through various formatters. + +You can pass the following keywords to the `--output` CLI option of the `analyser:run` command +in order to affect the output: + +- `console`: default table format for human reading. +- `dump`: raw format (`var_dump`) for debugging purpose only. +- `json`: creates minified json file format output without whitespaces. + +You can also implement your own custom formatter by implementing +the `Bartlett\CompatInfo\Application\Extension\Reporter\FormatterInterface` interface in a new class. + +This is how the `FormatterInterface` interface looks like: + +```php +input = $input; + $this->output = $output; + } + + /** + * {@inheritDoc} + */ + public function getName(): string + { + return static::NAME . '_reporter'; + } + + /** + * @param string[] $formats + */ + public function supportsFormatting(object $object, array $formats): bool + { + return ($object instanceof Profile && in_array(static::NAME, $formats)); + } + + /** + * {@inheritDoc} + */ + public function afterAnalysis(AfterAnalysisEvent $event): void + { + if ($event->hasArgument('profile')) { + $this->format($event->getArgument('profile')); // @phpstan-ignore-line + } + } +} diff --git a/src/Application/Extension/Reporter/ConsoleReporter.php b/src/Application/Extension/Reporter/ConsoleReporter.php new file mode 100644 index 00000000..246d5323 --- /dev/null +++ b/src/Application/Extension/Reporter/ConsoleReporter.php @@ -0,0 +1,291 @@ + */ + private $metrics; + + /** + * {@inheritDoc} + */ + public function format($data): void + { + /** @var string[] $format */ + $format = $this->input->getOption('output'); + if (!$this->supportsFormatting($data, $format)) { + return; + } + + $output = new Style($this->input, $this->output); + $response = current($data->getData()); + + if (empty($response)) { + // No reports printed if there are no metrics. + $output->warning('No metrics.'); + return; + } + + $output->title('Compatibility Analyser'); + + $output->section('Data Source Analysed'); + + $directories = []; + $files = $response['files']; + $errors = $response['errors']; + + foreach ($files as $file) { + $directories[] = dirname($file); + } + $directories = array_unique($directories); + + // print Data Source summaries + if (count($files) > 0) { + $output->columns( + count($directories), + 'Directories %10d' + ); + if ($output->isVerbose()) { + $directories = array_map(function ($dir) { + return ' + ' . $dir; + }, $directories); + $output->writeln(''); + $output->writeln($directories); + $output->writeln(''); + } + + $output->columns( + count($files), + 'Files %10d' + ); + if ($output->isVerbose()) { + $files = array_map(function ($file) { + return ' + ' . $file; + }, $files); + $output->writeln(''); + $output->writeln($files); + $output->writeln(''); + } + + $output->columns( + count($errors), + 'Errors %10d' + ); + } + + if (count($errors)) { + $output->caution( + sprintf( + 'Found %d error%s, while parsing data source', + count($errors), + count($errors) > 1 ? 's' : '' + ) + ); + + foreach ($errors as $msg) { + $text = sprintf( + '! %s', + $msg + ); + $output->text($text); + } + } + + $this->metrics = $metrics = $response[CompatibilityAnalyser::class]; + + $groups = [ + 'extensions', + 'namespaces', + 'interfaces', 'traits', 'classes', + 'generators', + 'functions', + 'constants', + 'conditions', + ]; + foreach ($groups as $section) { + $this->formatSection($section, $output); + } + + if ( + !array_key_exists('versions', $metrics) + || empty($metrics['versions']) + ) { + return; + } + + $min = sprintf('PHP %s (min)', $metrics['versions']['php.min']); + + if (empty($metrics['versions']['php.max'])) { + $max = ''; + } else { + $max = sprintf(', PHP %s (max)', $metrics['versions']['php.max']); + } + + $output->success(sprintf('Requires %s%s', $min, $max)); + $output->comment('Produced by ' . $this->getName()); + } + + private function formatSection(string $section, StyleInterface $io): void + { + $length = ('classes' == $section) ? -2 : -1; + $title = substr($section, 0, $length); + + $args = $this->metrics[$section] ?? []; + + if (empty($args)) { + $io->note(sprintf('No %s found', $title)); + return; + } + + $versions = [ + 'ext.name' => 'user', + 'ext.min' => '', + 'ext.max' => '', + 'ext.all' => '', + 'php.min' => '4.0.0', + 'php.max' => '', + 'php.all' => '', + ]; + // compute global versions of the $section + foreach ($args as $name => $base) { + if (isset($base['optional'])) { + // do not compute conditional elements + continue; + } + foreach ($base as $id => $version) { + if ( + !in_array(substr($id, -3), array('min', 'max', 'all')) + || 'arg.max' == $id + ) { + continue; + } + if (null !== $version && version_compare($version, $versions[$id], 'gt')) { + $versions[$id] = $version; + } + } + } + $phpRequired = self::php($versions); + + $io->section(sprintf('%s Analysis', ucfirst($section))); + + $rows = []; + ksort($args); + + foreach ($args as $arg => $versions) { + //if ($arg == 'var_dump') var_dump($versions); + + $flags = isset($versions['optional']) ? 'C' : ' '; + + if (in_array($section, ['classes', 'interfaces', 'traits'])) { + if ( + 'user' == $versions['ext.name'] + && ($versions['declared'] ?? false) === false + ) { + $flags .= 'U'; + } + } + + $row = [ + $flags, + $arg, + isset($versions['ext.name']) ? $versions['ext.name'] : '', + self::ext($versions), + self::php($versions), + ]; + /* + for reference:show command, + tell us if there are some PHP versions excluded + */ + if (!empty($versions['php.excludes'])) { + $row[0] = 'W'; + } + $rows[] = $row; + + if ( + in_array($section, ['classes', 'interfaces', 'traits']) + && $io->isVerbose() + && !in_array($arg, ['parent', 'self', 'static']) + ) { + foreach ($this->metrics['methods'] as $method => $version) { + if (strpos($method, "$arg\\") === 0) { + $flags = isset($version['optional']) ? 'C' : ' '; + $rows[] = [ + $flags, + sprintf('function %s', str_replace("$arg\\", '', $method)), + isset($version['ext.name']) ? $version['ext.name'] : '', + self::ext($version), + self::php($version), + ]; + } + } + } + } + + $headers = [' ', ucfirst($title), 'REF', 'EXT min/Max', 'PHP min/Max']; + + $footers = [ + '', + sprintf('Total [%d]', count($args)), + '', + '', + sprintf('%s', $phpRequired) + ]; + $rows[] = new TableSeparator(); + $rows[] = $footers; + + $io->table($headers, $rows); + } + + /** + * @param array $domain + * @return string + */ + private function ext(array $domain): string + { + return empty($domain['ext.max']) + ? $domain['ext.min'] + : $domain['ext.min'] . ' => ' . $domain['ext.max'] + ; + } + + /** + * @param array $domain + * @return string + */ + private function php(array $domain): string + { + return empty($domain['php.max']) + ? $domain['php.min'] + : $domain['php.min'] . ' => ' . $domain['php.max'] + ; + } +} diff --git a/src/Application/Extension/Reporter/DumpReporter.php b/src/Application/Extension/Reporter/DumpReporter.php new file mode 100644 index 00000000..befbc257 --- /dev/null +++ b/src/Application/Extension/Reporter/DumpReporter.php @@ -0,0 +1,34 @@ +input->getOption('output'); + if (!$this->supportsFormatting($data, $format)) { + return; + } + + var_dump($data); + + $output = new Style($this->input, $this->output); + $output->comment('Produced by ' . $this->getName()); + } +} diff --git a/src/Application/Extension/Reporter/FormatterInterface.php b/src/Application/Extension/Reporter/FormatterInterface.php new file mode 100644 index 00000000..9473628b --- /dev/null +++ b/src/Application/Extension/Reporter/FormatterInterface.php @@ -0,0 +1,23 @@ +input->getOption('output'); + if (!$this->supportsFormatting($data, $format)) { + return; + } + + $data = $data->getData(); + $token = key($data); + $target = '/tmp/' . $token . '-compatinfo.json'; + @file_put_contents($target, json_encode($data[$token])); + + $output = new Style($this->input, $this->output); + $output->note('Profile results are being formatted as JSON to file ' . $target); + $output->comment('Produced by ' . $this->getName()); + } +} diff --git a/src/Application/PhpParser/Parser.php b/src/Application/PhpParser/Parser.php index 3f9f23c7..5d9a711e 100644 --- a/src/Application/PhpParser/Parser.php +++ b/src/Application/PhpParser/Parser.php @@ -107,9 +107,15 @@ public function parse(string $source, Finder $finder, ErrorHandler $errorHandler $this->analyser->tearDownAfterVisitor(); - $this->dispatcher->dispatch(new AfterAnalysisEvent($this, ['source' => $source, 'successCount' => $this->filesProceeded])); + $profile = $profiler->collect(); + $this->dispatcher->dispatch( + new AfterAnalysisEvent( + $this, + ['source' => $source, 'successCount' => $this->filesProceeded, 'profile' => $profile] + ) + ); - return $profiler->collect(); + return $profile; } /** diff --git a/src/Presentation/Console/Application.php b/src/Presentation/Console/Application.php index 4a3e709d..03afd769 100644 --- a/src/Presentation/Console/Application.php +++ b/src/Presentation/Console/Application.php @@ -139,8 +139,9 @@ protected function getDefaultInputDefinition(): InputDefinition new InputOption( 'output', null, - InputOption::VALUE_OPTIONAL, - 'Write results to file' + InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, + 'Affect output to produce results in different format', + ['console'] ) ); $definition->addOption( diff --git a/src/Presentation/Console/ApplicationInterface.php b/src/Presentation/Console/ApplicationInterface.php index 595e6a16..91c3be70 100644 --- a/src/Presentation/Console/ApplicationInterface.php +++ b/src/Presentation/Console/ApplicationInterface.php @@ -13,7 +13,7 @@ interface ApplicationInterface extends ContainerAwareInterface { public const NAME = 'phpCompatInfo'; - public const VERSION = '6.0.1'; + public const VERSION = '6.1.0-dev'; /** * @param CommandLoaderInterface $commandLoader diff --git a/src/Presentation/Console/Command/AnalyserCommand.php b/src/Presentation/Console/Command/AnalyserCommand.php index 0786b098..4e2af45a 100644 --- a/src/Presentation/Console/Command/AnalyserCommand.php +++ b/src/Presentation/Console/Command/AnalyserCommand.php @@ -6,34 +6,15 @@ namespace Bartlett\CompatInfo\Presentation\Console\Command; -use Bartlett\CompatInfo\Application\Analyser\CompatibilityAnalyser; -use Bartlett\CompatInfo\Application\Profiler\Profile; use Bartlett\CompatInfo\Application\Query\Analyser\Compatibility\GetCompatibilityQuery; use Bartlett\CompatInfo\Presentation\Console\Style; -use Bartlett\CompatInfo\Presentation\Console\StyleInterface; -use Symfony\Component\Console\Helper\TableSeparator; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Messenger\Exception\HandlerFailedException; -use function array_key_exists; -use function array_map; -use function array_unique; -use function count; -use function current; -use function dirname; -use function in_array; -use function ksort; -use function sprintf; -use function str_replace; -use function strpos; -use function substr; -use function ucfirst; -use function version_compare; - /** * @since Release 6.0.0 */ @@ -41,9 +22,6 @@ final class AnalyserCommand extends AbstractCommand implements CommandInterface { public const NAME = 'analyser:run'; - /** @var array */ - private $metrics; - protected function configure(): void { $this->setName(self::NAME) @@ -77,267 +55,13 @@ protected function execute(InputInterface $input, OutputInterface $output) ); try { - $analysis = $this->queryBus->query($compatibilityQuery); + $this->queryBus->query($compatibilityQuery); } catch (HandlerFailedException $e) { $io = new Style($input, $output); $io->error($e->getMessage()); return self::FAILURE; } - if ($output->isDebug()) { - var_dump($analysis); - } elseif ($analysis instanceof Profile) { - $this->printReport(new Style($input, $output), current($analysis->getData())); - } - return self::SUCCESS; } - - /** - * @param StyleInterface $output - * @param array $response - */ - private function printReport(StyleInterface $output, array $response): void - { - if (empty($response)) { - // No reports printed if there are no metrics. - $output->warning('No metrics.'); - return; - } - - $output->title('Compatibility Analyser'); - - $output->section('Data Source Analysed'); - - $directories = []; - $files = $response['files']; - $errors = $response['errors']; - - foreach ($files as $file) { - $directories[] = dirname($file); - } - $directories = array_unique($directories); - - // print Data Source summaries - if (count($files) > 0) { - $output->columns( - count($directories), - 'Directories %10d' - ); - if ($output->isVerbose()) { - $directories = array_map(function ($dir) { - return ' + ' . $dir; - }, $directories); - $output->writeln(''); - $output->writeln($directories); - $output->writeln(''); - } - - $output->columns( - count($files), - 'Files %10d' - ); - if ($output->isVerbose()) { - $files = array_map(function ($file) { - return ' + ' . $file; - }, $files); - $output->writeln(''); - $output->writeln($files); - $output->writeln(''); - } - - $output->columns( - count($errors), - 'Errors %10d' - ); - } - - if (count($errors)) { - $output->caution( - sprintf( - 'Found %d error%s, while parsing data source', - count($errors), - count($errors) > 1 ? 's' : '' - ) - ); - - foreach ($errors as $msg) { - $text = sprintf( - '! %s', - $msg - ); - $output->text($text); - } - } - - $this->metrics = $metrics = $response[CompatibilityAnalyser::class]; - - $groups = [ - 'extensions', - 'namespaces', - 'interfaces', 'traits', 'classes', - 'generators', - 'functions', - 'constants', - 'conditions', - ]; - foreach ($groups as $section) { - $this->formatSection($section, $output); - } - - if ( - !array_key_exists('versions', $metrics) - || empty($metrics['versions']) - ) { - return; - } - - $min = sprintf('PHP %s (min)', $metrics['versions']['php.min']); - - if (empty($metrics['versions']['php.max'])) { - $max = ''; - } else { - $max = sprintf(', PHP %s (max)', $metrics['versions']['php.max']); - } - - $style = 'php'; - $style = $output->getFormatter()->hasStyle($style) ? $style : 'comment'; - - $output->success(sprintf('Requires %s%s', $min, $max)); - } - - private function formatSection(string $section, StyleInterface $io): void - { - $length = ('classes' == $section) ? -2 : -1; - $title = substr($section, 0, $length); - - $args = $this->metrics[$section] ?? []; - - if (empty($args)) { - $io->note(sprintf('No %s found', $title)); - return; - } - - $versions = [ - 'ext.name' => 'user', - 'ext.min' => '', - 'ext.max' => '', - 'ext.all' => '', - 'php.min' => '4.0.0', - 'php.max' => '', - 'php.all' => '', - ]; - // compute global versions of the $section - foreach ($args as $name => $base) { - if (isset($base['optional'])) { - // do not compute conditional elements - continue; - } - foreach ($base as $id => $version) { - if ( - !in_array(substr($id, -3), array('min', 'max', 'all')) - || 'arg.max' == $id - ) { - continue; - } - if (null !== $version && version_compare($version, $versions[$id], 'gt')) { - $versions[$id] = $version; - } - } - } - $phpRequired = self::php($versions); - - $io->section(sprintf('%s Analysis', ucfirst($section))); - - $rows = []; - ksort($args); - - foreach ($args as $arg => $versions) { - //if ($arg == 'var_dump') var_dump($versions); - - $flags = isset($versions['optional']) ? 'C' : ' '; - - if (in_array($section, ['classes', 'interfaces', 'traits'])) { - if ( - 'user' == $versions['ext.name'] - && ($versions['declared'] ?? false) === false - ) { - $flags .= 'U'; - } - } - - $row = [ - $flags, - $arg, - isset($versions['ext.name']) ? $versions['ext.name'] : '', - self::ext($versions), - self::php($versions), - ]; - /* - for reference:show command, - tell us if there are some PHP versions excluded - */ - if (!empty($versions['php.excludes'])) { - $row[0] = 'W'; - } - $rows[] = $row; - - if ( - in_array($section, ['classes', 'interfaces', 'traits']) - && $io->isVerbose() - && !in_array($arg, ['parent', 'self', 'static']) - ) { - foreach ($this->metrics['methods'] as $method => $version) { - if (strpos($method, "$arg\\") === 0) { - $flags = isset($version['optional']) ? 'C' : ' '; - $rows[] = [ - $flags, - sprintf('function %s', str_replace("$arg\\", '', $method)), - isset($version['ext.name']) ? $version['ext.name'] : '', - self::ext($version), - self::php($version), - ]; - } - } - } - } - - $headers = [' ', ucfirst($title), 'REF', 'EXT min/Max', 'PHP min/Max']; - - $footers = [ - '', - sprintf('Total [%d]', count($args)), - '', - '', - sprintf('%s', $phpRequired) - ]; - $rows[] = new TableSeparator(); - $rows[] = $footers; - - $io->table($headers, $rows); - } - - /** - * @param array $domain - * @return string - */ - private function ext(array $domain): string - { - return empty($domain['ext.max']) - ? $domain['ext.min'] - : $domain['ext.min'] . ' => ' . $domain['ext.max'] - ; - } - - /** - * @param array $domain - * @return string - */ - private function php(array $domain): string - { - return empty($domain['php.max']) - ? $domain['php.min'] - : $domain['php.min'] . ' => ' . $domain['php.max'] - ; - } }