Skip to content

Commit bc9d96f

Browse files
authored
DX: Progress output refactor (#6848)
1 parent 1ab962b commit bc9d96f

14 files changed

+327
-83
lines changed

src/Console/Command/FixCommand.php

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@
1919
use PhpCsFixer\ConfigurationException\InvalidConfigurationException;
2020
use PhpCsFixer\Console\ConfigurationResolver;
2121
use PhpCsFixer\Console\Output\ErrorOutput;
22-
use PhpCsFixer\Console\Output\NullOutput;
23-
use PhpCsFixer\Console\Output\ProcessOutput;
22+
use PhpCsFixer\Console\Output\OutputContext;
23+
use PhpCsFixer\Console\Output\Progress\ProgressOutputFactory;
24+
use PhpCsFixer\Console\Output\Progress\ProgressOutputType;
2425
use PhpCsFixer\Console\Report\FixReport\ReportSummary;
2526
use PhpCsFixer\Error\ErrorsManager;
27+
use PhpCsFixer\FixerFileProcessedEvent;
2628
use PhpCsFixer\Runner\Runner;
2729
use PhpCsFixer\ToolInfoInterface;
2830
use Symfony\Component\Console\Attribute\AsCommand;
@@ -61,6 +63,8 @@ final class FixCommand extends Command
6163

6264
private ToolInfoInterface $toolInfo;
6365

66+
private ProgressOutputFactory $progressOutputFactory;
67+
6468
public function __construct(ToolInfoInterface $toolInfo)
6569
{
6670
parent::__construct();
@@ -70,6 +74,7 @@ public function __construct(ToolInfoInterface $toolInfo)
7074
$this->stopwatch = new Stopwatch();
7175
$this->defaultConfig = new Config();
7276
$this->toolInfo = $toolInfo;
77+
$this->progressOutputFactory = new ProgressOutputFactory();
7378
}
7479

7580
/**
@@ -264,7 +269,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int
264269
}
265270
}
266271

267-
$progressType = $resolver->getProgress();
268272
$finder = new \ArrayIterator(iterator_to_array($resolver->getFinder()));
269273

270274
if (null !== $stdErr && $resolver->configFinderIsOverridden()) {
@@ -273,22 +277,21 @@ protected function execute(InputInterface $input, OutputInterface $output): int
273277
);
274278
}
275279

276-
if ('none' === $progressType || null === $stdErr) {
277-
$progressOutput = new NullOutput();
278-
} else {
279-
$progressOutput = new ProcessOutput(
280+
$progressType = $resolver->getProgressType();
281+
$progressOutput = $this->progressOutputFactory->create(
282+
$progressType,
283+
new OutputContext(
280284
$stdErr,
281-
$this->eventDispatcher,
282285
(new Terminal())->getWidth(),
283286
\count($finder)
284-
);
285-
}
287+
)
288+
);
286289

287290
$runner = new Runner(
288291
$finder,
289292
$resolver->getFixers(),
290293
$resolver->getDiffer(),
291-
'none' !== $progressType ? $this->eventDispatcher : null,
294+
ProgressOutputType::NONE !== $progressType ? $this->eventDispatcher : null,
292295
$this->errorsManager,
293296
$resolver->getLinter(),
294297
$resolver->isDryRun(),
@@ -297,9 +300,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int
297300
$resolver->shouldStopOnViolation()
298301
);
299302

303+
$this->eventDispatcher->addListener(FixerFileProcessedEvent::NAME, [$progressOutput, 'onFixerFileProcessed']);
300304
$this->stopwatch->start('fixFiles');
301305
$changed = $runner->fix();
302306
$this->stopwatch->stop('fixFiles');
307+
$this->eventDispatcher->removeListener(FixerFileProcessedEvent::NAME, [$progressOutput, 'onFixerFileProcessed']);
303308

304309
$progressOutput->printLegend();
305310

src/Console/ConfigurationResolver.php

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use PhpCsFixer\Cache\Signature;
2424
use PhpCsFixer\ConfigInterface;
2525
use PhpCsFixer\ConfigurationException\InvalidConfigurationException;
26+
use PhpCsFixer\Console\Output\Progress\ProgressOutputType;
2627
use PhpCsFixer\Console\Report\FixReport\ReporterFactory;
2728
use PhpCsFixer\Console\Report\FixReport\ReporterInterface;
2829
use PhpCsFixer\Differ\DifferInterface;
@@ -406,26 +407,27 @@ static function (string $rawPath) use ($cwd, $filesystem): string {
406407
/**
407408
* @throws InvalidConfigurationException
408409
*/
409-
public function getProgress(): string
410+
public function getProgressType(): string
410411
{
411412
if (null === $this->progress) {
412413
if (OutputInterface::VERBOSITY_VERBOSE <= $this->options['verbosity'] && 'txt' === $this->getFormat()) {
413414
$progressType = $this->options['show-progress'];
414-
$progressTypes = ['none', 'dots'];
415415

416416
if (null === $progressType) {
417-
$progressType = $this->getConfig()->getHideProgress() ? 'none' : 'dots';
418-
} elseif (!\in_array($progressType, $progressTypes, true)) {
417+
$progressType = $this->getConfig()->getHideProgress()
418+
? ProgressOutputType::NONE
419+
: ProgressOutputType::DOTS;
420+
} elseif (!\in_array($progressType, ProgressOutputType::AVAILABLE, true)) {
419421
throw new InvalidConfigurationException(sprintf(
420422
'The progress type "%s" is not defined, supported are %s.',
421423
$progressType,
422-
Utils::naturalLanguageJoin($progressTypes)
424+
Utils::naturalLanguageJoin(ProgressOutputType::AVAILABLE)
423425
));
424426
}
425427

426428
$this->progress = $progressType;
427429
} else {
428-
$this->progress = 'none';
430+
$this->progress = ProgressOutputType::NONE;
429431
}
430432
}
431433

src/Console/Output/OutputContext.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of PHP CS Fixer.
7+
*
8+
* (c) Fabien Potencier <fabien@symfony.com>
9+
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
10+
*
11+
* This source file is subject to the MIT license that is bundled
12+
* with this source code in the file LICENSE.
13+
*/
14+
15+
namespace PhpCsFixer\Console\Output;
16+
17+
use Symfony\Component\Console\Output\OutputInterface;
18+
19+
/**
20+
* @internal
21+
*/
22+
final class OutputContext
23+
{
24+
private ?OutputInterface $output;
25+
private int $terminalWidth;
26+
private int $filesCount;
27+
28+
public function __construct(
29+
?OutputInterface $output,
30+
int $terminalWidth,
31+
int $filesCount
32+
) {
33+
$this->output = $output;
34+
$this->terminalWidth = $terminalWidth;
35+
$this->filesCount = $filesCount;
36+
}
37+
38+
public function getOutput(): ?OutputInterface
39+
{
40+
return $this->output;
41+
}
42+
43+
public function getTerminalWidth(): int
44+
{
45+
return $this->terminalWidth;
46+
}
47+
48+
public function getFilesCount(): int
49+
{
50+
return $this->filesCount;
51+
}
52+
}

src/Console/Output/ProcessOutput.php renamed to src/Console/Output/Progress/DotsOutput.php

Lines changed: 23 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,18 @@
1212
* with this source code in the file LICENSE.
1313
*/
1414

15-
namespace PhpCsFixer\Console\Output;
15+
namespace PhpCsFixer\Console\Output\Progress;
1616

17+
use PhpCsFixer\Console\Output\OutputContext;
1718
use PhpCsFixer\FixerFileProcessedEvent;
1819
use Symfony\Component\Console\Output\OutputInterface;
19-
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
2020

2121
/**
22-
* Output writer to show the process of a FixCommand.
22+
* Output writer to show the progress of a FixCommand using dots and meaningful letters.
2323
*
2424
* @internal
2525
*/
26-
final class ProcessOutput implements ProcessOutputInterface
26+
final class DotsOutput implements ProgressOutputInterface
2727
{
2828
/**
2929
* File statuses map.
@@ -39,11 +39,8 @@ final class ProcessOutput implements ProcessOutputInterface
3939
FixerFileProcessedEvent::STATUS_LINT => ['symbol' => 'E', 'format' => '<bg=red>%s</bg=red>', 'description' => 'error'],
4040
];
4141

42-
private OutputInterface $output;
43-
44-
private EventDispatcherInterface $eventDispatcher;
45-
46-
private int $files;
42+
/** @readonly */
43+
private OutputContext $context;
4744

4845
private int $processedFiles = 0;
4946

@@ -52,22 +49,14 @@ final class ProcessOutput implements ProcessOutputInterface
5249
*/
5350
private $symbolsPerLine;
5451

55-
public function __construct(OutputInterface $output, EventDispatcherInterface $dispatcher, int $width, int $nbFiles)
52+
public function __construct(OutputContext $context)
5653
{
57-
$this->output = $output;
58-
$this->eventDispatcher = $dispatcher;
59-
$this->eventDispatcher->addListener(FixerFileProcessedEvent::NAME, [$this, 'onFixerFileProcessed']);
60-
$this->files = $nbFiles;
54+
$this->context = $context;
6155

6256
// max number of characters per line
6357
// - total length x 2 (e.g. " 1 / 123" => 6 digits and padding spaces)
6458
// - 11 (extra spaces, parentheses and percentage characters, e.g. " x / x (100%)")
65-
$this->symbolsPerLine = max(1, $width - \strlen((string) $this->files) * 2 - 11);
66-
}
67-
68-
public function __destruct()
69-
{
70-
$this->eventDispatcher->removeListener(FixerFileProcessedEvent::NAME, [$this, 'onFixerFileProcessed']);
59+
$this->symbolsPerLine = max(1, $context->getTerminalWidth() - \strlen((string) $context->getFilesCount()) * 2 - 11);
7160
}
7261

7362
/**
@@ -93,24 +82,24 @@ public function __wakeup(): void
9382
public function onFixerFileProcessed(FixerFileProcessedEvent $event): void
9483
{
9584
$status = self::$eventStatusMap[$event->getStatus()];
96-
$this->output->write($this->output->isDecorated() ? sprintf($status['format'], $status['symbol']) : $status['symbol']);
85+
$this->getOutput()->write($this->getOutput()->isDecorated() ? sprintf($status['format'], $status['symbol']) : $status['symbol']);
9786

9887
++$this->processedFiles;
9988

10089
$symbolsOnCurrentLine = $this->processedFiles % $this->symbolsPerLine;
101-
$isLast = $this->processedFiles === $this->files;
90+
$isLast = $this->processedFiles === $this->context->getFilesCount();
10291

10392
if (0 === $symbolsOnCurrentLine || $isLast) {
104-
$this->output->write(sprintf(
105-
'%s %'.\strlen((string) $this->files).'d / %d (%3d%%)',
93+
$this->getOutput()->write(sprintf(
94+
'%s %'.\strlen((string) $this->context->getFilesCount()).'d / %d (%3d%%)',
10695
$isLast && 0 !== $symbolsOnCurrentLine ? str_repeat(' ', $this->symbolsPerLine - $symbolsOnCurrentLine) : '',
10796
$this->processedFiles,
108-
$this->files,
109-
round($this->processedFiles / $this->files * 100)
97+
$this->context->getFilesCount(),
98+
round($this->processedFiles / $this->context->getFilesCount() * 100)
11099
));
111100

112101
if (!$isLast) {
113-
$this->output->writeln('');
102+
$this->getOutput()->writeln('');
114103
}
115104
}
116105
}
@@ -125,9 +114,14 @@ public function printLegend(): void
125114
continue;
126115
}
127116

128-
$symbols[$symbol] = sprintf('%s-%s', $this->output->isDecorated() ? sprintf($status['format'], $symbol) : $symbol, $status['description']);
117+
$symbols[$symbol] = sprintf('%s-%s', $this->getOutput()->isDecorated() ? sprintf($status['format'], $symbol) : $symbol, $status['description']);
129118
}
130119

131-
$this->output->write(sprintf("\nLegend: %s\n", implode(', ', $symbols)));
120+
$this->getOutput()->write(sprintf("\nLegend: %s\n", implode(', ', $symbols)));
121+
}
122+
123+
private function getOutput(): OutputInterface
124+
{
125+
return $this->context->getOutput();
132126
}
133127
}

src/Console/Output/NullOutput.php renamed to src/Console/Output/Progress/NullOutput.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,20 @@
1212
* with this source code in the file LICENSE.
1313
*/
1414

15-
namespace PhpCsFixer\Console\Output;
15+
namespace PhpCsFixer\Console\Output\Progress;
16+
17+
use PhpCsFixer\FixerFileProcessedEvent;
1618

1719
/**
1820
* @internal
1921
*/
20-
final class NullOutput implements ProcessOutputInterface
22+
final class NullOutput implements ProgressOutputInterface
2123
{
2224
public function printLegend(): void
2325
{
2426
}
27+
28+
public function onFixerFileProcessed(FixerFileProcessedEvent $event): void
29+
{
30+
}
2531
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of PHP CS Fixer.
7+
*
8+
* (c) Fabien Potencier <fabien@symfony.com>
9+
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
10+
*
11+
* This source file is subject to the MIT license that is bundled
12+
* with this source code in the file LICENSE.
13+
*/
14+
15+
namespace PhpCsFixer\Console\Output\Progress;
16+
17+
use PhpCsFixer\Console\Output\OutputContext;
18+
19+
/**
20+
* @internal
21+
*/
22+
final class ProgressOutputFactory
23+
{
24+
public function create(string $outputType, OutputContext $context): ProgressOutputInterface
25+
{
26+
if (null === $context->getOutput()) {
27+
$outputType = ProgressOutputType::NONE;
28+
}
29+
30+
if (!$this->isBuiltInType($outputType)) {
31+
throw new \InvalidArgumentException(
32+
sprintf(
33+
'Something went wrong, "%s" output type is not supported',
34+
$outputType
35+
)
36+
);
37+
}
38+
39+
return ProgressOutputType::NONE === $outputType
40+
? new NullOutput()
41+
: new DotsOutput($context);
42+
}
43+
44+
private function isBuiltInType(string $outputType): bool
45+
{
46+
return \in_array($outputType, [
47+
ProgressOutputType::NONE,
48+
ProgressOutputType::DOTS,
49+
], true);
50+
}
51+
}

src/Console/Output/ProcessOutputInterface.php renamed to src/Console/Output/Progress/ProgressOutputInterface.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,16 @@
1212
* with this source code in the file LICENSE.
1313
*/
1414

15-
namespace PhpCsFixer\Console\Output;
15+
namespace PhpCsFixer\Console\Output\Progress;
16+
17+
use PhpCsFixer\FixerFileProcessedEvent;
1618

1719
/**
1820
* @internal
1921
*/
20-
interface ProcessOutputInterface
22+
interface ProgressOutputInterface
2123
{
2224
public function printLegend(): void;
25+
26+
public function onFixerFileProcessed(FixerFileProcessedEvent $event): void;
2327
}

0 commit comments

Comments
 (0)