diff --git a/Formatter/OutputFormatter.php b/Formatter/OutputFormatter.php index edc6a1d71..4ad248868 100644 --- a/Formatter/OutputFormatter.php +++ b/Formatter/OutputFormatter.php @@ -25,6 +25,14 @@ class OutputFormatter implements WrappableOutputFormatterInterface private $styles = []; private $styleStack; + public function __clone() + { + $this->styleStack = clone $this->styleStack; + foreach ($this->styles as $key => $value) { + $this->styles[$key] = clone $value; + } + } + /** * Escapes "<" special char in given text. * diff --git a/Helper/Table.php b/Helper/Table.php index fee5a416b..21848e65d 100644 --- a/Helper/Table.php +++ b/Helper/Table.php @@ -418,13 +418,13 @@ private function renderRowSeparator(int $type = self::SEPARATOR_MID, string $tit $crossings = $this->style->getCrossingChars(); if (self::SEPARATOR_MID === $type) { - list($horizontal, $leftChar, $midChar, $rightChar) = [$borders[2], $crossings[8], $crossings[0], $crossings[4]]; + [$horizontal, $leftChar, $midChar, $rightChar] = [$borders[2], $crossings[8], $crossings[0], $crossings[4]]; } elseif (self::SEPARATOR_TOP === $type) { - list($horizontal, $leftChar, $midChar, $rightChar) = [$borders[0], $crossings[1], $crossings[2], $crossings[3]]; + [$horizontal, $leftChar, $midChar, $rightChar] = [$borders[0], $crossings[1], $crossings[2], $crossings[3]]; } elseif (self::SEPARATOR_TOP_BOTTOM === $type) { - list($horizontal, $leftChar, $midChar, $rightChar) = [$borders[0], $crossings[9], $crossings[10], $crossings[11]]; + [$horizontal, $leftChar, $midChar, $rightChar] = [$borders[0], $crossings[9], $crossings[10], $crossings[11]]; } else { - list($horizontal, $leftChar, $midChar, $rightChar) = [$borders[0], $crossings[7], $crossings[6], $crossings[5]]; + [$horizontal, $leftChar, $midChar, $rightChar] = [$borders[0], $crossings[7], $crossings[6], $crossings[5]]; } $markup = $leftChar; diff --git a/Output/ConsoleOutput.php b/Output/ConsoleOutput.php index 96664f150..2cda213a0 100644 --- a/Output/ConsoleOutput.php +++ b/Output/ConsoleOutput.php @@ -41,6 +41,13 @@ public function __construct(int $verbosity = self::VERBOSITY_NORMAL, bool $decor { parent::__construct($this->openOutputStream(), $verbosity, $decorated, $formatter); + if (null === $formatter) { + // for BC reasons, stdErr has it own Formatter only when user don't inject a specific formatter. + $this->stderr = new StreamOutput($this->openErrorStream(), $verbosity, $decorated); + + return; + } + $actualDecorated = $this->isDecorated(); $this->stderr = new StreamOutput($this->openErrorStream(), $verbosity, $decorated, $this->getFormatter()); diff --git a/Output/TrimmedBufferOutput.php b/Output/TrimmedBufferOutput.php new file mode 100644 index 000000000..a03aa835f --- /dev/null +++ b/Output/TrimmedBufferOutput.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Output; + +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Formatter\OutputFormatterInterface; + +/** + * A BufferedOutput that keeps only the last N chars. + * + * @author Jérémy Derussé + */ +class TrimmedBufferOutput extends Output +{ + private $maxLength; + private $buffer = ''; + + public function __construct( + int $maxLength, + ?int $verbosity = self::VERBOSITY_NORMAL, + bool $decorated = false, + OutputFormatterInterface $formatter = null + ) { + if ($maxLength <= 0) { + throw new InvalidArgumentException(sprintf('"%s()" expects a strictly positive maxLength. Got %d.', __METHOD__, $maxLength)); + } + + parent::__construct($verbosity, $decorated, $formatter); + $this->maxLength = $maxLength; + } + + /** + * Empties buffer and returns its content. + * + * @return string + */ + public function fetch() + { + $content = $this->buffer; + $this->buffer = ''; + + return $content; + } + + /** + * {@inheritdoc} + */ + protected function doWrite($message, $newline) + { + $this->buffer .= $message; + + if ($newline) { + $this->buffer .= \PHP_EOL; + } + + $this->buffer = substr($this->buffer, 0 - $this->maxLength); + } +} diff --git a/Style/SymfonyStyle.php b/Style/SymfonyStyle.php index fa4dfa018..eb455cda2 100644 --- a/Style/SymfonyStyle.php +++ b/Style/SymfonyStyle.php @@ -21,8 +21,8 @@ use Symfony\Component\Console\Helper\TableCell; use Symfony\Component\Console\Helper\TableSeparator; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Output\TrimmedBufferOutput; use Symfony\Component\Console\Question\ChoiceQuestion; use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Question\Question; @@ -46,7 +46,7 @@ class SymfonyStyle extends OutputStyle public function __construct(InputInterface $input, OutputInterface $output) { $this->input = $input; - $this->bufferedOutput = new BufferedOutput($output->getVerbosity(), false, clone $output->getFormatter()); + $this->bufferedOutput = new TrimmedBufferOutput(\DIRECTORY_SEPARATOR === '\\' ? 4 : 2, $output->getVerbosity(), false, clone $output->getFormatter()); // Windows cmd wraps lines as soon as the terminal width is reached, whether there are following chars or not. $width = (new Terminal())->getWidth() ?: self::MAX_LINE_LENGTH; $this->lineLength = min($width - (int) (\DIRECTORY_SEPARATOR === '\\'), self::MAX_LINE_LENGTH); @@ -444,9 +444,8 @@ private function autoPrependText(): void private function writeBuffer(string $message, bool $newLine, int $type): void { - // We need to know if the two last chars are PHP_EOL - // Preserve the last 4 chars inserted (PHP_EOL on windows is two chars) in the history buffer - $this->bufferedOutput->write(substr($message, -4), $newLine, $type); + // We need to know if the last chars are PHP_EOL + $this->bufferedOutput->write($message, $newLine, $type); } private function createBlock(iterable $messages, string $type = null, string $style = null, string $prefix = ' ', bool $padding = false, bool $escape = false): array diff --git a/Tests/Fixtures/Style/SymfonyStyle/command/command_20.php b/Tests/Fixtures/Style/SymfonyStyle/command/command_20.php new file mode 100644 index 000000000..6b47969ee --- /dev/null +++ b/Tests/Fixtures/Style/SymfonyStyle/command/command_20.php @@ -0,0 +1,13 @@ +setDecorated(true); + $output = new SymfonyStyle($input, $output); + $output->write('do you want something'); + $output->writeln('?'); +}; diff --git a/Tests/Fixtures/Style/SymfonyStyle/output/output_20.txt b/Tests/Fixtures/Style/SymfonyStyle/output/output_20.txt new file mode 100644 index 000000000..c08298530 --- /dev/null +++ b/Tests/Fixtures/Style/SymfonyStyle/output/output_20.txt @@ -0,0 +1 @@ +do you want something? diff --git a/Tests/Output/ConsoleOutputTest.php b/Tests/Output/ConsoleOutputTest.php index db39a02b8..33a5371d6 100644 --- a/Tests/Output/ConsoleOutputTest.php +++ b/Tests/Output/ConsoleOutputTest.php @@ -18,11 +18,19 @@ class ConsoleOutputTest extends TestCase { - public function testConstructor() + public function testConstructorWithoutFormatter() { $output = new ConsoleOutput(Output::VERBOSITY_QUIET, true); $this->assertEquals(Output::VERBOSITY_QUIET, $output->getVerbosity(), '__construct() takes the verbosity as its first argument'); - $this->assertSame($output->getFormatter(), $output->getErrorOutput()->getFormatter(), '__construct() takes a formatter or null as the third argument'); + $this->assertNotSame($output->getFormatter(), $output->getErrorOutput()->getFormatter(), 'ErrorOutput should use it own formatter'); + } + + public function testConstructorWithFormatter() + { + $output = new ConsoleOutput(Output::VERBOSITY_QUIET, true, $formatter = new OutputFormatter()); + $this->assertEquals(Output::VERBOSITY_QUIET, $output->getVerbosity(), '__construct() takes the verbosity as its first argument'); + $this->assertSame($formatter, $output->getFormatter()); + $this->assertSame($formatter, $output->getErrorOutput()->getFormatter(), 'Output and ErrorOutput should use the same provided formatter'); } public function testSetFormatter() @@ -31,6 +39,7 @@ public function testSetFormatter() $outputFormatter = new OutputFormatter(); $output->setFormatter($outputFormatter); $this->assertSame($outputFormatter, $output->getFormatter()); + $this->assertSame($outputFormatter, $output->getErrorOutput()->getFormatter()); } public function testSetVerbosity() diff --git a/Tests/Style/SymfonyStyleTest.php b/Tests/Style/SymfonyStyleTest.php index 943b94172..16bb2baec 100644 --- a/Tests/Style/SymfonyStyleTest.php +++ b/Tests/Style/SymfonyStyleTest.php @@ -14,8 +14,10 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Formatter\OutputFormatter; +use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\NullOutput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Console\Tester\CommandTester; @@ -115,4 +117,18 @@ public function testGetErrorStyleUsesTheCurrentOutputIfNoErrorOutputIsAvailable( $this->assertInstanceOf(SymfonyStyle::class, $style->getErrorStyle()); } + + public function testMemoryConsumption() + { + $io = new SymfonyStyle(new ArrayInput([]), new NullOutput()); + $str = 'teststr'; + $io->writeln($str, SymfonyStyle::VERBOSITY_QUIET); + $io->writeln($str, SymfonyStyle::VERBOSITY_QUIET); + $start = memory_get_usage(); + for ($i = 0; $i < 100; ++$i) { + $io->writeln($str, SymfonyStyle::VERBOSITY_QUIET); + } + + $this->assertSame(0, memory_get_usage() - $start); + } }