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']
- ;
- }
}