Merge branch '8.0' into 8.1

* 8.0:
  [Serializer] Fix handling of constructor enum denormalization errors
  [Console] ProgressIndicator console helper display with multiple processes
  [HttpFoundation] Handle empty session data in updateTimestamp() to fix compat with PHP 8.6
  [Console] Fix arguments set via #[Ask] wrongly considered null in profiler
  [Cache] Wrap `DoctrineDbalAdapter::doSave()` in savepoint to prevent transaction poisoning
  Update security-1.0.xsd with missing oauth2 element
  [Console] Silence shell_exec warning in hasSttyAvailable
  [Validator] Sync validators.pt.xlf
  Minor: Review and finalize Latvian translations for validators
  streamline ini settings in phpunit.xml.dist files
  stop using with*() without expects()
  stop using with*() without expects()
  TypeContextFactory::collectTemplates now also works with @phpstan-template and @psalm-template
This commit is contained in:
Nicolas Grekas
2026-02-21 17:40:49 +01:00
5 changed files with 94 additions and 15 deletions

View File

@@ -286,8 +286,8 @@ final class TraceableCommand extends Command
{
$this->input = $input;
$this->output = $output;
$this->arguments = $input->getArguments();
$this->options = $input->getOptions();
$initialArguments = $input->getArguments();
$initialOptions = $input->getOptions();
$event = $this->stopwatch->start($this->getName(), 'command');
try {
@@ -302,9 +302,11 @@ final class TraceableCommand extends Command
$this->duration = $event->getDuration().' ms';
$this->maxMemoryUsage = ($event->getMemory() >> 20).' MiB';
if ($this->isInteractive) {
$this->extractInteractiveInputs($input->getArguments(), $input->getOptions());
}
$this->arguments = $input->getArguments();
$this->options = $input->getOptions();
$this->extractInteractiveInputs($initialArguments, $initialOptions);
$this->isInteractive = $this->isInteractive || $this->interactiveInputs;
}
return $this->exitCode;
@@ -343,22 +345,24 @@ final class TraceableCommand extends Command
return $exitCode;
}
private function extractInteractiveInputs(array $arguments, array $options): void
private function extractInteractiveInputs(array $initialArguments, array $initialOptions): void
{
foreach ($arguments as $argName => $argValue) {
if (\array_key_exists($argName, $this->arguments) && $this->arguments[$argName] === $argValue) {
$nativeDefinition = $this->command->getNativeDefinition();
foreach ($nativeDefinition->getArguments() as $argName => $argument) {
if (\array_key_exists($argName, $initialArguments) && $initialArguments[$argName] === $this->arguments[$argName]) {
continue;
}
$this->interactiveInputs[$argName] = $argValue;
$this->interactiveInputs[$argName] = $this->arguments[$argName];
}
foreach ($options as $optName => $optValue) {
if (\array_key_exists($optName, $this->options) && $this->options[$optName] === $optValue) {
foreach ($nativeDefinition->getOptions() as $optName => $option) {
if (\array_key_exists($optName, $initialOptions) && $initialOptions[$optName] === $this->options[$optName]) {
continue;
}
$this->interactiveInputs['--'.$optName] = $optValue;
$this->interactiveInputs['--'.$optName] = $this->options[$optName];
}
}
}

View File

@@ -13,6 +13,7 @@ namespace Symfony\Component\Console\Helper;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Output\ConsoleSectionOutput;
use Symfony\Component\Console\Output\OutputInterface;
/**
@@ -142,7 +143,9 @@ class ProgressIndicator
$this->finished = true;
$this->message = $message;
$this->display();
$this->output->writeln('');
if (!$this->output instanceof ConsoleSectionOutput) {
$this->output->writeln('');
}
$this->started = false;
}
@@ -207,7 +210,9 @@ class ProgressIndicator
*/
private function overwrite(string $message): void
{
if ($this->output->isDecorated()) {
if ($this->output instanceof ConsoleSectionOutput) {
$this->output->overwrite($message);
} elseif ($this->output->isDecorated()) {
$this->output->write("\x0D\x1B[2K");
$this->output->write($message);
} else {

View File

@@ -124,7 +124,7 @@ class Terminal
return false;
}
return self::$stty = (bool) shell_exec('stty 2> '.('\\' === \DIRECTORY_SEPARATOR ? 'NUL' : '/dev/null'));
return self::$stty = (bool) @shell_exec('stty 2> '.('\\' === \DIRECTORY_SEPARATOR ? 'NUL' : '/dev/null'));
}
public static function supportsKittyGraphics(): bool

View File

@@ -85,6 +85,22 @@ class TraceableCommandTest extends TestCase
self::assertStringContainsString('Hello World', $commandTester->getDisplay());
}
public function testArgumentsCaptureValueSetDuringInteract()
{
$this->application->addCommand(new InvokableWithAskCommand());
$command = $this->application->find('invokable:ask');
$traceableCommand = new TraceableCommand($command, new Stopwatch());
$commandTester = new CommandTester($traceableCommand);
$commandTester->setInputs(['Robin']);
$commandTester->execute([], ['interactive' => true]);
$commandTester->assertCommandIsSuccessful();
self::assertSame('Robin', $traceableCommand->arguments['name']);
self::assertTrue($traceableCommand->isInteractive);
self::assertSame(['name' => 'Robin'], $traceableCommand->interactiveInputs);
}
public function assertLoopOutputCorrectness(string $output)
{
$completeChar = '\\' !== \DIRECTORY_SEPARATOR ? '▓' : '=';

View File

@@ -14,7 +14,9 @@ namespace Symfony\Component\Console\Tests\Helper;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Helper\ProgressIndicator;
use Symfony\Component\Console\Output\ConsoleSectionOutput;
use Symfony\Component\Console\Output\StreamOutput;
#[Group('time-sensitive')]
@@ -201,6 +203,58 @@ class ProgressIndicatorTest extends TestCase
];
}
public function testWithConsoleSectionOutput()
{
$sections = [];
$stream = fopen('php://memory', 'r+', false);
$output = new ConsoleSectionOutput($stream, $sections, StreamOutput::VERBOSITY_NORMAL, true, new OutputFormatter());
$bar = new ProgressIndicator($output, null, 100, ['-', '\\', '|', '/']);
$bar->start('Starting...');
usleep(101000);
$bar->advance();
$bar->finish('Done...');
rewind($stream);
$content = stream_get_contents($stream);
// Must not use raw ANSI line-clear sequences — those corrupt ConsoleSectionOutput's internal line tracking
$this->assertStringNotContainsString("\x0D\x1B[2K", $content);
// finish() must not add an extra trailing newline — ConsoleSectionOutput::overwrite() already ends with writeln()
$this->assertStringEndsWith(' \\ Done...'.\PHP_EOL, $content);
}
public function testMultipleSectionsWithProgressIndicators()
{
$sections = [];
$stream = fopen('php://memory', 'r+', false);
$formatter = new OutputFormatter();
$section1 = new ConsoleSectionOutput($stream, $sections, StreamOutput::VERBOSITY_NORMAL, true, $formatter);
$section2 = new ConsoleSectionOutput($stream, $sections, StreamOutput::VERBOSITY_NORMAL, true, $formatter);
$bar1 = new ProgressIndicator($section1, null, 100, ['-', '\\', '|', '/']);
$bar2 = new ProgressIndicator($section2, null, 100, ['-', '\\', '|', '/']);
$bar1->start('Project 1...');
$bar2->start('Project 2...');
usleep(101000);
$bar1->advance();
$bar2->advance();
$bar1->finish('Project 1 Done.');
$bar2->finish('Project 2 Done.');
rewind($stream);
$content = stream_get_contents($stream);
// Must not use raw ANSI line-clear sequences
$this->assertStringNotContainsString("\x0D\x1B[2K", $content);
// Both finished messages must appear in the output
$this->assertStringContainsString('Project 1 Done.', $content);
$this->assertStringContainsString('Project 2 Done.', $content);
}
protected function getOutputStream($decorated = true, $verbosity = StreamOutput::VERBOSITY_NORMAL)
{
return new StreamOutput(fopen('php://memory', 'r+', false), $verbosity, $decorated);