Dep: update

주로 PHAN
This commit is contained in:
2021-08-06 22:39:09 +09:00
parent 5310c2e7f6
commit b306601c72
1098 changed files with 92137 additions and 33228 deletions
+97 -33
View File
@@ -13,15 +13,19 @@ namespace Symfony\Component\Console;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Command\HelpCommand;
use Symfony\Component\Console\Command\LazyCommand;
use Symfony\Component\Console\Command\ListCommand;
use Symfony\Component\Console\Command\SignalableCommandInterface;
use Symfony\Component\Console\CommandLoader\CommandLoaderInterface;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\Console\Event\ConsoleErrorEvent;
use Symfony\Component\Console\Event\ConsoleSignalEvent;
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
use Symfony\Component\Console\Exception\CommandNotFoundException;
use Symfony\Component\Console\Exception\ExceptionInterface;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Exception\NamespaceNotFoundException;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Helper\DebugFormatterHelper;
use Symfony\Component\Console\Helper\FormatterHelper;
@@ -39,6 +43,7 @@ use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\SignalRegistry\SignalRegistry;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\ErrorHandler\ErrorHandler;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
@@ -76,6 +81,8 @@ class Application implements ResetInterface
private $defaultCommand;
private $singleCommand = false;
private $initialized;
private $signalRegistry;
private $signalsToDispatchEvent = [];
public function __construct(string $name = 'UNKNOWN', string $version = 'UNKNOWN')
{
@@ -83,6 +90,10 @@ class Application implements ResetInterface
$this->version = $version;
$this->terminal = new Terminal();
$this->defaultCommand = 'list';
if (\defined('SIGINT') && SignalRegistry::isSupported()) {
$this->signalRegistry = new SignalRegistry();
$this->signalsToDispatchEvent = [\SIGINT, \SIGTERM, \SIGUSR1, \SIGUSR2];
}
}
/**
@@ -98,6 +109,20 @@ class Application implements ResetInterface
$this->commandLoader = $commandLoader;
}
public function getSignalRegistry(): SignalRegistry
{
if (!$this->signalRegistry) {
throw new RuntimeException('Signals are not supported. Make sure that the `pcntl` extension is installed and that "pcntl_*" functions are not disabled by your php.ini\'s "disable_functions" directive.');
}
return $this->signalRegistry;
}
public function setSignalsToDispatchEvent(int ...$signalsToDispatchEvent)
{
$this->signalsToDispatchEvent = $signalsToDispatchEvent;
}
/**
* Runs the current application.
*
@@ -107,8 +132,10 @@ class Application implements ResetInterface
*/
public function run(InputInterface $input = null, OutputInterface $output = null)
{
putenv('LINES='.$this->terminal->getHeight());
putenv('COLUMNS='.$this->terminal->getWidth());
if (\function_exists('putenv')) {
@putenv('LINES='.$this->terminal->getHeight());
@putenv('COLUMNS='.$this->terminal->getWidth());
}
if (null === $input) {
$input = new ArgvInput();
@@ -260,6 +287,10 @@ class Application implements ResetInterface
$command = $this->find($alternative);
}
if ($command instanceof LazyCommand) {
$command = $command->getCommand();
}
$this->runningCommand = $command;
$exitCode = $this->doRunCommand($command, $input, $output);
$this->runningCommand = null;
@@ -463,11 +494,13 @@ class Application implements ResetInterface
return null;
}
// Will throw if the command is not correctly initialized.
$command->getDefinition();
if (!$command instanceof LazyCommand) {
// Will throw if the command is not correctly initialized.
$command->getDefinition();
}
if (!$command->getName()) {
throw new LogicException(sprintf('The command defined in "%s" cannot have an empty name.', \get_class($command)));
throw new LogicException(sprintf('The command defined in "%s" cannot have an empty name.', get_debug_type($command)));
}
$this->commands[$command->getName()] = $command;
@@ -494,6 +527,11 @@ class Application implements ResetInterface
throw new CommandNotFoundException(sprintf('The command "%s" does not exist.', $name));
}
// When the command has a different name than the one used at the command loader level
if (!isset($this->commands[$name])) {
throw new CommandNotFoundException(sprintf('The "%s" command cannot be found because it is registered under multiple names. Make sure you don\'t set a different name via constructor or "setName()".', $name));
}
$command = $this->commands[$name];
if ($this->wantHelps) {
@@ -555,7 +593,7 @@ class Application implements ResetInterface
public function findNamespace(string $namespace)
{
$allNamespaces = $this->getNamespaces();
$expr = preg_replace_callback('{([^:]+|)}', function ($matches) { return preg_quote($matches[1]).'[^:]*'; }, $namespace);
$expr = implode('[^:]*:', array_map('preg_quote', explode(':', $namespace))).'[^:]*';
$namespaces = preg_grep('{^'.$expr.'}', $allNamespaces);
if (empty($namespaces)) {
@@ -611,7 +649,7 @@ class Application implements ResetInterface
}
$allCommands = $this->commandLoader ? array_merge($this->commandLoader->getNames(), array_keys($this->commands)) : array_keys($this->commands);
$expr = preg_replace_callback('{([^:]+|)}', function ($matches) { return preg_quote($matches[1]).'[^:]*'; }, $name);
$expr = implode('[^:]*:', array_map('preg_quote', explode(':', $name))).'[^:]*';
$commands = preg_grep('{^'.$expr.'}', $allCommands);
if (empty($commands)) {
@@ -665,7 +703,7 @@ class Application implements ResetInterface
$abbrevs = array_values($commands);
$maxLen = 0;
foreach ($abbrevs as $abbrev) {
$maxLen = max(Helper::strlen($abbrev), $maxLen);
$maxLen = max(Helper::width($abbrev), $maxLen);
}
$abbrevs = array_map(function ($cmd) use ($commandList, $usableWidth, $maxLen, &$commands) {
if ($commandList[$cmd]->isHidden()) {
@@ -676,7 +714,7 @@ class Application implements ResetInterface
$abbrev = str_pad($cmd, $maxLen, ' ').' '.$commandList[$cmd]->getDescription();
return Helper::strlen($abbrev) > $usableWidth ? Helper::substr($abbrev, 0, $usableWidth - 3).'...' : $abbrev;
return Helper::width($abbrev) > $usableWidth ? Helper::substr($abbrev, 0, $usableWidth - 3).'...' : $abbrev;
}, array_values($commands));
if (\count($commands) > 1) {
@@ -764,7 +802,7 @@ class Application implements ResetInterface
$this->doRenderThrowable($e, $output);
if (null !== $this->runningCommand) {
$output->writeln(sprintf('<info>%s</info>', sprintf($this->runningCommand->getSynopsis(), $this->getName())), OutputInterface::VERBOSITY_QUIET);
$output->writeln(sprintf('<info>%s</info>', OutputFormatter::escape(sprintf($this->runningCommand->getSynopsis(), $this->getName()))), OutputInterface::VERBOSITY_QUIET);
$output->writeln('', OutputInterface::VERBOSITY_QUIET);
}
}
@@ -774,26 +812,25 @@ class Application implements ResetInterface
do {
$message = trim($e->getMessage());
if ('' === $message || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
$class = \get_class($e);
$class = 'c' === $class[0] && 0 === strpos($class, "class@anonymous\0") ? get_parent_class($class).'@anonymous' : $class;
$class = get_debug_type($e);
$title = sprintf(' [%s%s] ', $class, 0 !== ($code = $e->getCode()) ? ' ('.$code.')' : '');
$len = Helper::strlen($title);
$len = Helper::width($title);
} else {
$len = 0;
}
if (false !== strpos($message, "class@anonymous\0")) {
$message = preg_replace_callback('/class@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', function ($m) {
return class_exists($m[0], false) ? get_parent_class($m[0]).'@anonymous' : $m[0];
if (str_contains($message, "@anonymous\0")) {
$message = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', function ($m) {
return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0];
}, $message);
}
$width = $this->terminal->getWidth() ? $this->terminal->getWidth() - 1 : PHP_INT_MAX;
$width = $this->terminal->getWidth() ? $this->terminal->getWidth() - 1 : \PHP_INT_MAX;
$lines = [];
foreach ('' !== $message ? preg_split('/\r?\n/', $message) : [] as $line) {
foreach ($this->splitStringByWidth($line, $width - 4) as $line) {
// pre-format lines to get the right string length
$lineLength = Helper::strlen($line) + 4;
$lineLength = Helper::width($line) + 4;
$lines[] = [$line, $lineLength];
$len = max($lineLength, $len);
@@ -806,7 +843,7 @@ class Application implements ResetInterface
}
$messages[] = $emptyLine = sprintf('<error>%s</error>', str_repeat(' ', $len));
if ('' === $message || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
$messages[] = sprintf('<error>%s%s</error>', $title, str_repeat(' ', max(0, $len - Helper::strlen($title))));
$messages[] = sprintf('<error>%s%s</error>', $title, str_repeat(' ', max(0, $len - Helper::width($title))));
}
foreach ($lines as $line) {
$messages[] = sprintf('<error> %s %s</error>', OutputFormatter::escape($line[0]), str_repeat(' ', $len - $line[1]));
@@ -830,11 +867,11 @@ class Application implements ResetInterface
]);
for ($i = 0, $count = \count($trace); $i < $count; ++$i) {
$class = isset($trace[$i]['class']) ? $trace[$i]['class'] : '';
$type = isset($trace[$i]['type']) ? $trace[$i]['type'] : '';
$function = isset($trace[$i]['function']) ? $trace[$i]['function'] : '';
$file = isset($trace[$i]['file']) ? $trace[$i]['file'] : 'n/a';
$line = isset($trace[$i]['line']) ? $trace[$i]['line'] : 'n/a';
$class = $trace[$i]['class'] ?? '';
$type = $trace[$i]['type'] ?? '';
$function = $trace[$i]['function'] ?? '';
$file = $trace[$i]['file'] ?? 'n/a';
$line = $trace[$i]['line'] ?? 'n/a';
$output->writeln(sprintf(' %s%s at <info>%s:%s</info>', $class, $function ? $type.$function.'()' : '', $file, $line), OutputInterface::VERBOSITY_QUIET);
}
@@ -887,7 +924,9 @@ class Application implements ResetInterface
$input->setInteractive(false);
}
putenv('SHELL_VERBOSITY='.$shellVerbosity);
if (\function_exists('putenv')) {
@putenv('SHELL_VERBOSITY='.$shellVerbosity);
}
$_ENV['SHELL_VERBOSITY'] = $shellVerbosity;
$_SERVER['SHELL_VERBOSITY'] = $shellVerbosity;
}
@@ -908,6 +947,33 @@ class Application implements ResetInterface
}
}
if ($command instanceof SignalableCommandInterface && ($this->signalsToDispatchEvent || $command->getSubscribedSignals())) {
if (!$this->signalRegistry) {
throw new RuntimeException('Unable to subscribe to signal events. Make sure that the `pcntl` extension is installed and that "pcntl_*" functions are not disabled by your php.ini\'s "disable_functions" directive.');
}
if ($this->dispatcher) {
foreach ($this->signalsToDispatchEvent as $signal) {
$event = new ConsoleSignalEvent($command, $input, $output, $signal);
$this->signalRegistry->register($signal, function ($signal, $hasNext) use ($event) {
$this->dispatcher->dispatch($event, ConsoleEvents::SIGNAL);
// No more handlers, we try to simulate PHP default behavior
if (!$hasNext) {
if (!\in_array($signal, [\SIGUSR1, \SIGUSR2], true)) {
exit(0);
}
}
});
}
}
foreach ($command->getSubscribedSignals() as $signal) {
$this->signalRegistry->register($signal, [$command, 'handleSignal']);
}
}
if (null === $this->dispatcher) {
return $command->run($input, $output);
}
@@ -970,13 +1036,11 @@ class Application implements ResetInterface
{
return new InputDefinition([
new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'),
new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display this help message'),
new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display help for the given command. When no command is given display help for the <info>'.$this->defaultCommand.'</info> command'),
new InputOption('--quiet', '-q', InputOption::VALUE_NONE, 'Do not output any message'),
new InputOption('--verbose', '-v|vv|vvv', InputOption::VALUE_NONE, 'Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug'),
new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this application version'),
new InputOption('--ansi', '', InputOption::VALUE_NONE, 'Force ANSI output'),
new InputOption('--no-ansi', '', InputOption::VALUE_NONE, 'Disable ANSI output'),
new InputOption('--ansi', '', InputOption::VALUE_NEGATABLE, 'Force (or disable --no-ansi) ANSI output', false),
new InputOption('--no-interaction', '-n', InputOption::VALUE_NONE, 'Do not ask any interactive question'),
]);
}
@@ -1055,7 +1119,7 @@ class Application implements ResetInterface
}
$lev = levenshtein($subname, $parts[$i]);
if ($lev <= \strlen($subname) / 3 || '' !== $subname && false !== strpos($parts[$i], $subname)) {
if ($lev <= \strlen($subname) / 3 || '' !== $subname && str_contains($parts[$i], $subname)) {
$alternatives[$collectionName] = $exists ? $alternatives[$collectionName] + $lev : $lev;
} elseif ($exists) {
$alternatives[$collectionName] += $threshold;
@@ -1065,13 +1129,13 @@ class Application implements ResetInterface
foreach ($collection as $item) {
$lev = levenshtein($name, $item);
if ($lev <= \strlen($name) / 3 || false !== strpos($item, $name)) {
if ($lev <= \strlen($name) / 3 || str_contains($item, $name)) {
$alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev;
}
}
$alternatives = array_filter($alternatives, function ($lev) use ($threshold) { return $lev < 2 * $threshold; });
ksort($alternatives, SORT_NATURAL | SORT_FLAG_CASE);
ksort($alternatives, \SORT_NATURAL | \SORT_FLAG_CASE);
return array_keys($alternatives);
}
@@ -1083,7 +1147,7 @@ class Application implements ResetInterface
*/
public function setDefaultCommand(string $commandName, bool $isSingleCommand = false)
{
$this->defaultCommand = $commandName;
$this->defaultCommand = explode('|', ltrim($commandName, '|'))[0];
if ($isSingleCommand) {
// Ensure the command exist
+39
View File
@@ -0,0 +1,39 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Attribute;
/**
* Service tag to autoconfigure commands.
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class AsCommand
{
public function __construct(
public string $name,
public ?string $description = null,
array $aliases = [],
bool $hidden = false,
) {
if (!$hidden && !$aliases) {
return;
}
$name = explode('|', $name);
$name = array_merge($name, $aliases);
if ($hidden && '' !== $name[0]) {
array_unshift($name, '');
}
$this->name = implode('|', $name);
}
}
+48 -16
View File
@@ -1,6 +1,38 @@
CHANGELOG
=========
5.3
---
* Add `GithubActionReporter` to render annotations in a Github Action
* Add `InputOption::VALUE_NEGATABLE` flag to handle `--foo`/`--no-foo` options
* Add the `Command::$defaultDescription` static property and the `description` attribute
on the `console.command` tag to allow the `list` command to instantiate commands lazily
* Add option `--short` to the `list` command
* Add support for bright colors
* Add `#[AsCommand]` attribute for declaring commands on PHP 8
* Add `Helper::width()` and `Helper::length()`
5.2.0
-----
* Added `SingleCommandApplication::setAutoExit()` to allow testing via `CommandTester`
* added support for multiline responses to questions through `Question::setMultiline()`
and `Question::isMultiline()`
* Added `SignalRegistry` class to stack signals handlers
* Added support for signals:
* Added `Application::getSignalRegistry()` and `Application::setSignalsToDispatchEvent()` methods
* Added `SignalableCommandInterface` interface
* Added `TableCellStyle` class to customize table cell
* Removed `php ` prefix invocation from help messages.
5.1.0
-----
* `Command::setHidden()` is final since Symfony 5.1
* Add `SingleCommandApplication`
* Add `Cursor` class
5.0.0
-----
@@ -63,9 +95,9 @@ CHANGELOG
* `OutputFormatter` throws an exception when unknown options are used
* removed `QuestionHelper::setInputStream()/getInputStream()`
* removed `Application::getTerminalWidth()/getTerminalHeight()` and
`Application::setTerminalDimensions()/getTerminalDimensions()`
* removed `ConsoleExceptionEvent`
* removed `ConsoleEvents::EXCEPTION`
`Application::setTerminalDimensions()/getTerminalDimensions()`
* removed `ConsoleExceptionEvent`
* removed `ConsoleEvents::EXCEPTION`
3.4.0
-----
@@ -82,23 +114,23 @@ CHANGELOG
3.3.0
-----
* added `ExceptionListener`
* added `AddConsoleCommandPass` (originally in FrameworkBundle)
* [BC BREAK] `Input::getOption()` no longer returns the default value for options
with value optional explicitly passed empty
* added console.error event to catch exceptions thrown by other listeners
* deprecated console.exception event in favor of console.error
* added ability to handle `CommandNotFoundException` through the
`console.error` event
* deprecated default validation in `SymfonyQuestionHelper::ask`
* added `ExceptionListener`
* added `AddConsoleCommandPass` (originally in FrameworkBundle)
* [BC BREAK] `Input::getOption()` no longer returns the default value for options
with value optional explicitly passed empty
* added console.error event to catch exceptions thrown by other listeners
* deprecated console.exception event in favor of console.error
* added ability to handle `CommandNotFoundException` through the
`console.error` event
* deprecated default validation in `SymfonyQuestionHelper::ask`
3.2.0
------
* added `setInputs()` method to CommandTester for ease testing of commands expecting inputs
* added `setStream()` and `getStream()` methods to Input (implement StreamableInputInterface)
* added StreamableInputInterface
* added LockableTrait
* added `setInputs()` method to CommandTester for ease testing of commands expecting inputs
* added `setStream()` and `getStream()` methods to Input (implement StreamableInputInterface)
* added StreamableInputInterface
* added LockableTrait
3.1.0
-----
+99
View File
@@ -0,0 +1,99 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\CI;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Utility class for Github actions.
*
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
class GithubActionReporter
{
private $output;
/**
* @see https://github.com/actions/toolkit/blob/5e5e1b7aacba68a53836a34db4a288c3c1c1585b/packages/core/src/command.ts#L80-L85
*/
private const ESCAPED_DATA = [
'%' => '%25',
"\r" => '%0D',
"\n" => '%0A',
];
/**
* @see https://github.com/actions/toolkit/blob/5e5e1b7aacba68a53836a34db4a288c3c1c1585b/packages/core/src/command.ts#L87-L94
*/
private const ESCAPED_PROPERTIES = [
'%' => '%25',
"\r" => '%0D',
"\n" => '%0A',
':' => '%3A',
',' => '%2C',
];
public function __construct(OutputInterface $output)
{
$this->output = $output;
}
public static function isGithubActionEnvironment(): bool
{
return false !== getenv('GITHUB_ACTIONS');
}
/**
* Output an error using the Github annotations format.
*
* @see https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-an-error-message
*/
public function error(string $message, string $file = null, int $line = null, int $col = null): void
{
$this->log('error', $message, $file, $line, $col);
}
/**
* Output a warning using the Github annotations format.
*
* @see https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-a-warning-message
*/
public function warning(string $message, string $file = null, int $line = null, int $col = null): void
{
$this->log('warning', $message, $file, $line, $col);
}
/**
* Output a debug log using the Github annotations format.
*
* @see https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-a-debug-message
*/
public function debug(string $message, string $file = null, int $line = null, int $col = null): void
{
$this->log('debug', $message, $file, $line, $col);
}
private function log(string $type, string $message, string $file = null, int $line = null, int $col = null): void
{
// Some values must be encoded.
$message = strtr($message, self::ESCAPED_DATA);
if (!$file) {
// No file provided, output the message solely:
$this->output->writeln(sprintf('::%s::%s', $type, $message));
return;
}
$this->output->writeln(sprintf('::%s file=%s,line=%s,col=%s::%s', $type, strtr($file, self::ESCAPED_PROPERTIES), strtr($line ?? 1, self::ESCAPED_PROPERTIES), strtr($col ?? 0, self::ESCAPED_PROPERTIES), $message));
}
}
+180
View File
@@ -0,0 +1,180 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console;
use Symfony\Component\Console\Exception\InvalidArgumentException;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
final class Color
{
private const COLORS = [
'black' => 0,
'red' => 1,
'green' => 2,
'yellow' => 3,
'blue' => 4,
'magenta' => 5,
'cyan' => 6,
'white' => 7,
'default' => 9,
];
private const BRIGHT_COLORS = [
'gray' => 0,
'bright-red' => 1,
'bright-green' => 2,
'bright-yellow' => 3,
'bright-blue' => 4,
'bright-magenta' => 5,
'bright-cyan' => 6,
'bright-white' => 7,
];
private const AVAILABLE_OPTIONS = [
'bold' => ['set' => 1, 'unset' => 22],
'underscore' => ['set' => 4, 'unset' => 24],
'blink' => ['set' => 5, 'unset' => 25],
'reverse' => ['set' => 7, 'unset' => 27],
'conceal' => ['set' => 8, 'unset' => 28],
];
private $foreground;
private $background;
private $options = [];
public function __construct(string $foreground = '', string $background = '', array $options = [])
{
$this->foreground = $this->parseColor($foreground);
$this->background = $this->parseColor($background, true);
foreach ($options as $option) {
if (!isset(self::AVAILABLE_OPTIONS[$option])) {
throw new InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s).', $option, implode(', ', array_keys(self::AVAILABLE_OPTIONS))));
}
$this->options[$option] = self::AVAILABLE_OPTIONS[$option];
}
}
public function apply(string $text): string
{
return $this->set().$text.$this->unset();
}
public function set(): string
{
$setCodes = [];
if ('' !== $this->foreground) {
$setCodes[] = $this->foreground;
}
if ('' !== $this->background) {
$setCodes[] = $this->background;
}
foreach ($this->options as $option) {
$setCodes[] = $option['set'];
}
if (0 === \count($setCodes)) {
return '';
}
return sprintf("\033[%sm", implode(';', $setCodes));
}
public function unset(): string
{
$unsetCodes = [];
if ('' !== $this->foreground) {
$unsetCodes[] = 39;
}
if ('' !== $this->background) {
$unsetCodes[] = 49;
}
foreach ($this->options as $option) {
$unsetCodes[] = $option['unset'];
}
if (0 === \count($unsetCodes)) {
return '';
}
return sprintf("\033[%sm", implode(';', $unsetCodes));
}
private function parseColor(string $color, bool $background = false): string
{
if ('' === $color) {
return '';
}
if ('#' === $color[0]) {
$color = substr($color, 1);
if (3 === \strlen($color)) {
$color = $color[0].$color[0].$color[1].$color[1].$color[2].$color[2];
}
if (6 !== \strlen($color)) {
throw new InvalidArgumentException(sprintf('Invalid "%s" color.', $color));
}
return ($background ? '4' : '3').$this->convertHexColorToAnsi(hexdec($color));
}
if (isset(self::COLORS[$color])) {
return ($background ? '4' : '3').self::COLORS[$color];
}
if (isset(self::BRIGHT_COLORS[$color])) {
return ($background ? '10' : '9').self::BRIGHT_COLORS[$color];
}
throw new InvalidArgumentException(sprintf('Invalid "%s" color; expected one of (%s).', $color, implode(', ', array_merge(array_keys(self::COLORS), array_keys(self::BRIGHT_COLORS)))));
}
private function convertHexColorToAnsi(int $color): string
{
$r = ($color >> 16) & 255;
$g = ($color >> 8) & 255;
$b = $color & 255;
// see https://github.com/termstandard/colors/ for more information about true color support
if ('truecolor' !== getenv('COLORTERM')) {
return (string) $this->degradeHexColorToAnsi($r, $g, $b);
}
return sprintf('8;2;%d;%d;%d', $r, $g, $b);
}
private function degradeHexColorToAnsi(int $r, int $g, int $b): int
{
if (0 === round($this->getSaturation($r, $g, $b) / 50)) {
return 0;
}
return (round($b / 255) << 2) | (round($g / 255) << 1) | round($r / 255);
}
private function getSaturation(int $r, int $g, int $b): int
{
$r = $r / 255;
$g = $g / 255;
$b = $b / 255;
$v = max($r, $g, $b);
if (0 === $diff = $v - min($r, $g, $b)) {
return 0;
}
return (int) $diff * 100 / $v;
}
}
+100 -38
View File
@@ -12,6 +12,7 @@
namespace Symfony\Component\Console\Command;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Exception\ExceptionInterface;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;
@@ -29,11 +30,21 @@ use Symfony\Component\Console\Output\OutputInterface;
*/
class Command
{
// see https://tldp.org/LDP/abs/html/exitcodes.html
public const SUCCESS = 0;
public const FAILURE = 1;
public const INVALID = 2;
/**
* @var string|null The default command name
*/
protected static $defaultName;
/**
* @var string|null The default command description
*/
protected static $defaultDescription;
private $application;
private $name;
private $processTitle;
@@ -42,9 +53,8 @@ class Command
private $hidden = false;
private $help = '';
private $description = '';
private $fullDefinition;
private $ignoreValidationErrors = false;
private $applicationDefinitionMerged = false;
private $applicationDefinitionMergedWithArgs = false;
private $code;
private $synopsis = [];
private $usages = [];
@@ -56,11 +66,32 @@ class Command
public static function getDefaultName()
{
$class = static::class;
if (\PHP_VERSION_ID >= 80000 && $attribute = (new \ReflectionClass($class))->getAttributes(AsCommand::class)) {
return $attribute[0]->newInstance()->name;
}
$r = new \ReflectionProperty($class, 'defaultName');
return $class === $r->class ? static::$defaultName : null;
}
/**
* @return string|null The default command description or null when no default description is set
*/
public static function getDefaultDescription(): ?string
{
$class = static::class;
if (\PHP_VERSION_ID >= 80000 && $attribute = (new \ReflectionClass($class))->getAttributes(AsCommand::class)) {
return $attribute[0]->newInstance()->description;
}
$r = new \ReflectionProperty($class, 'defaultDescription');
return $class === $r->class ? static::$defaultDescription : null;
}
/**
* @param string|null $name The name of the command; passing null means it must be set in configure()
*
@@ -70,10 +101,25 @@ class Command
{
$this->definition = new InputDefinition();
if (null !== $name || null !== $name = static::getDefaultName()) {
if (null === $name && null !== $name = static::getDefaultName()) {
$aliases = explode('|', $name);
if ('' === $name = array_shift($aliases)) {
$this->setHidden(true);
$name = array_shift($aliases);
}
$this->setAliases($aliases);
}
if (null !== $name) {
$this->setName($name);
}
if ('' === $this->description) {
$this->setDescription(static::getDefaultDescription() ?? '');
}
$this->configure();
}
@@ -95,6 +141,8 @@ class Command
} else {
$this->helperSet = null;
}
$this->fullDefinition = null;
}
public function setHelperSet(HelperSet $helperSet)
@@ -202,16 +250,12 @@ class Command
*/
public function run(InputInterface $input, OutputInterface $output)
{
// force the creation of the synopsis before the merge with the app definition
$this->getSynopsis(true);
$this->getSynopsis(false);
// add the application arguments and options
$this->mergeApplicationDefinition();
// bind the input against the command specific arguments/options
try {
$input->bind($this->definition);
$input->bind($this->getDefinition());
} catch (ExceptionInterface $e) {
if (!$this->ignoreValidationErrors) {
throw $e;
@@ -223,7 +267,7 @@ class Command
if (null !== $this->processTitle) {
if (\function_exists('cli_set_process_title')) {
if (!@cli_set_process_title($this->processTitle)) {
if ('Darwin' === PHP_OS) {
if ('Darwin' === \PHP_OS) {
$output->writeln('<comment>Running "cli_set_process_title" as an unprivileged user is not supported on MacOS.</comment>', OutputInterface::VERBOSITY_VERY_VERBOSE);
} else {
cli_set_process_title($this->processTitle);
@@ -255,7 +299,7 @@ class Command
$statusCode = $this->execute($input, $output);
if (!\is_int($statusCode)) {
throw new \TypeError(sprintf('Return value of "%s::execute()" must be of the type int, "%s" returned.', static::class, \gettype($statusCode)));
throw new \TypeError(sprintf('Return value of "%s::execute()" must be of the type int, "%s" returned.', static::class, get_debug_type($statusCode)));
}
}
@@ -281,7 +325,14 @@ class Command
if ($code instanceof \Closure) {
$r = new \ReflectionFunction($code);
if (null === $r->getClosureThis()) {
$code = \Closure::bind($code, $this);
set_error_handler(static function () {});
try {
if ($c = \Closure::bind($code, $this)) {
$code = $c;
}
} finally {
restore_error_handler();
}
}
}
@@ -296,23 +347,24 @@ class Command
* This method is not part of public API and should not be used directly.
*
* @param bool $mergeArgs Whether to merge or not the Application definition arguments to Command definition arguments
*
* @internal
*/
public function mergeApplicationDefinition(bool $mergeArgs = true)
{
if (null === $this->application || (true === $this->applicationDefinitionMerged && ($this->applicationDefinitionMergedWithArgs || !$mergeArgs))) {
if (null === $this->application) {
return;
}
$this->definition->addOptions($this->application->getDefinition()->getOptions());
$this->applicationDefinitionMerged = true;
$this->fullDefinition = new InputDefinition();
$this->fullDefinition->setOptions($this->definition->getOptions());
$this->fullDefinition->addOptions($this->application->getDefinition()->getOptions());
if ($mergeArgs) {
$currentArguments = $this->definition->getArguments();
$this->definition->setArguments($this->application->getDefinition()->getArguments());
$this->definition->addArguments($currentArguments);
$this->applicationDefinitionMergedWithArgs = true;
$this->fullDefinition->setArguments($this->application->getDefinition()->getArguments());
$this->fullDefinition->addArguments($this->definition->getArguments());
} else {
$this->fullDefinition->setArguments($this->definition->getArguments());
}
}
@@ -331,7 +383,7 @@ class Command
$this->definition->setDefinition($definition);
}
$this->applicationDefinitionMerged = false;
$this->fullDefinition = null;
return $this;
}
@@ -343,11 +395,7 @@ class Command
*/
public function getDefinition()
{
if (null === $this->definition) {
throw new LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', static::class));
}
return $this->definition;
return $this->fullDefinition ?? $this->getNativeDefinition();
}
/**
@@ -362,14 +410,18 @@ class Command
*/
public function getNativeDefinition()
{
return $this->getDefinition();
if (null === $this->definition) {
throw new LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', static::class));
}
return $this->definition;
}
/**
* Adds an argument.
*
* @param int|null $mode The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL
* @param string|string[]|null $default The default value (for InputArgument::OPTIONAL mode only)
* @param int|null $mode The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL
* @param mixed $default The default value (for InputArgument::OPTIONAL mode only)
*
* @throws InvalidArgumentException When argument mode is not valid
*
@@ -378,6 +430,9 @@ class Command
public function addArgument(string $name, int $mode = null, string $description = '', $default = null)
{
$this->definition->addArgument(new InputArgument($name, $mode, $description, $default));
if (null !== $this->fullDefinition) {
$this->fullDefinition->addArgument(new InputArgument($name, $mode, $description, $default));
}
return $this;
}
@@ -385,9 +440,9 @@ class Command
/**
* Adds an option.
*
* @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
* @param int|null $mode The option mode: One of the InputOption::VALUE_* constants
* @param string|string[]|int|bool|null $default The default value (must be null for InputOption::VALUE_NONE)
* @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
* @param int|null $mode The option mode: One of the InputOption::VALUE_* constants
* @param mixed $default The default value (must be null for InputOption::VALUE_NONE)
*
* @throws InvalidArgumentException If option mode is invalid or incompatible
*
@@ -396,6 +451,9 @@ class Command
public function addOption(string $name, $shortcut = null, int $mode = null, string $description = '', $default = null)
{
$this->definition->addOption(new InputOption($name, $shortcut, $mode, $description, $default));
if (null !== $this->fullDefinition) {
$this->fullDefinition->addOption(new InputOption($name, $shortcut, $mode, $description, $default));
}
return $this;
}
@@ -427,8 +485,6 @@ class Command
* This feature should be used only when creating a long process command,
* like a daemon.
*
* PHP 5.5+ or the proctitle PECL library is required
*
* @return $this
*/
public function setProcessTitle(string $title)
@@ -450,10 +506,13 @@ class Command
/**
* @param bool $hidden Whether or not the command should be hidden from the list of commands
* The default value will be true in Symfony 6.0
*
* @return Command The current instance
* @return $this
*
* @final since Symfony 5.1
*/
public function setHidden(bool $hidden)
public function setHidden(bool $hidden /*= true*/)
{
$this->hidden = $hidden;
@@ -546,11 +605,14 @@ class Command
*/
public function setAliases(iterable $aliases)
{
$list = [];
foreach ($aliases as $alias) {
$this->validateName($alias);
$list[] = $alias;
}
$this->aliases = $aliases;
$this->aliases = \is_array($aliases) ? $aliases : $list;
return $this;
}
@@ -590,7 +652,7 @@ class Command
*/
public function addUsage(string $usage)
{
if (0 !== strpos($usage, $this->name)) {
if (!str_starts_with($usage, $this->name)) {
$usage = sprintf('%s %s', $this->name, $usage);
}
+3 -3
View File
@@ -40,15 +40,15 @@ class HelpCommand extends Command
new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'),
new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command help'),
])
->setDescription('Displays help for a command')
->setDescription('Display help for a command')
->setHelp(<<<'EOF'
The <info>%command.name%</info> command displays help for a given command:
<info>php %command.full_name% list</info>
<info>%command.full_name% list</info>
You can also output the help in other formats by using the <comment>--format</comment> option:
<info>php %command.full_name% --format=xml list</info>
<info>%command.full_name% --format=xml list</info>
To display the list of available commands, please use the <info>list</info> command.
EOF
+211
View File
@@ -0,0 +1,211 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Command;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
final class LazyCommand extends Command
{
private $command;
private $isEnabled;
public function __construct(string $name, array $aliases, string $description, bool $isHidden, \Closure $commandFactory, ?bool $isEnabled = true)
{
$this->setName($name)
->setAliases($aliases)
->setHidden($isHidden)
->setDescription($description);
$this->command = $commandFactory;
$this->isEnabled = $isEnabled;
}
public function ignoreValidationErrors(): void
{
$this->getCommand()->ignoreValidationErrors();
}
public function setApplication(Application $application = null): void
{
if ($this->command instanceof parent) {
$this->command->setApplication($application);
}
parent::setApplication($application);
}
public function setHelperSet(HelperSet $helperSet): void
{
if ($this->command instanceof parent) {
$this->command->setHelperSet($helperSet);
}
parent::setHelperSet($helperSet);
}
public function isEnabled(): bool
{
return $this->isEnabled ?? $this->getCommand()->isEnabled();
}
public function run(InputInterface $input, OutputInterface $output): int
{
return $this->getCommand()->run($input, $output);
}
/**
* @return $this
*/
public function setCode(callable $code): self
{
$this->getCommand()->setCode($code);
return $this;
}
/**
* @internal
*/
public function mergeApplicationDefinition(bool $mergeArgs = true): void
{
$this->getCommand()->mergeApplicationDefinition($mergeArgs);
}
/**
* @return $this
*/
public function setDefinition($definition): self
{
$this->getCommand()->setDefinition($definition);
return $this;
}
public function getDefinition(): InputDefinition
{
return $this->getCommand()->getDefinition();
}
public function getNativeDefinition(): InputDefinition
{
return $this->getCommand()->getNativeDefinition();
}
/**
* @return $this
*/
public function addArgument(string $name, int $mode = null, string $description = '', $default = null): self
{
$this->getCommand()->addArgument($name, $mode, $description, $default);
return $this;
}
/**
* @return $this
*/
public function addOption(string $name, $shortcut = null, int $mode = null, string $description = '', $default = null): self
{
$this->getCommand()->addOption($name, $shortcut, $mode, $description, $default);
return $this;
}
/**
* @return $this
*/
public function setProcessTitle(string $title): self
{
$this->getCommand()->setProcessTitle($title);
return $this;
}
/**
* @return $this
*/
public function setHelp(string $help): self
{
$this->getCommand()->setHelp($help);
return $this;
}
public function getHelp(): string
{
return $this->getCommand()->getHelp();
}
public function getProcessedHelp(): string
{
return $this->getCommand()->getProcessedHelp();
}
public function getSynopsis(bool $short = false): string
{
return $this->getCommand()->getSynopsis($short);
}
/**
* @return $this
*/
public function addUsage(string $usage): self
{
$this->getCommand()->addUsage($usage);
return $this;
}
public function getUsages(): array
{
return $this->getCommand()->getUsages();
}
/**
* @return mixed
*/
public function getHelper(string $name)
{
return $this->getCommand()->getHelper($name);
}
public function getCommand(): parent
{
if (!$this->command instanceof \Closure) {
return $this->command;
}
$command = $this->command = ($this->command)();
$command->setApplication($this->getApplication());
if (null !== $this->getHelperSet()) {
$command->setHelperSet($this->getHelperSet());
}
$command->setName($this->getName())
->setAliases($this->getAliases())
->setHidden($this->isHidden())
->setDescription($this->getDescription());
// Will throw if the command is not correctly initialized.
$command->getDefinition();
return $command;
}
}
+12 -24
View File
@@ -13,7 +13,6 @@ namespace Symfony\Component\Console\Command;
use Symfony\Component\Console\Helper\DescriptorHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
@@ -32,37 +31,34 @@ class ListCommand extends Command
{
$this
->setName('list')
->setDefinition($this->createDefinition())
->setDescription('Lists commands')
->setDefinition([
new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name'),
new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list'),
new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'),
new InputOption('short', null, InputOption::VALUE_NONE, 'To skip describing commands\' arguments'),
])
->setDescription('List commands')
->setHelp(<<<'EOF'
The <info>%command.name%</info> command lists all commands:
<info>php %command.full_name%</info>
<info>%command.full_name%</info>
You can also display the commands for a specific namespace:
<info>php %command.full_name% test</info>
<info>%command.full_name% test</info>
You can also output the information in other formats by using the <comment>--format</comment> option:
<info>php %command.full_name% --format=xml</info>
<info>%command.full_name% --format=xml</info>
It's also possible to get raw list of commands (useful for embedding command runner):
<info>php %command.full_name% --raw</info>
<info>%command.full_name% --raw</info>
EOF
)
;
}
/**
* {@inheritdoc}
*/
public function getNativeDefinition()
{
return $this->createDefinition();
}
/**
* {@inheritdoc}
*/
@@ -73,17 +69,9 @@ EOF
'format' => $input->getOption('format'),
'raw_text' => $input->getOption('raw'),
'namespace' => $input->getArgument('namespace'),
'short' => $input->getOption('short'),
]);
return 0;
}
private function createDefinition(): InputDefinition
{
return new InputDefinition([
new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name'),
new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list'),
new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'),
]);
}
}
@@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Command;
/**
* Interface for command reacting to signal.
*
* @author Grégoire Pineau <lyrixx@lyrix.info>
*/
interface SignalableCommandInterface
{
/**
* Returns the list of signals to subscribe.
*/
public function getSubscribedSignals(): array;
/**
* The method will be called when the application is signaled.
*/
public function handleSignal(int $signal): void;
}
+29 -4
View File
@@ -11,6 +11,11 @@
namespace Symfony\Component\Console;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\Console\Event\ConsoleErrorEvent;
use Symfony\Component\Console\Event\ConsoleSignalEvent;
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
/**
* Contains all events dispatched by an Application.
*
@@ -21,11 +26,19 @@ final class ConsoleEvents
/**
* The COMMAND event allows you to attach listeners before any command is
* executed by the console. It also allows you to modify the command, input and output
* before they are handled to the command.
* before they are handed to the command.
*
* @Event("Symfony\Component\Console\Event\ConsoleCommandEvent")
*/
const COMMAND = 'console.command';
public const COMMAND = 'console.command';
/**
* The SIGNAL event allows you to perform some actions
* after the command execution was interrupted.
*
* @Event("Symfony\Component\Console\Event\ConsoleSignalEvent")
*/
public const SIGNAL = 'console.signal';
/**
* The TERMINATE event allows you to attach listeners after a command is
@@ -33,7 +46,7 @@ final class ConsoleEvents
*
* @Event("Symfony\Component\Console\Event\ConsoleTerminateEvent")
*/
const TERMINATE = 'console.terminate';
public const TERMINATE = 'console.terminate';
/**
* The ERROR event occurs when an uncaught exception or error appears.
@@ -43,5 +56,17 @@ final class ConsoleEvents
*
* @Event("Symfony\Component\Console\Event\ConsoleErrorEvent")
*/
const ERROR = 'console.error';
public const ERROR = 'console.error';
/**
* Event aliases.
*
* These aliases can be consumed by RegisterListenersPass.
*/
public const ALIASES = [
ConsoleCommandEvent::class => self::COMMAND,
ConsoleErrorEvent::class => self::ERROR,
ConsoleSignalEvent::class => self::SIGNAL,
ConsoleTerminateEvent::class => self::TERMINATE,
];
}
+168
View File
@@ -0,0 +1,168 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @author Pierre du Plessis <pdples@gmail.com>
*/
final class Cursor
{
private $output;
private $input;
public function __construct(OutputInterface $output, $input = null)
{
$this->output = $output;
$this->input = $input ?? (\defined('STDIN') ? \STDIN : fopen('php://input', 'r+'));
}
public function moveUp(int $lines = 1): self
{
$this->output->write(sprintf("\x1b[%dA", $lines));
return $this;
}
public function moveDown(int $lines = 1): self
{
$this->output->write(sprintf("\x1b[%dB", $lines));
return $this;
}
public function moveRight(int $columns = 1): self
{
$this->output->write(sprintf("\x1b[%dC", $columns));
return $this;
}
public function moveLeft(int $columns = 1): self
{
$this->output->write(sprintf("\x1b[%dD", $columns));
return $this;
}
public function moveToColumn(int $column): self
{
$this->output->write(sprintf("\x1b[%dG", $column));
return $this;
}
public function moveToPosition(int $column, int $row): self
{
$this->output->write(sprintf("\x1b[%d;%dH", $row + 1, $column));
return $this;
}
public function savePosition(): self
{
$this->output->write("\x1b7");
return $this;
}
public function restorePosition(): self
{
$this->output->write("\x1b8");
return $this;
}
public function hide(): self
{
$this->output->write("\x1b[?25l");
return $this;
}
public function show(): self
{
$this->output->write("\x1b[?25h\x1b[?0c");
return $this;
}
/**
* Clears all the output from the current line.
*/
public function clearLine(): self
{
$this->output->write("\x1b[2K");
return $this;
}
/**
* Clears all the output from the current line after the current position.
*/
public function clearLineAfter(): self
{
$this->output->write("\x1b[K");
return $this;
}
/**
* Clears all the output from the cursors' current position to the end of the screen.
*/
public function clearOutput(): self
{
$this->output->write("\x1b[0J");
return $this;
}
/**
* Clears the entire screen.
*/
public function clearScreen(): self
{
$this->output->write("\x1b[2J");
return $this;
}
/**
* Returns the current cursor position as x,y coordinates.
*/
public function getCurrentPosition(): array
{
static $isTtySupported;
if (null === $isTtySupported && \function_exists('proc_open')) {
$isTtySupported = (bool) @proc_open('echo 1 >/dev/null', [['file', '/dev/tty', 'r'], ['file', '/dev/tty', 'w'], ['file', '/dev/tty', 'w']], $pipes);
}
if (!$isTtySupported) {
return [1, 1];
}
$sttyMode = shell_exec('stty -g');
shell_exec('stty -icanon -echo');
@fwrite($this->input, "\033[6n");
$code = trim(fread($this->input, 1024));
shell_exec(sprintf('stty %s', $sttyMode));
sscanf($code, "\033[%d;%dR", $row, $col);
return [$col, $row];
}
}
@@ -12,11 +12,14 @@
namespace Symfony\Component\Console\DependencyInjection;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Command\LazyCommand;
use Symfony\Component\Console\CommandLoader\ContainerCommandLoader;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\TypedReference;
/**
@@ -28,11 +31,19 @@ class AddConsoleCommandPass implements CompilerPassInterface
{
private $commandLoaderServiceId;
private $commandTag;
private $noPreloadTag;
private $privateTagName;
public function __construct(string $commandLoaderServiceId = 'console.command_loader', string $commandTag = 'console.command')
public function __construct(string $commandLoaderServiceId = 'console.command_loader', string $commandTag = 'console.command', string $noPreloadTag = 'container.no_preload', string $privateTagName = 'container.private')
{
if (0 < \func_num_args()) {
trigger_deprecation('symfony/console', '5.3', 'Configuring "%s" is deprecated.', __CLASS__);
}
$this->commandLoaderServiceId = $commandLoaderServiceId;
$this->commandTag = $commandTag;
$this->noPreloadTag = $noPreloadTag;
$this->privateTagName = $privateTagName;
}
public function process(ContainerBuilder $container)
@@ -44,10 +55,11 @@ class AddConsoleCommandPass implements CompilerPassInterface
foreach ($commandServices as $id => $tags) {
$definition = $container->getDefinition($id);
$definition->addTag($this->noPreloadTag);
$class = $container->getParameterBag()->resolveValue($definition->getClass());
if (isset($tags[0]['command'])) {
$commandName = $tags[0]['command'];
$aliases = $tags[0]['command'];
} else {
if (!$r = $container->getReflectionClass($class)) {
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
@@ -55,11 +67,18 @@ class AddConsoleCommandPass implements CompilerPassInterface
if (!$r->isSubclassOf(Command::class)) {
throw new InvalidArgumentException(sprintf('The service "%s" tagged "%s" must be a subclass of "%s".', $id, $this->commandTag, Command::class));
}
$commandName = $class::getDefaultName();
$aliases = $class::getDefaultName();
}
$aliases = explode('|', $aliases ?? '');
$commandName = array_shift($aliases);
if ($isHidden = '' === $commandName) {
$commandName = array_shift($aliases);
}
if (null === $commandName) {
if (!$definition->isPublic() || $definition->isPrivate()) {
if (!$definition->isPublic() || $definition->isPrivate() || $definition->hasTag($this->privateTagName)) {
$commandId = 'console.command.public_alias.'.$id;
$container->setAlias($commandId, $id)->setPublic(true);
$id = $commandId;
@@ -69,16 +88,23 @@ class AddConsoleCommandPass implements CompilerPassInterface
continue;
}
$description = $tags[0]['description'] ?? null;
unset($tags[0]);
$lazyCommandMap[$commandName] = $id;
$lazyCommandRefs[$id] = new TypedReference($id, $class);
$aliases = [];
foreach ($aliases as $alias) {
$lazyCommandMap[$alias] = $id;
}
foreach ($tags as $tag) {
if (isset($tag['command'])) {
$aliases[] = $tag['command'];
$lazyCommandMap[$tag['command']] = $id;
}
$description = $description ?? $tag['description'] ?? null;
}
$definition->addMethodCall('setName', [$commandName]);
@@ -86,11 +112,35 @@ class AddConsoleCommandPass implements CompilerPassInterface
if ($aliases) {
$definition->addMethodCall('setAliases', [$aliases]);
}
if ($isHidden) {
$definition->addMethodCall('setHidden', [true]);
}
if (!$description) {
if (!$r = $container->getReflectionClass($class)) {
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
}
if (!$r->isSubclassOf(Command::class)) {
throw new InvalidArgumentException(sprintf('The service "%s" tagged "%s" must be a subclass of "%s".', $id, $this->commandTag, Command::class));
}
$description = $class::getDefaultDescription();
}
if ($description) {
$definition->addMethodCall('setDescription', [$description]);
$container->register('.'.$id.'.lazy', LazyCommand::class)
->setArguments([$commandName, $aliases, $description, $isHidden, new ServiceClosureArgument($lazyCommandRefs[$id])]);
$lazyCommandRefs[$id] = new Reference('.'.$id.'.lazy');
}
}
$container
->register($this->commandLoaderServiceId, ContainerCommandLoader::class)
->setPublic(true)
->addTag($this->noPreloadTag)
->setArguments([ServiceLocatorTagPass::register($container, $lazyCommandRefs), $lazyCommandMap]);
$container->setParameter('console.command.ids', $serviceIds);
@@ -22,7 +22,7 @@ use Symfony\Component\Console\Exception\CommandNotFoundException;
*/
class ApplicationDescription
{
const GLOBAL_NAMESPACE = '_global';
public const GLOBAL_NAMESPACE = '_global';
private $application;
private $namespace;
@@ -80,7 +80,7 @@ class ApplicationDescription
throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name));
}
return isset($this->commands[$name]) ? $this->commands[$name] : $this->aliases[$name];
return $this->commands[$name] ?? $this->aliases[$name];
}
private function inspectApplication()
+2 -12
View File
@@ -34,7 +34,7 @@ abstract class Descriptor implements DescriptorInterface
/**
* {@inheritdoc}
*/
public function describe(OutputInterface $output, $object, array $options = [])
public function describe(OutputInterface $output, object $object, array $options = [])
{
$this->output = $output;
@@ -55,7 +55,7 @@ abstract class Descriptor implements DescriptorInterface
$this->describeApplication($object, $options);
break;
default:
throw new InvalidArgumentException(sprintf('Object of type "%s" is not describable.', \get_class($object)));
throw new InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_debug_type($object)));
}
}
@@ -69,36 +69,26 @@ abstract class Descriptor implements DescriptorInterface
/**
* Describes an InputArgument instance.
*
* @return string|mixed
*/
abstract protected function describeInputArgument(InputArgument $argument, array $options = []);
/**
* Describes an InputOption instance.
*
* @return string|mixed
*/
abstract protected function describeInputOption(InputOption $option, array $options = []);
/**
* Describes an InputDefinition instance.
*
* @return string|mixed
*/
abstract protected function describeInputDefinition(InputDefinition $definition, array $options = []);
/**
* Describes a Command instance.
*
* @return string|mixed
*/
abstract protected function describeCommand(Command $command, array $options = []);
/**
* Describes an Application instance.
*
* @return string|mixed
*/
abstract protected function describeApplication(Application $application, array $options = []);
}
+1 -6
View File
@@ -20,10 +20,5 @@ use Symfony\Component\Console\Output\OutputInterface;
*/
interface DescriptorInterface
{
/**
* Describes an object if supported.
*
* @param object $object
*/
public function describe(OutputInterface $output, $object, array $options = []);
public function describe(OutputInterface $output, object $object, array $options = []);
}
+42 -17
View File
@@ -40,6 +40,9 @@ class JsonDescriptor extends Descriptor
protected function describeInputOption(InputOption $option, array $options = [])
{
$this->writeData($this->getInputOptionData($option), $options);
if ($option->isNegatable()) {
$this->writeData($this->getInputOptionData($option, true), $options);
}
}
/**
@@ -55,7 +58,7 @@ class JsonDescriptor extends Descriptor
*/
protected function describeCommand(Command $command, array $options = [])
{
$this->writeData($this->getCommandData($command), $options);
$this->writeData($this->getCommandData($command, $options['short'] ?? false), $options);
}
/**
@@ -63,12 +66,12 @@ class JsonDescriptor extends Descriptor
*/
protected function describeApplication(Application $application, array $options = [])
{
$describedNamespace = isset($options['namespace']) ? $options['namespace'] : null;
$describedNamespace = $options['namespace'] ?? null;
$description = new ApplicationDescription($application, $describedNamespace, true);
$commands = [];
foreach ($description->getCommands() as $command) {
$commands[] = $this->getCommandData($command);
$commands[] = $this->getCommandData($command, $options['short'] ?? false);
}
$data = [];
@@ -95,7 +98,7 @@ class JsonDescriptor extends Descriptor
*/
private function writeData(array $data, array $options)
{
$flags = isset($options['json_encoding']) ? $options['json_encoding'] : 0;
$flags = $options['json_encoding'] ?? 0;
$this->write(json_encode($data, $flags));
}
@@ -107,20 +110,28 @@ class JsonDescriptor extends Descriptor
'is_required' => $argument->isRequired(),
'is_array' => $argument->isArray(),
'description' => preg_replace('/\s*[\r\n]\s*/', ' ', $argument->getDescription()),
'default' => INF === $argument->getDefault() ? 'INF' : $argument->getDefault(),
'default' => \INF === $argument->getDefault() ? 'INF' : $argument->getDefault(),
];
}
private function getInputOptionData(InputOption $option): array
private function getInputOptionData(InputOption $option, bool $negated = false): array
{
return [
return $negated ? [
'name' => '--no-'.$option->getName(),
'shortcut' => '',
'accept_value' => false,
'is_value_required' => false,
'is_multiple' => false,
'description' => 'Negate the "--'.$option->getName().'" option',
'default' => false,
] : [
'name' => '--'.$option->getName(),
'shortcut' => $option->getShortcut() ? '-'.str_replace('|', '|-', $option->getShortcut()) : '',
'accept_value' => $option->acceptValue(),
'is_value_required' => $option->isValueRequired(),
'is_multiple' => $option->isArray(),
'description' => preg_replace('/\s*[\r\n]\s*/', ' ', $option->getDescription()),
'default' => INF === $option->getDefault() ? 'INF' : $option->getDefault(),
'default' => \INF === $option->getDefault() ? 'INF' : $option->getDefault(),
];
}
@@ -134,23 +145,37 @@ class JsonDescriptor extends Descriptor
$inputOptions = [];
foreach ($definition->getOptions() as $name => $option) {
$inputOptions[$name] = $this->getInputOptionData($option);
if ($option->isNegatable()) {
$inputOptions['no-'.$name] = $this->getInputOptionData($option, true);
}
}
return ['arguments' => $inputArguments, 'options' => $inputOptions];
}
private function getCommandData(Command $command): array
private function getCommandData(Command $command, bool $short = false): array
{
$command->getSynopsis();
$command->mergeApplicationDefinition(false);
return [
$data = [
'name' => $command->getName(),
'usage' => array_merge([$command->getSynopsis()], $command->getUsages(), $command->getAliases()),
'description' => $command->getDescription(),
'help' => $command->getProcessedHelp(),
'definition' => $this->getInputDefinitionData($command->getNativeDefinition()),
'hidden' => $command->isHidden(),
];
if ($short) {
$data += [
'usage' => $command->getAliases(),
];
} else {
$command->mergeApplicationDefinition(false);
$data += [
'usage' => array_merge([$command->getSynopsis()], $command->getUsages(), $command->getAliases()),
'help' => $command->getProcessedHelp(),
'definition' => $this->getInputDefinitionData($command->getDefinition()),
];
}
$data['hidden'] = $command->isHidden();
return $data;
}
}
+26 -8
View File
@@ -31,7 +31,7 @@ class MarkdownDescriptor extends Descriptor
/**
* {@inheritdoc}
*/
public function describe(OutputInterface $output, $object, array $options = [])
public function describe(OutputInterface $output, object $object, array $options = [])
{
$decorated = $output->isDecorated();
$output->setDecorated(false);
@@ -69,6 +69,9 @@ class MarkdownDescriptor extends Descriptor
protected function describeInputOption(InputOption $option, array $options = [])
{
$name = '--'.$option->getName();
if ($option->isNegatable()) {
$name .= '|--no-'.$option->getName();
}
if ($option->getShortcut()) {
$name .= '|-'.str_replace('|', '|-', $option->getShortcut()).'';
}
@@ -79,6 +82,7 @@ class MarkdownDescriptor extends Descriptor
.'* Accept value: '.($option->acceptValue() ? 'yes' : 'no')."\n"
.'* Is value required: '.($option->isValueRequired() ? 'yes' : 'no')."\n"
.'* Is multiple: '.($option->isArray() ? 'yes' : 'no')."\n"
.'* Is negatable: '.($option->isNegatable() ? 'yes' : 'no')."\n"
.'* Default: `'.str_replace("\n", '', var_export($option->getDefault(), true)).'`'
);
}
@@ -118,12 +122,25 @@ class MarkdownDescriptor extends Descriptor
*/
protected function describeCommand(Command $command, array $options = [])
{
$command->getSynopsis();
if ($options['short'] ?? false) {
$this->write(
'`'.$command->getName()."`\n"
.str_repeat('-', Helper::width($command->getName()) + 2)."\n\n"
.($command->getDescription() ? $command->getDescription()."\n\n" : '')
.'### Usage'."\n\n"
.array_reduce($command->getAliases(), function ($carry, $usage) {
return $carry.'* `'.$usage.'`'."\n";
})
);
return;
}
$command->mergeApplicationDefinition(false);
$this->write(
'`'.$command->getName()."`\n"
.str_repeat('-', Helper::strlen($command->getName()) + 2)."\n\n"
.str_repeat('-', Helper::width($command->getName()) + 2)."\n\n"
.($command->getDescription() ? $command->getDescription()."\n\n" : '')
.'### Usage'."\n\n"
.array_reduce(array_merge([$command->getSynopsis()], $command->getAliases(), $command->getUsages()), function ($carry, $usage) {
@@ -136,9 +153,10 @@ class MarkdownDescriptor extends Descriptor
$this->write($help);
}
if ($command->getNativeDefinition()) {
$definition = $command->getDefinition();
if ($definition->getOptions() || $definition->getArguments()) {
$this->write("\n\n");
$this->describeInputDefinition($command->getNativeDefinition());
$this->describeInputDefinition($definition);
}
}
@@ -147,11 +165,11 @@ class MarkdownDescriptor extends Descriptor
*/
protected function describeApplication(Application $application, array $options = [])
{
$describedNamespace = isset($options['namespace']) ? $options['namespace'] : null;
$describedNamespace = $options['namespace'] ?? null;
$description = new ApplicationDescription($application, $describedNamespace);
$title = $this->getApplicationTitle($application);
$this->write($title."\n".str_repeat('=', Helper::strlen($title)));
$this->write($title."\n".str_repeat('=', Helper::width($title)));
foreach ($description->getNamespaces() as $namespace) {
if (ApplicationDescription::GLOBAL_NAMESPACE !== $namespace['id']) {
@@ -167,7 +185,7 @@ class MarkdownDescriptor extends Descriptor
foreach ($description->getCommands() as $command) {
$this->write("\n\n");
if (null !== $describeCommand = $this->describeCommand($command)) {
if (null !== $describeCommand = $this->describeCommand($command, $options)) {
$this->write($describeCommand);
}
}
+21 -22
View File
@@ -39,7 +39,7 @@ class TextDescriptor extends Descriptor
$default = '';
}
$totalWidth = isset($options['total_width']) ? $options['total_width'] : Helper::strlen($argument->getName());
$totalWidth = $options['total_width'] ?? Helper::width($argument->getName());
$spacingWidth = $totalWidth - \strlen($argument->getName());
$this->writeText(sprintf(' <info>%s</info> %s%s%s',
@@ -71,13 +71,13 @@ class TextDescriptor extends Descriptor
}
}
$totalWidth = isset($options['total_width']) ? $options['total_width'] : $this->calculateTotalWidthForOptions([$option]);
$totalWidth = $options['total_width'] ?? $this->calculateTotalWidthForOptions([$option]);
$synopsis = sprintf('%s%s',
$option->getShortcut() ? sprintf('-%s, ', $option->getShortcut()) : ' ',
sprintf('--%s%s', $option->getName(), $value)
sprintf($option->isNegatable() ? '--%1$s|--no-%1$s' : '--%1$s%2$s', $option->getName(), $value)
);
$spacingWidth = $totalWidth - Helper::strlen($synopsis);
$spacingWidth = $totalWidth - Helper::width($synopsis);
$this->writeText(sprintf(' <info>%s</info> %s%s%s%s',
$synopsis,
@@ -96,7 +96,7 @@ class TextDescriptor extends Descriptor
{
$totalWidth = $this->calculateTotalWidthForOptions($definition->getOptions());
foreach ($definition->getArguments() as $argument) {
$totalWidth = max($totalWidth, Helper::strlen($argument->getName()));
$totalWidth = max($totalWidth, Helper::width($argument->getName()));
}
if ($definition->getArguments()) {
@@ -117,7 +117,7 @@ class TextDescriptor extends Descriptor
$this->writeText('<comment>Options:</comment>', $options);
foreach ($definition->getOptions() as $option) {
if (\strlen($option->getShortcut()) > 1) {
if (\strlen($option->getShortcut() ?? '') > 1) {
$laterOptions[] = $option;
continue;
}
@@ -136,8 +136,6 @@ class TextDescriptor extends Descriptor
*/
protected function describeCommand(Command $command, array $options = [])
{
$command->getSynopsis(true);
$command->getSynopsis(false);
$command->mergeApplicationDefinition(false);
if ($description = $command->getDescription()) {
@@ -154,7 +152,7 @@ class TextDescriptor extends Descriptor
}
$this->writeText("\n");
$definition = $command->getNativeDefinition();
$definition = $command->getDefinition();
if ($definition->getOptions() || $definition->getArguments()) {
$this->writeText("\n");
$this->describeInputDefinition($definition, $options);
@@ -176,7 +174,7 @@ class TextDescriptor extends Descriptor
*/
protected function describeApplication(Application $application, array $options = [])
{
$describedNamespace = isset($options['namespace']) ? $options['namespace'] : null;
$describedNamespace = $options['namespace'] ?? null;
$description = new ApplicationDescription($application, $describedNamespace);
if (isset($options['raw_text']) && $options['raw_text']) {
@@ -212,7 +210,7 @@ class TextDescriptor extends Descriptor
// calculate max. width based on available commands per namespace
$width = $this->getColumnWidth(array_merge(...array_values(array_map(function ($namespace) use ($commands) {
return array_intersect($namespace['commands'], array_keys($commands));
}, $namespaces))));
}, array_values($namespaces)))));
if ($describedNamespace) {
$this->writeText(sprintf('<comment>Available commands for the "%s" namespace:</comment>', $describedNamespace), $options);
@@ -236,7 +234,7 @@ class TextDescriptor extends Descriptor
foreach ($namespace['commands'] as $name) {
$this->writeText("\n");
$spacingWidth = $width - Helper::strlen($name);
$spacingWidth = $width - Helper::width($name);
$command = $commands[$name];
$commandAliases = $name === $command->getName() ? $this->getCommandAliasesText($command) : '';
$this->writeText(sprintf(' <info>%s</info>%s%s', $name, str_repeat(' ', $spacingWidth), $commandAliases.$command->getDescription()), $options);
@@ -280,7 +278,7 @@ class TextDescriptor extends Descriptor
*/
private function formatDefaultValue($default): string
{
if (INF === $default) {
if (\INF === $default) {
return 'INF';
}
@@ -294,11 +292,11 @@ class TextDescriptor extends Descriptor
}
}
return str_replace('\\\\', '\\', json_encode($default, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
return str_replace('\\\\', '\\', json_encode($default, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE));
}
/**
* @param (Command|string)[] $commands
* @param array<Command|string> $commands
*/
private function getColumnWidth(array $commands): int
{
@@ -306,12 +304,12 @@ class TextDescriptor extends Descriptor
foreach ($commands as $command) {
if ($command instanceof Command) {
$widths[] = Helper::strlen($command->getName());
$widths[] = Helper::width($command->getName());
foreach ($command->getAliases() as $alias) {
$widths[] = Helper::strlen($alias);
$widths[] = Helper::width($alias);
}
} else {
$widths[] = Helper::strlen($command);
$widths[] = Helper::width($command);
}
}
@@ -326,10 +324,11 @@ class TextDescriptor extends Descriptor
$totalWidth = 0;
foreach ($options as $option) {
// "-" + shortcut + ", --" + name
$nameLength = 1 + max(Helper::strlen($option->getShortcut()), 1) + 4 + Helper::strlen($option->getName());
if ($option->acceptValue()) {
$valueLength = 1 + Helper::strlen($option->getName()); // = + value
$nameLength = 1 + max(Helper::width($option->getShortcut()), 1) + 4 + Helper::width($option->getName());
if ($option->isNegatable()) {
$nameLength += 6 + Helper::width($option->getName()); // |--no- + name
} elseif ($option->acceptValue()) {
$valueLength = 1 + Helper::width($option->getName()); // = + value
$valueLength += $option->isValueOptional() ? 2 : 0; // [ + ]
$nameLength += $valueLength;
+33 -17
View File
@@ -44,37 +44,42 @@ class XmlDescriptor extends Descriptor
return $dom;
}
public function getCommandDocument(Command $command): \DOMDocument
public function getCommandDocument(Command $command, bool $short = false): \DOMDocument
{
$dom = new \DOMDocument('1.0', 'UTF-8');
$dom->appendChild($commandXML = $dom->createElement('command'));
$command->getSynopsis();
$command->mergeApplicationDefinition(false);
$commandXML->setAttribute('id', $command->getName());
$commandXML->setAttribute('name', $command->getName());
$commandXML->setAttribute('hidden', $command->isHidden() ? 1 : 0);
$commandXML->appendChild($usagesXML = $dom->createElement('usages'));
foreach (array_merge([$command->getSynopsis()], $command->getAliases(), $command->getUsages()) as $usage) {
$usagesXML->appendChild($dom->createElement('usage', $usage));
}
$commandXML->appendChild($descriptionXML = $dom->createElement('description'));
$descriptionXML->appendChild($dom->createTextNode(str_replace("\n", "\n ", $command->getDescription())));
$commandXML->appendChild($helpXML = $dom->createElement('help'));
$helpXML->appendChild($dom->createTextNode(str_replace("\n", "\n ", $command->getProcessedHelp())));
if ($short) {
foreach ($command->getAliases() as $usage) {
$usagesXML->appendChild($dom->createElement('usage', $usage));
}
} else {
$command->mergeApplicationDefinition(false);
$definitionXML = $this->getInputDefinitionDocument($command->getNativeDefinition());
$this->appendDocument($commandXML, $definitionXML->getElementsByTagName('definition')->item(0));
foreach (array_merge([$command->getSynopsis()], $command->getAliases(), $command->getUsages()) as $usage) {
$usagesXML->appendChild($dom->createElement('usage', $usage));
}
$commandXML->appendChild($helpXML = $dom->createElement('help'));
$helpXML->appendChild($dom->createTextNode(str_replace("\n", "\n ", $command->getProcessedHelp())));
$definitionXML = $this->getInputDefinitionDocument($command->getDefinition());
$this->appendDocument($commandXML, $definitionXML->getElementsByTagName('definition')->item(0));
}
return $dom;
}
public function getApplicationDocument(Application $application, string $namespace = null): \DOMDocument
public function getApplicationDocument(Application $application, string $namespace = null, bool $short = false): \DOMDocument
{
$dom = new \DOMDocument('1.0', 'UTF-8');
$dom->appendChild($rootXml = $dom->createElement('symfony'));
@@ -95,7 +100,7 @@ class XmlDescriptor extends Descriptor
}
foreach ($description->getCommands() as $command) {
$this->appendDocument($commandsXML, $this->getCommandDocument($command));
$this->appendDocument($commandsXML, $this->getCommandDocument($command, $short));
}
if (!$namespace) {
@@ -144,7 +149,7 @@ class XmlDescriptor extends Descriptor
*/
protected function describeCommand(Command $command, array $options = [])
{
$this->writeDocument($this->getCommandDocument($command));
$this->writeDocument($this->getCommandDocument($command, $options['short'] ?? false));
}
/**
@@ -152,7 +157,7 @@ class XmlDescriptor extends Descriptor
*/
protected function describeApplication(Application $application, array $options = [])
{
$this->writeDocument($this->getApplicationDocument($application, isset($options['namespace']) ? $options['namespace'] : null));
$this->writeDocument($this->getApplicationDocument($application, $options['namespace'] ?? null, $options['short'] ?? false));
}
/**
@@ -201,7 +206,7 @@ class XmlDescriptor extends Descriptor
$dom->appendChild($objectXML = $dom->createElement('option'));
$objectXML->setAttribute('name', '--'.$option->getName());
$pos = strpos($option->getShortcut(), '|');
$pos = strpos($option->getShortcut() ?? '', '|');
if (false !== $pos) {
$objectXML->setAttribute('shortcut', '-'.substr($option->getShortcut(), 0, $pos));
$objectXML->setAttribute('shortcuts', '-'.str_replace('|', '|-', $option->getShortcut()));
@@ -226,6 +231,17 @@ class XmlDescriptor extends Descriptor
}
}
if ($option->isNegatable()) {
$dom->appendChild($objectXML = $dom->createElement('option'));
$objectXML->setAttribute('name', '--no-'.$option->getName());
$objectXML->setAttribute('shortcut', '');
$objectXML->setAttribute('accept_value', 0);
$objectXML->setAttribute('is_value_required', 0);
$objectXML->setAttribute('is_multiple', 0);
$objectXML->appendChild($descriptionXML = $dom->createElement('description'));
$descriptionXML->appendChild($dom->createTextNode('Negate the "--'.$option->getName().'" option'));
}
return $dom;
}
}
+1 -1
View File
@@ -21,7 +21,7 @@ final class ConsoleCommandEvent extends ConsoleEvent
/**
* The return code for skipped commands, this will also be passed into the terminate event.
*/
const RETURN_CODE_DISABLED = 113;
public const RETURN_CODE_DISABLED = 113;
/**
* Indicates if the command should be run or skipped.
+1 -1
View File
@@ -53,6 +53,6 @@ final class ConsoleErrorEvent extends ConsoleEvent
public function getExitCode(): int
{
return null !== $this->exitCode ? $this->exitCode : (\is_int($this->error->getCode()) && 0 !== $this->error->getCode() ? $this->error->getCode() : 1);
return $this->exitCode ?? (\is_int($this->error->getCode()) && 0 !== $this->error->getCode() ? $this->error->getCode() : 1);
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Event;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @author marie <marie@users.noreply.github.com>
*/
final class ConsoleSignalEvent extends ConsoleEvent
{
private $handlingSignal;
public function __construct(Command $command, InputInterface $input, OutputInterface $output, int $handlingSignal)
{
parent::__construct($command, $input, $output);
$this->handlingSignal = $handlingSignal;
}
public function getHandlingSignal(): int
{
return $this->handlingSignal;
}
}
+2 -2
View File
@@ -40,12 +40,12 @@ class ErrorListener implements EventSubscriberInterface
$error = $event->getError();
if (!$inputString = $this->getInputString($event)) {
$this->logger->error('An error occurred while using the console. Message: "{message}"', ['exception' => $error, 'message' => $error->getMessage()]);
$this->logger->critical('An error occurred while using the console. Message: "{message}"', ['exception' => $error, 'message' => $error->getMessage()]);
return;
}
$this->logger->error('Error thrown while running command "{command}". Message: "{message}"', ['exception' => $error, 'command' => $inputString, 'message' => $error->getMessage()]);
$this->logger->critical('Error thrown while running command "{command}". Message: "{message}"', ['exception' => $error, 'command' => $inputString, 'message' => $error->getMessage()]);
}
public function onConsoleTerminate(ConsoleTerminateEvent $event)
@@ -21,10 +21,10 @@ class CommandNotFoundException extends \InvalidArgumentException implements Exce
private $alternatives;
/**
* @param string $message Exception message to throw
* @param array $alternatives List of similar defined names
* @param int $code Exception code
* @param \Throwable $previous Previous exception used for the exception chaining
* @param string $message Exception message to throw
* @param string[] $alternatives List of similar defined names
* @param int $code Exception code
* @param \Throwable|null $previous Previous exception used for the exception chaining
*/
public function __construct(string $message, array $alternatives = [], int $code = 0, \Throwable $previous = null)
{
@@ -34,7 +34,7 @@ class CommandNotFoundException extends \InvalidArgumentException implements Exce
}
/**
* @return array A list of similar defined names
* @return string[] A list of similar defined names
*/
public function getAlternatives()
{
@@ -0,0 +1,72 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Formatter;
/**
* @author Tien Xuan Vo <tien.xuan.vo@gmail.com>
*/
final class NullOutputFormatter implements OutputFormatterInterface
{
private $style;
/**
* {@inheritdoc}
*/
public function format(?string $message): void
{
// do nothing
}
/**
* {@inheritdoc}
*/
public function getStyle(string $name): OutputFormatterStyleInterface
{
if ($this->style) {
return $this->style;
}
// to comply with the interface we must return a OutputFormatterStyleInterface
return $this->style = new NullOutputFormatterStyle();
}
/**
* {@inheritdoc}
*/
public function hasStyle(string $name): bool
{
return false;
}
/**
* {@inheritdoc}
*/
public function isDecorated(): bool
{
return false;
}
/**
* {@inheritdoc}
*/
public function setDecorated(bool $decorated): void
{
// do nothing
}
/**
* {@inheritdoc}
*/
public function setStyle(string $name, OutputFormatterStyleInterface $style): void
{
// do nothing
}
}
@@ -0,0 +1,65 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Formatter;
/**
* @author Tien Xuan Vo <tien.xuan.vo@gmail.com>
*/
final class NullOutputFormatterStyle implements OutputFormatterStyleInterface
{
/**
* {@inheritdoc}
*/
public function apply(string $text): string
{
return $text;
}
/**
* {@inheritdoc}
*/
public function setBackground(string $color = null): void
{
// do nothing
}
/**
* {@inheritdoc}
*/
public function setForeground(string $color = null): void
{
// do nothing
}
/**
* {@inheritdoc}
*/
public function setOption(string $option): void
{
// do nothing
}
/**
* {@inheritdoc}
*/
public function setOptions(array $options): void
{
// do nothing
}
/**
* {@inheritdoc}
*/
public function unsetOption(string $option): void
{
// do nothing
}
}
+13 -5
View File
@@ -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.
*
@@ -44,7 +52,7 @@ class OutputFormatter implements WrappableOutputFormatterInterface
*/
public static function escapeTrailingBackslash(string $text): string
{
if ('\\' === substr($text, -1)) {
if (str_ends_with($text, '\\')) {
$len = \strlen($text);
$text = rtrim($text, '\\');
$text = str_replace("\0", '', $text);
@@ -136,7 +144,7 @@ class OutputFormatter implements WrappableOutputFormatterInterface
$output = '';
$tagRegex = '[a-z][^<>]*+';
$currentLineLength = 0;
preg_match_all("#<(($tagRegex) | /($tagRegex)?)>#ix", $message, $matches, PREG_OFFSET_CAPTURE);
preg_match_all("#<(($tagRegex) | /($tagRegex)?)>#ix", $message, $matches, \PREG_OFFSET_CAPTURE);
foreach ($matches[0] as $i => $match) {
$pos = $match[1];
$text = $match[0];
@@ -153,7 +161,7 @@ class OutputFormatter implements WrappableOutputFormatterInterface
if ($open = '/' != $text[1]) {
$tag = $matches[1][$i][0];
} else {
$tag = isset($matches[3][$i][0]) ? $matches[3][$i][0] : '';
$tag = $matches[3][$i][0] ?? '';
}
if (!$open && !$tag) {
@@ -170,7 +178,7 @@ class OutputFormatter implements WrappableOutputFormatterInterface
$output .= $this->applyCurrentStyle(substr($message, $offset), $output, $width, $currentLineLength);
if (false !== strpos($output, "\0")) {
if (str_contains($output, "\0")) {
return strtr($output, ["\0" => '\\', '\\<' => '<']);
}
@@ -194,7 +202,7 @@ class OutputFormatter implements WrappableOutputFormatterInterface
return $this->styles[$string];
}
if (!preg_match_all('/([^=]+)=([^;]+)(;|$)/', $string, $matches, PREG_SET_ORDER)) {
if (!preg_match_all('/([^=]+)=([^;]+)(;|$)/', $string, $matches, \PREG_SET_ORDER)) {
return null;
}
+15 -103
View File
@@ -11,7 +11,7 @@
namespace Symfony\Component\Console\Formatter;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Color;
/**
* Formatter style class for defining styles.
@@ -20,40 +20,11 @@ use Symfony\Component\Console\Exception\InvalidArgumentException;
*/
class OutputFormatterStyle implements OutputFormatterStyleInterface
{
private static $availableForegroundColors = [
'black' => ['set' => 30, 'unset' => 39],
'red' => ['set' => 31, 'unset' => 39],
'green' => ['set' => 32, 'unset' => 39],
'yellow' => ['set' => 33, 'unset' => 39],
'blue' => ['set' => 34, 'unset' => 39],
'magenta' => ['set' => 35, 'unset' => 39],
'cyan' => ['set' => 36, 'unset' => 39],
'white' => ['set' => 37, 'unset' => 39],
'default' => ['set' => 39, 'unset' => 39],
];
private static $availableBackgroundColors = [
'black' => ['set' => 40, 'unset' => 49],
'red' => ['set' => 41, 'unset' => 49],
'green' => ['set' => 42, 'unset' => 49],
'yellow' => ['set' => 43, 'unset' => 49],
'blue' => ['set' => 44, 'unset' => 49],
'magenta' => ['set' => 45, 'unset' => 49],
'cyan' => ['set' => 46, 'unset' => 49],
'white' => ['set' => 47, 'unset' => 49],
'default' => ['set' => 49, 'unset' => 49],
];
private static $availableOptions = [
'bold' => ['set' => 1, 'unset' => 22],
'underscore' => ['set' => 4, 'unset' => 24],
'blink' => ['set' => 5, 'unset' => 25],
'reverse' => ['set' => 7, 'unset' => 27],
'conceal' => ['set' => 8, 'unset' => 28],
];
private $color;
private $foreground;
private $background;
private $options;
private $href;
private $options = [];
private $handlesHrefGracefully;
/**
@@ -64,15 +35,7 @@ class OutputFormatterStyle implements OutputFormatterStyleInterface
*/
public function __construct(string $foreground = null, string $background = null, array $options = [])
{
if (null !== $foreground) {
$this->setForeground($foreground);
}
if (null !== $background) {
$this->setBackground($background);
}
if (\count($options)) {
$this->setOptions($options);
}
$this->color = new Color($this->foreground = $foreground ?: '', $this->background = $background ?: '', $this->options = $options);
}
/**
@@ -80,17 +43,7 @@ class OutputFormatterStyle implements OutputFormatterStyleInterface
*/
public function setForeground(string $color = null)
{
if (null === $color) {
$this->foreground = null;
return;
}
if (!isset(static::$availableForegroundColors[$color])) {
throw new InvalidArgumentException(sprintf('Invalid foreground color specified: "%s". Expected one of (%s).', $color, implode(', ', array_keys(static::$availableForegroundColors))));
}
$this->foreground = static::$availableForegroundColors[$color];
$this->color = new Color($this->foreground = $color ?: '', $this->background, $this->options);
}
/**
@@ -98,17 +51,7 @@ class OutputFormatterStyle implements OutputFormatterStyleInterface
*/
public function setBackground(string $color = null)
{
if (null === $color) {
$this->background = null;
return;
}
if (!isset(static::$availableBackgroundColors[$color])) {
throw new InvalidArgumentException(sprintf('Invalid background color specified: "%s". Expected one of (%s).', $color, implode(', ', array_keys(static::$availableBackgroundColors))));
}
$this->background = static::$availableBackgroundColors[$color];
$this->color = new Color($this->foreground, $this->background = $color ?: '', $this->options);
}
public function setHref(string $url): void
@@ -121,13 +64,8 @@ class OutputFormatterStyle implements OutputFormatterStyleInterface
*/
public function setOption(string $option)
{
if (!isset(static::$availableOptions[$option])) {
throw new InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s).', $option, implode(', ', array_keys(static::$availableOptions))));
}
if (!\in_array(static::$availableOptions[$option], $this->options)) {
$this->options[] = static::$availableOptions[$option];
}
$this->options[] = $option;
$this->color = new Color($this->foreground, $this->background, $this->options);
}
/**
@@ -135,14 +73,12 @@ class OutputFormatterStyle implements OutputFormatterStyleInterface
*/
public function unsetOption(string $option)
{
if (!isset(static::$availableOptions[$option])) {
throw new InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s).', $option, implode(', ', array_keys(static::$availableOptions))));
}
$pos = array_search(static::$availableOptions[$option], $this->options);
$pos = array_search($option, $this->options);
if (false !== $pos) {
unset($this->options[$pos]);
}
$this->color = new Color($this->foreground, $this->background, $this->options);
}
/**
@@ -150,11 +86,7 @@ class OutputFormatterStyle implements OutputFormatterStyleInterface
*/
public function setOptions(array $options)
{
$this->options = [];
foreach ($options as $option) {
$this->setOption($option);
}
$this->color = new Color($this->foreground, $this->background, $this->options = $options);
}
/**
@@ -162,35 +94,15 @@ class OutputFormatterStyle implements OutputFormatterStyleInterface
*/
public function apply(string $text)
{
$setCodes = [];
$unsetCodes = [];
if (null === $this->handlesHrefGracefully) {
$this->handlesHrefGracefully = 'JetBrains-JediTerm' !== getenv('TERMINAL_EMULATOR') && !getenv('KONSOLE_VERSION');
}
if (null !== $this->foreground) {
$setCodes[] = $this->foreground['set'];
$unsetCodes[] = $this->foreground['unset'];
}
if (null !== $this->background) {
$setCodes[] = $this->background['set'];
$unsetCodes[] = $this->background['unset'];
}
foreach ($this->options as $option) {
$setCodes[] = $option['set'];
$unsetCodes[] = $option['unset'];
$this->handlesHrefGracefully = 'JetBrains-JediTerm' !== getenv('TERMINAL_EMULATOR')
&& (!getenv('KONSOLE_VERSION') || (int) getenv('KONSOLE_VERSION') > 201100);
}
if (null !== $this->href && $this->handlesHrefGracefully) {
$text = "\033]8;;$this->href\033\\$text\033]8;;\033\\";
}
if (0 === \count($setCodes)) {
return $text;
}
return sprintf("\033[%sm%s\033[%sm", implode(';', $setCodes), $text, implode(';', $unsetCodes));
return $this->color->apply($text);
}
}
@@ -28,7 +28,7 @@ class OutputFormatterStyleStack implements ResetInterface
public function __construct(OutputFormatterStyleInterface $emptyStyle = null)
{
$this->emptyStyle = $emptyStyle ?: new OutputFormatterStyle();
$this->emptyStyle = $emptyStyle ?? new OutputFormatterStyle();
$this->reset();
}
+4 -4
View File
@@ -48,12 +48,12 @@ class FormatterHelper extends Helper
foreach ($messages as $message) {
$message = OutputFormatter::escape($message);
$lines[] = sprintf($large ? ' %s ' : ' %s ', $message);
$len = max(self::strlen($message) + ($large ? 4 : 2), $len);
$len = max(self::width($message) + ($large ? 4 : 2), $len);
}
$messages = $large ? [str_repeat(' ', $len)] : [];
for ($i = 0; isset($lines[$i]); ++$i) {
$messages[] = $lines[$i].str_repeat(' ', $len - self::strlen($lines[$i]));
$messages[] = $lines[$i].str_repeat(' ', $len - self::width($lines[$i]));
}
if ($large) {
$messages[] = str_repeat(' ', $len);
@@ -73,9 +73,9 @@ class FormatterHelper extends Helper
*/
public function truncate(string $message, int $length, string $suffix = '...')
{
$computedLength = $length - self::strlen($suffix);
$computedLength = $length - self::width($suffix);
if ($computedLength > self::strlen($message)) {
if ($computedLength > self::width($message)) {
return $message;
}
+51 -5
View File
@@ -12,6 +12,7 @@
namespace Symfony\Component\Console\Helper;
use Symfony\Component\Console\Formatter\OutputFormatterInterface;
use Symfony\Component\String\UnicodeString;
/**
* Helper is the base class for all helper classes.
@@ -41,10 +42,29 @@ abstract class Helper implements HelperInterface
/**
* Returns the length of a string, using mb_strwidth if it is available.
*
* @deprecated since 5.3
*
* @return int The length of the string
*/
public static function strlen(?string $string)
{
trigger_deprecation('symfony/console', '5.3', 'Method "%s()" is deprecated and will be removed in Symfony 6.0. Use Helper::width() or Helper::length() instead.', __METHOD__);
return self::width($string);
}
/**
* Returns the width of a string, using mb_strwidth if it is available.
* The width is how many characters positions the string will use.
*/
public static function width(?string $string): int
{
$string ?? $string = '';
if (preg_match('//u', $string)) {
return (new UnicodeString($string))->width(false);
}
if (false === $encoding = mb_detect_encoding($string, null, true)) {
return \strlen($string);
}
@@ -52,13 +72,34 @@ abstract class Helper implements HelperInterface
return mb_strwidth($string, $encoding);
}
/**
* Returns the length of a string, using mb_strlen if it is available.
* The length is related to how many bytes the string will use.
*/
public static function length(?string $string): int
{
$string ?? $string = '';
if (preg_match('//u', $string)) {
return (new UnicodeString($string))->length();
}
if (false === $encoding = mb_detect_encoding($string, null, true)) {
return \strlen($string);
}
return mb_strlen($string, $encoding);
}
/**
* Returns the subset of a string, using mb_substr if it is available.
*
* @return string The string subset
*/
public static function substr(string $string, int $from, int $length = null)
public static function substr(?string $string, int $from, int $length = null)
{
$string ?? $string = '';
if (false === $encoding = mb_detect_encoding($string, null, true)) {
return substr($string, $from, $length);
}
@@ -112,17 +153,22 @@ abstract class Helper implements HelperInterface
return sprintf('%d B', $memory);
}
public static function strlenWithoutDecoration(OutputFormatterInterface $formatter, $string)
/**
* @deprecated since 5.3
*/
public static function strlenWithoutDecoration(OutputFormatterInterface $formatter, ?string $string)
{
return self::strlen(self::removeDecoration($formatter, $string));
trigger_deprecation('symfony/console', '5.3', 'Method "%s()" is deprecated and will be removed in Symfony 6.0. Use Helper::removeDecoration() instead.', __METHOD__);
return self::width(self::removeDecoration($formatter, $string));
}
public static function removeDecoration(OutputFormatterInterface $formatter, $string)
public static function removeDecoration(OutputFormatterInterface $formatter, ?string $string)
{
$isDecorated = $formatter->isDecorated();
$formatter->setDecorated(false);
// remove <...> formatting
$string = $formatter->format($string);
$string = $formatter->format($string ?? '');
// remove already formatted characters
$string = preg_replace("/\033\[[^m]*m/", '', $string);
$formatter->setDecorated($isDecorated);
+5 -1
View File
@@ -36,6 +36,10 @@ class ProcessHelper extends Helper
*/
public function run(OutputInterface $output, $cmd, string $error = null, callable $callback = null, int $verbosity = OutputInterface::VERBOSITY_VERY_VERBOSE): Process
{
if (!class_exists(Process::class)) {
throw new \LogicException('The ProcessHelper cannot be run as the Process component is not installed. Try running "compose require symfony/process".');
}
if ($output instanceof ConsoleOutputInterface) {
$output = $output->getErrorOutput();
}
@@ -47,7 +51,7 @@ class ProcessHelper extends Helper
}
if (!\is_array($cmd)) {
throw new \TypeError(sprintf('The "command" argument of "%s()" must be an array or a "%s" instance, "%s" given.', __METHOD__, Process::class, \is_object($cmd) ? \get_class($cmd) : \gettype($cmd)));
throw new \TypeError(sprintf('The "command" argument of "%s()" must be an array or a "%s" instance, "%s" given.', __METHOD__, Process::class, get_debug_type($cmd)));
}
if (\is_string($cmd[0] ?? null)) {
+68 -43
View File
@@ -11,6 +11,7 @@
namespace Symfony\Component\Console\Helper;
use Symfony\Component\Console\Cursor;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\ConsoleSectionOutput;
@@ -25,6 +26,16 @@ use Symfony\Component\Console\Terminal;
*/
final class ProgressBar
{
public const FORMAT_VERBOSE = 'verbose';
public const FORMAT_VERY_VERBOSE = 'very_verbose';
public const FORMAT_DEBUG = 'debug';
public const FORMAT_NORMAL = 'normal';
private const FORMAT_VERBOSE_NOMAX = 'verbose_nomax';
private const FORMAT_VERY_VERBOSE_NOMAX = 'very_verbose_nomax';
private const FORMAT_DEBUG_NOMAX = 'debug_nomax';
private const FORMAT_NORMAL_NOMAX = 'normal_nomax';
private $barWidth = 28;
private $barChar;
private $emptyBarChar = '-';
@@ -47,6 +58,7 @@ final class ProgressBar
private $overwrite = true;
private $terminal;
private $previousMessage;
private $cursor;
private static $formatters;
private static $formats;
@@ -54,7 +66,7 @@ final class ProgressBar
/**
* @param int $max Maximum steps (0 if unknown)
*/
public function __construct(OutputInterface $output, int $max = 0, float $minSecondsBetweenRedraws = 0.1)
public function __construct(OutputInterface $output, int $max = 0, float $minSecondsBetweenRedraws = 1 / 25)
{
if ($output instanceof ConsoleOutputInterface) {
$output = $output->getErrorOutput();
@@ -78,6 +90,7 @@ final class ProgressBar
}
$this->startTime = time();
$this->cursor = new Cursor($output);
}
/**
@@ -110,7 +123,7 @@ final class ProgressBar
self::$formatters = self::initPlaceholderFormatters();
}
return isset(self::$formatters[$name]) ? self::$formatters[$name] : null;
return self::$formatters[$name] ?? null;
}
/**
@@ -143,7 +156,7 @@ final class ProgressBar
self::$formats = self::initFormats();
}
return isset(self::$formats[$name]) ? self::$formats[$name] : null;
return self::$formats[$name] ?? null;
}
/**
@@ -191,9 +204,27 @@ final class ProgressBar
return $this->percent;
}
public function getBarOffset(): int
public function getBarOffset(): float
{
return floor($this->max ? $this->percent * $this->barWidth : (null === $this->redrawFreq ? min(5, $this->barWidth / 15) * $this->writeCount : $this->step) % $this->barWidth);
return floor($this->max ? $this->percent * $this->barWidth : (null === $this->redrawFreq ? (int) (min(5, $this->barWidth / 15) * $this->writeCount) : $this->step) % $this->barWidth);
}
public function getEstimated(): float
{
if (!$this->step) {
return 0;
}
return round((time() - $this->startTime) / $this->step * $this->max);
}
public function getRemaining(): float
{
if (!$this->step) {
return 0;
}
return round((time() - $this->startTime) / $this->step * ($this->max - $this->step));
}
public function setBarWidth(int $size)
@@ -249,7 +280,7 @@ final class ProgressBar
/**
* Sets the redraw frequency.
*
* @param int|float $freq The frequency in steps
* @param int|null $freq The frequency in steps
*/
public function setRedrawFrequency(?int $freq)
{
@@ -357,7 +388,7 @@ final class ProgressBar
{
$this->format = null;
$this->max = max(0, $max);
$this->stepWidth = $this->max ? Helper::strlen((string) $this->max) : 4;
$this->stepWidth = $this->max ? Helper::width((string) $this->max) : 4;
}
/**
@@ -441,20 +472,26 @@ final class ProgressBar
if ($this->overwrite) {
if (null !== $this->previousMessage) {
if ($this->output instanceof ConsoleSectionOutput) {
$lines = floor(Helper::strlen($message) / $this->terminal->getWidth()) + $this->formatLineCount + 1;
$this->output->clear($lines);
$messageLines = explode("\n", $message);
$lineCount = \count($messageLines);
foreach ($messageLines as $messageLine) {
$messageLineLength = Helper::width(Helper::removeDecoration($this->output->getFormatter(), $messageLine));
if ($messageLineLength > $this->terminal->getWidth()) {
$lineCount += floor($messageLineLength / $this->terminal->getWidth());
}
}
$this->output->clear($lineCount);
} else {
// Erase previous lines
if ($this->formatLineCount > 0) {
$message = str_repeat("\x1B[1A\x1B[2K", $this->formatLineCount).$message;
$this->cursor->moveUp($this->formatLineCount);
}
// Move the cursor to the beginning of the line and erase the line
$message = "\x0D\x1B[2K$message";
$this->cursor->moveToColumn(1);
$this->cursor->clearLine();
}
}
} elseif ($this->step > 0) {
$message = PHP_EOL.$message;
$message = \PHP_EOL.$message;
}
$this->previousMessage = $originalMessage;
@@ -469,13 +506,13 @@ final class ProgressBar
switch ($this->output->getVerbosity()) {
// OutputInterface::VERBOSITY_QUIET: display is disabled anyway
case OutputInterface::VERBOSITY_VERBOSE:
return $this->max ? 'verbose' : 'verbose_nomax';
return $this->max ? self::FORMAT_VERBOSE : self::FORMAT_VERBOSE_NOMAX;
case OutputInterface::VERBOSITY_VERY_VERBOSE:
return $this->max ? 'very_verbose' : 'very_verbose_nomax';
return $this->max ? self::FORMAT_VERY_VERBOSE : self::FORMAT_VERY_VERBOSE_NOMAX;
case OutputInterface::VERBOSITY_DEBUG:
return $this->max ? 'debug' : 'debug_nomax';
return $this->max ? self::FORMAT_DEBUG : self::FORMAT_DEBUG_NOMAX;
default:
return $this->max ? 'normal' : 'normal_nomax';
return $this->max ? self::FORMAT_NORMAL : self::FORMAT_NORMAL_NOMAX;
}
}
@@ -486,7 +523,7 @@ final class ProgressBar
$completeBars = $bar->getBarOffset();
$display = str_repeat($bar->getBarCharacter(), $completeBars);
if ($completeBars < $bar->getBarWidth()) {
$emptyBars = $bar->getBarWidth() - $completeBars - Helper::strlenWithoutDecoration($output->getFormatter(), $bar->getProgressCharacter());
$emptyBars = $bar->getBarWidth() - $completeBars - Helper::length(Helper::removeDecoration($output->getFormatter(), $bar->getProgressCharacter()));
$display .= $bar->getProgressCharacter().str_repeat($bar->getEmptyBarCharacter(), $emptyBars);
}
@@ -500,32 +537,20 @@ final class ProgressBar
throw new LogicException('Unable to display the remaining time if the maximum number of steps is not set.');
}
if (!$bar->getProgress()) {
$remaining = 0;
} else {
$remaining = round((time() - $bar->getStartTime()) / $bar->getProgress() * ($bar->getMaxSteps() - $bar->getProgress()));
}
return Helper::formatTime($remaining);
return Helper::formatTime($bar->getRemaining());
},
'estimated' => function (self $bar) {
if (!$bar->getMaxSteps()) {
throw new LogicException('Unable to display the estimated time if the maximum number of steps is not set.');
}
if (!$bar->getProgress()) {
$estimated = 0;
} else {
$estimated = round((time() - $bar->getStartTime()) / $bar->getProgress() * $bar->getMaxSteps());
}
return Helper::formatTime($estimated);
return Helper::formatTime($bar->getEstimated());
},
'memory' => function (self $bar) {
return Helper::formatMemory(memory_get_usage(true));
},
'current' => function (self $bar) {
return str_pad($bar->getProgress(), $bar->getStepWidth(), ' ', STR_PAD_LEFT);
return str_pad($bar->getProgress(), $bar->getStepWidth(), ' ', \STR_PAD_LEFT);
},
'max' => function (self $bar) {
return $bar->getMaxSteps();
@@ -539,17 +564,17 @@ final class ProgressBar
private static function initFormats(): array
{
return [
'normal' => ' %current%/%max% [%bar%] %percent:3s%%',
'normal_nomax' => ' %current% [%bar%]',
self::FORMAT_NORMAL => ' %current%/%max% [%bar%] %percent:3s%%',
self::FORMAT_NORMAL_NOMAX => ' %current% [%bar%]',
'verbose' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%',
'verbose_nomax' => ' %current% [%bar%] %elapsed:6s%',
self::FORMAT_VERBOSE => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%',
self::FORMAT_VERBOSE_NOMAX => ' %current% [%bar%] %elapsed:6s%',
'very_verbose' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%',
'very_verbose_nomax' => ' %current% [%bar%] %elapsed:6s%',
self::FORMAT_VERY_VERBOSE => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%',
self::FORMAT_VERY_VERBOSE_NOMAX => ' %current% [%bar%] %elapsed:6s%',
'debug' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%',
'debug_nomax' => ' %current% [%bar%] %elapsed:6s% %memory:6s%',
self::FORMAT_DEBUG => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%',
self::FORMAT_DEBUG_NOMAX => ' %current% [%bar%] %elapsed:6s% %memory:6s%',
];
}
@@ -575,7 +600,7 @@ final class ProgressBar
// gets string length for each sub line with multiline format
$linesLength = array_map(function ($subLine) {
return Helper::strlenWithoutDecoration($this->output->getFormatter(), rtrim($subLine, "\r"));
return Helper::width(Helper::removeDecoration($this->output->getFormatter(), rtrim($subLine, "\r")));
}, explode("\n", $line));
$linesWidth = max($linesLength);
+3 -3
View File
@@ -142,7 +142,7 @@ class ProgressIndicator
self::$formats = self::initFormats();
}
return isset(self::$formats[$name]) ? self::$formats[$name] : null;
return self::$formats[$name] ?? null;
}
/**
@@ -170,7 +170,7 @@ class ProgressIndicator
self::$formatters = self::initPlaceholderFormatters();
}
return isset(self::$formatters[$name]) ? self::$formatters[$name] : null;
return self::$formatters[$name] ?? null;
}
private function display()
@@ -185,7 +185,7 @@ class ProgressIndicator
}
return $matches[0];
}, $this->format));
}, $this->format ?? ''));
}
private function determineBestFormat(): string
+164 -63
View File
@@ -11,6 +11,7 @@
namespace Symfony\Component\Console\Helper;
use Symfony\Component\Console\Cursor;
use Symfony\Component\Console\Exception\MissingInputException;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Formatter\OutputFormatter;
@@ -23,6 +24,7 @@ use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Terminal;
use function Symfony\Component\String\s;
/**
* The QuestionHelper class provides helpers to interact with the user.
@@ -33,7 +35,8 @@ class QuestionHelper extends Helper
{
private $inputStream;
private static $shell;
private static $stty;
private static $stty = true;
private static $stdinIsInteractive;
/**
* Asks a question to the user.
@@ -96,7 +99,7 @@ class QuestionHelper extends Helper
/**
* Asks the question to the user.
*
* @return bool|mixed|string|null
* @return mixed
*
* @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden
*/
@@ -104,10 +107,10 @@ class QuestionHelper extends Helper
{
$this->writePrompt($output, $question);
$inputStream = $this->inputStream ?: STDIN;
$inputStream = $this->inputStream ?: \STDIN;
$autocomplete = $question->getAutocompleterCallback();
if (null === $autocomplete || !Terminal::hasSttyAvailable()) {
if (null === $autocomplete || !self::$stty || !Terminal::hasSttyAvailable()) {
$ret = false;
if ($question->isHidden()) {
try {
@@ -121,7 +124,7 @@ class QuestionHelper extends Helper
}
if (false === $ret) {
$ret = fgets($inputStream, 4096);
$ret = $this->readInput($inputStream, $question);
if (false === $ret) {
throw new MissingInputException('Aborted.');
}
@@ -164,13 +167,13 @@ class QuestionHelper extends Helper
$choices = $question->getChoices();
if (!$question->isMultiselect()) {
return isset($choices[$default]) ? $choices[$default] : $default;
return $choices[$default] ?? $default;
}
$default = explode(',', $default);
foreach ($default as $k => $v) {
$v = $question->isTrimmable() ? trim($v) : $v;
$default[$k] = isset($choices[$v]) ? $choices[$v] : $v;
$default[$k] = $choices[$v] ?? $v;
}
}
@@ -202,10 +205,10 @@ class QuestionHelper extends Helper
{
$messages = [];
$maxWidth = max(array_map('self::strlen', array_keys($choices = $question->getChoices())));
$maxWidth = max(array_map('self::width', array_keys($choices = $question->getChoices())));
foreach ($choices as $key => $value) {
$padding = str_repeat(' ', $maxWidth - self::strlen($key));
$padding = str_repeat(' ', $maxWidth - self::width($key));
$messages[] = sprintf(" [<$tag>%s$padding</$tag>] %s", $key, $value);
}
@@ -234,6 +237,8 @@ class QuestionHelper extends Helper
*/
private function autocomplete(OutputInterface $output, Question $question, $inputStream, callable $autocomplete): string
{
$cursor = new Cursor($output, $inputStream);
$fullChoice = '';
$ret = '';
@@ -261,9 +266,9 @@ class QuestionHelper extends Helper
} elseif ("\177" === $c) { // Backspace Character
if (0 === $numMatches && 0 !== $i) {
--$i;
$cursor->moveLeft(s($fullChoice)->slice(-1)->width(false));
$fullChoice = self::substr($fullChoice, 0, $i);
// Move cursor backwards
$output->write("\033[1D");
}
if (0 === $i) {
@@ -301,12 +306,12 @@ class QuestionHelper extends Helper
$remainingCharacters = substr($ret, \strlen(trim($this->mostRecentlyEnteredValue($fullChoice))));
$output->write($remainingCharacters);
$fullChoice .= $remainingCharacters;
$i = self::strlen($fullChoice);
$i = (false === $encoding = mb_detect_encoding($fullChoice, null, true)) ? \strlen($fullChoice) : mb_strlen($fullChoice, $encoding);
$matches = array_filter(
$autocomplete($ret),
function ($match) use ($ret) {
return '' === $ret || 0 === strpos($match, $ret);
return '' === $ret || str_starts_with($match, $ret);
}
);
$numMatches = \count($matches);
@@ -343,23 +348,20 @@ class QuestionHelper extends Helper
foreach ($autocomplete($ret) as $value) {
// If typed characters match the beginning chunk of value (e.g. [AcmeDe]moBundle)
if (0 === strpos($value, $tempRet)) {
if (str_starts_with($value, $tempRet)) {
$matches[$numMatches++] = $value;
}
}
}
// Erase characters from cursor to end of line
$output->write("\033[K");
$cursor->clearLineAfter();
if ($numMatches > 0 && -1 !== $ofs) {
// Save cursor position
$output->write("\0337");
$cursor->savePosition();
// Write highlighted text, complete the partially entered response
$charactersEntered = \strlen(trim($this->mostRecentlyEnteredValue($fullChoice)));
$output->write('<hl>'.OutputFormatter::escapeTrailingBackslash(substr($matches[$ofs], $charactersEntered)).'</hl>');
// Restore cursor position
$output->write("\0338");
$cursor->restorePosition();
}
}
@@ -372,12 +374,12 @@ class QuestionHelper extends Helper
private function mostRecentlyEnteredValue(string $entered): string
{
// Determine the most recent value that the user entered
if (false === strpos($entered, ',')) {
if (!str_contains($entered, ',')) {
return $entered;
}
$choices = explode(',', $entered);
if (\strlen($lastChoice = trim($choices[\count($choices) - 1])) > 0) {
if ('' !== $lastChoice = trim($choices[\count($choices) - 1])) {
return $lastChoice;
}
@@ -404,7 +406,7 @@ class QuestionHelper extends Helper
$exe = $tmpExe;
}
$sExec = shell_exec($exe);
$sExec = shell_exec('"'.$exe.'"');
$value = $trimmable ? rtrim($sExec) : $sExec;
$output->writeln('');
@@ -415,35 +417,28 @@ class QuestionHelper extends Helper
return $value;
}
if (Terminal::hasSttyAvailable()) {
if (self::$stty && Terminal::hasSttyAvailable()) {
$sttyMode = shell_exec('stty -g');
shell_exec('stty -echo');
$value = fgets($inputStream, 4096);
} elseif ($this->isInteractiveInput($inputStream)) {
throw new RuntimeException('Unable to hide the response.');
}
$value = fgets($inputStream, 4096);
if (self::$stty && Terminal::hasSttyAvailable()) {
shell_exec(sprintf('stty %s', $sttyMode));
if (false === $value) {
throw new MissingInputException('Aborted.');
}
if ($trimmable) {
$value = trim($value);
}
$output->writeln('');
return $value;
}
if (false !== $shell = $this->getShell()) {
$readCmd = 'csh' === $shell ? 'set mypassword = $<' : 'read -r mypassword';
$command = sprintf("/usr/bin/env %s -c 'stty -echo; %s; stty echo; echo \$mypassword'", $shell, $readCmd);
$sCommand = shell_exec($command);
$value = $trimmable ? rtrim($sCommand) : $sCommand;
$output->writeln('');
return $value;
if (false === $value) {
throw new MissingInputException('Aborted.');
}
if ($trimmable) {
$value = trim($value);
}
$output->writeln('');
throw new RuntimeException('Unable to hide the response.');
return $value;
}
/**
@@ -459,6 +454,7 @@ class QuestionHelper extends Helper
{
$error = null;
$attempts = $question->getMaxAttempts();
while (null === $attempts || $attempts--) {
if (null !== $error) {
$this->writeError($output, $error);
@@ -475,30 +471,135 @@ class QuestionHelper extends Helper
throw $error;
}
/**
* Returns a valid unix shell.
*
* @return string|bool The valid shell name, false in case no valid shell is found
*/
private function getShell()
private function isInteractiveInput($inputStream): bool
{
if (null !== self::$shell) {
return self::$shell;
if ('php://stdin' !== (stream_get_meta_data($inputStream)['uri'] ?? null)) {
return false;
}
self::$shell = false;
if (null !== self::$stdinIsInteractive) {
return self::$stdinIsInteractive;
}
if (file_exists('/usr/bin/env')) {
// handle other OSs with bash/zsh/ksh/csh if available to hide the answer
$test = "/usr/bin/env %s -c 'echo OK' 2> /dev/null";
foreach (['bash', 'zsh', 'ksh', 'csh'] as $sh) {
if ('OK' === rtrim(shell_exec(sprintf($test, $sh)))) {
self::$shell = $sh;
break;
}
if (\function_exists('stream_isatty')) {
return self::$stdinIsInteractive = stream_isatty(fopen('php://stdin', 'r'));
}
if (\function_exists('posix_isatty')) {
return self::$stdinIsInteractive = posix_isatty(fopen('php://stdin', 'r'));
}
if (!\function_exists('exec')) {
return self::$stdinIsInteractive = true;
}
exec('stty 2> /dev/null', $output, $status);
return self::$stdinIsInteractive = 1 !== $status;
}
/**
* Reads one or more lines of input and returns what is read.
*
* @param resource $inputStream The handler resource
* @param Question $question The question being asked
*
* @return string|false The input received, false in case input could not be read
*/
private function readInput($inputStream, Question $question)
{
if (!$question->isMultiline()) {
$cp = $this->setIOCodepage();
$ret = fgets($inputStream, 4096);
return $this->resetIOCodepage($cp, $ret);
}
$multiLineStreamReader = $this->cloneInputStream($inputStream);
if (null === $multiLineStreamReader) {
return false;
}
$ret = '';
$cp = $this->setIOCodepage();
while (false !== ($char = fgetc($multiLineStreamReader))) {
if (\PHP_EOL === "{$ret}{$char}") {
break;
}
$ret .= $char;
}
return $this->resetIOCodepage($cp, $ret);
}
/**
* Sets console I/O to the host code page.
*
* @return int Previous code page in IBM/EBCDIC format
*/
private function setIOCodepage(): int
{
if (\function_exists('sapi_windows_cp_set')) {
$cp = sapi_windows_cp_get();
sapi_windows_cp_set(sapi_windows_cp_get('oem'));
return $cp;
}
return 0;
}
/**
* Sets console I/O to the specified code page and converts the user input.
*
* @param string|false $input
*
* @return string|false
*/
private function resetIOCodepage(int $cp, $input)
{
if (0 !== $cp) {
sapi_windows_cp_set($cp);
if (false !== $input && '' !== $input) {
$input = sapi_windows_cp_conv(sapi_windows_cp_get('oem'), $cp, $input);
}
}
return self::$shell;
return $input;
}
/**
* Clones an input stream in order to act on one instance of the same
* stream without affecting the other instance.
*
* @param resource $inputStream The handler resource
*
* @return resource|null The cloned resource, null in case it could not be cloned
*/
private function cloneInputStream($inputStream)
{
$streamMetaData = stream_get_meta_data($inputStream);
$seekable = $streamMetaData['seekable'] ?? false;
$mode = $streamMetaData['mode'] ?? 'rb';
$uri = $streamMetaData['uri'] ?? null;
if (null === $uri) {
return null;
}
$cloneStream = fopen($uri, $mode);
// For seekable and writable streams, add all the same data to the
// cloned stream and then seek to the same offset.
if (true === $seekable && !\in_array($mode, ['r', 'rb', 'rt'])) {
$offset = ftell($inputStream);
rewind($inputStream);
stream_copy_to_stream($inputStream, $cloneStream);
fseek($inputStream, $offset);
fseek($cloneStream, $offset);
}
return $cloneStream;
}
}
+14 -1
View File
@@ -33,6 +33,10 @@ class SymfonyQuestionHelper extends QuestionHelper
$text = OutputFormatter::escapeTrailingBackslash($question->getQuestion());
$default = $question->getDefault();
if ($question->isMultiline()) {
$text .= sprintf(' (press %s to continue)', $this->getEofShortcut());
}
switch (true) {
case null === $default:
$text = sprintf(' <info>%s</info>:', $text);
@@ -58,7 +62,7 @@ class SymfonyQuestionHelper extends QuestionHelper
case $question instanceof ChoiceQuestion:
$choices = $question->getChoices();
$text = sprintf(' <info>%s</info> [<comment>%s</comment>]:', $text, OutputFormatter::escape(isset($choices[$default]) ? $choices[$default] : $default));
$text = sprintf(' <info>%s</info> [<comment>%s</comment>]:', $text, OutputFormatter::escape($choices[$default] ?? $default));
break;
@@ -93,4 +97,13 @@ class SymfonyQuestionHelper extends QuestionHelper
parent::writeError($output, $error);
}
private function getEofShortcut(): string
{
if ('Windows' === \PHP_OS_FAMILY) {
return '<comment>Ctrl+Z</comment> then <comment>Enter</comment>';
}
return '<comment>Ctrl+D</comment>';
}
}
+61 -32
View File
@@ -218,7 +218,7 @@ class Table
public function setColumnMaxWidth(int $columnIndex, int $width): self
{
if (!$this->output->getFormatter() instanceof WrappableOutputFormatterInterface) {
throw new \LogicException(sprintf('Setting a maximum column width is only supported when using a "%s" formatter, got "%s".', WrappableOutputFormatterInterface::class, \get_class($this->output->getFormatter())));
throw new \LogicException(sprintf('Setting a maximum column width is only supported when using a "%s" formatter, got "%s".', WrappableOutputFormatterInterface::class, get_debug_type($this->output->getFormatter())));
}
$this->columnMaxWidths[$columnIndex] = $width;
@@ -362,6 +362,7 @@ class Table
$isHeader = !$this->horizontal;
$isFirstRow = $this->horizontal;
$hasTitle = (bool) $this->headerTitle;
foreach ($rows as $row) {
if ($divider === $row) {
$isHeader = false;
@@ -379,12 +380,13 @@ class Table
}
if ($isHeader || $isFirstRow) {
if ($isFirstRow) {
$this->renderRowSeparator(self::SEPARATOR_TOP_BOTTOM);
$isFirstRow = false;
} else {
$this->renderRowSeparator(self::SEPARATOR_TOP, $this->headerTitle, $this->style->getHeaderTitleFormat());
}
$this->renderRowSeparator(
$isHeader ? self::SEPARATOR_TOP : self::SEPARATOR_TOP_BOTTOM,
$hasTitle ? $this->headerTitle : null,
$hasTitle ? $this->style->getHeaderTitleFormat() : null
);
$isFirstRow = false;
$hasTitle = false;
}
if ($this->horizontal) {
$this->renderRow($row, $this->style->getCellRowFormat(), $this->style->getCellHeaderFormat());
@@ -418,13 +420,13 @@ class Table
$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;
@@ -434,15 +436,15 @@ class Table
}
if (null !== $title) {
$titleLength = Helper::strlenWithoutDecoration($formatter = $this->output->getFormatter(), $formattedTitle = sprintf($titleFormat, $title));
$markupLength = Helper::strlen($markup);
$titleLength = Helper::width(Helper::removeDecoration($formatter = $this->output->getFormatter(), $formattedTitle = sprintf($titleFormat, $title)));
$markupLength = Helper::width($markup);
if ($titleLength > $limit = $markupLength - 4) {
$titleLength = $limit;
$formatLength = Helper::strlenWithoutDecoration($formatter, sprintf($titleFormat, ''));
$formatLength = Helper::width(Helper::removeDecoration($formatter, sprintf($titleFormat, '')));
$formattedTitle = sprintf($titleFormat, Helper::substr($title, 0, $limit - $formatLength - 3).'...');
}
$titleStart = ($markupLength - $titleLength) / 2;
$titleStart = intdiv($markupLength - $titleLength, 2);
if (false === mb_detect_encoding($markup, null, true)) {
$markup = substr_replace($markup, $formattedTitle, $titleStart, $titleLength);
} else {
@@ -491,7 +493,7 @@ class Table
*/
private function renderCell(array $row, int $column, string $cellFormat): string
{
$cell = isset($row[$column]) ? $row[$column] : '';
$cell = $row[$column] ?? '';
$width = $this->effectiveColumnWidths[$column];
if ($cell instanceof TableCell && $cell->getColspan() > 1) {
// add the width of the following columns(numbers of colspan).
@@ -511,10 +513,33 @@ class Table
return sprintf($style->getBorderFormat(), str_repeat($style->getBorderChars()[2], $width));
}
$width += Helper::strlen($cell) - Helper::strlenWithoutDecoration($this->output->getFormatter(), $cell);
$width += Helper::length($cell) - Helper::length(Helper::removeDecoration($this->output->getFormatter(), $cell));
$content = sprintf($style->getCellRowContentFormat(), $cell);
return sprintf($cellFormat, str_pad($content, $width, $style->getPaddingChar(), $style->getPadType()));
$padType = $style->getPadType();
if ($cell instanceof TableCell && $cell->getStyle() instanceof TableCellStyle) {
$isNotStyledByTag = !preg_match('/^<(\w+|(\w+=[\w,]+;?)*)>.+<\/(\w+|(\w+=\w+;?)*)?>$/', $cell);
if ($isNotStyledByTag) {
$cellFormat = $cell->getStyle()->getCellFormat();
if (!\is_string($cellFormat)) {
$tag = http_build_query($cell->getStyle()->getTagOptions(), '', ';');
$cellFormat = '<'.$tag.'>%s</>';
}
if (strstr($content, '</>')) {
$content = str_replace('</>', '', $content);
$width -= 3;
}
if (strstr($content, '<fg=default;bg=default>')) {
$content = str_replace('<fg=default;bg=default>', '', $content);
$width -= \strlen('<fg=default;bg=default>');
}
}
$padType = $cell->getStyle()->getPadByAlign();
}
return sprintf($cellFormat, str_pad($content, $width, $style->getPaddingChar(), $padType));
}
/**
@@ -546,10 +571,10 @@ class Table
foreach ($rows[$rowKey] as $column => $cell) {
$colspan = $cell instanceof TableCell ? $cell->getColspan() : 1;
if (isset($this->columnMaxWidths[$column]) && Helper::strlenWithoutDecoration($formatter, $cell) > $this->columnMaxWidths[$column]) {
if (isset($this->columnMaxWidths[$column]) && Helper::width(Helper::removeDecoration($formatter, $cell)) > $this->columnMaxWidths[$column]) {
$cell = $formatter->formatAndWrap($cell, $this->columnMaxWidths[$column] * $colspan);
}
if (!strstr($cell, "\n")) {
if (!strstr($cell ?? '', "\n")) {
continue;
}
$escaped = implode("\n", array_map([OutputFormatter::class, 'escapeTrailingBackslash'], explode("\n", $cell)));
@@ -562,6 +587,9 @@ class Table
if (0 === $lineKey) {
$rows[$rowKey][$column] = $line;
} else {
if (!\array_key_exists($rowKey, $unmergedRows) || !\array_key_exists($lineKey, $unmergedRows[$rowKey])) {
$unmergedRows[$rowKey][$lineKey] = $this->copyRow($rows, $rowKey);
}
$unmergedRows[$rowKey][$lineKey][$column] = $line;
}
}
@@ -570,11 +598,11 @@ class Table
return new TableRows(function () use ($rows, $unmergedRows): \Traversable {
foreach ($rows as $rowKey => $row) {
yield $this->fillCells($row);
yield $row instanceof TableSeparator ? $row : $this->fillCells($row);
if (isset($unmergedRows[$rowKey])) {
foreach ($unmergedRows[$rowKey] as $row) {
yield $row;
yield $row instanceof TableSeparator ? $row : $this->fillCells($row);
}
}
}
@@ -606,7 +634,7 @@ class Table
$unmergedRows = [];
foreach ($rows[$line] as $column => $cell) {
if (null !== $cell && !$cell instanceof TableCell && !is_scalar($cell) && !(\is_object($cell) && method_exists($cell, '__toString'))) {
throw new InvalidArgumentException(sprintf('A cell must be a TableCell, a scalar or an object implementing "__toString()", "%s" given.', \gettype($cell)));
throw new InvalidArgumentException(sprintf('A cell must be a TableCell, a scalar or an object implementing "__toString()", "%s" given.', get_debug_type($cell)));
}
if ($cell instanceof TableCell && $cell->getRowspan() > 1) {
$nbLines = $cell->getRowspan() - 1;
@@ -615,15 +643,15 @@ class Table
$lines = explode("\n", str_replace("\n", "<fg=default;bg=default>\n</>", $cell));
$nbLines = \count($lines) > $nbLines ? substr_count($cell, "\n") : $nbLines;
$rows[$line][$column] = new TableCell($lines[0], ['colspan' => $cell->getColspan()]);
$rows[$line][$column] = new TableCell($lines[0], ['colspan' => $cell->getColspan(), 'style' => $cell->getStyle()]);
unset($lines[0]);
}
// create a two dimensional array (rowspan x colspan)
$unmergedRows = array_replace_recursive(array_fill($line + 1, $nbLines, []), $unmergedRows);
foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) {
$value = isset($lines[$unmergedRowKey - $line]) ? $lines[$unmergedRowKey - $line] : '';
$unmergedRows[$unmergedRowKey][$column] = new TableCell($value, ['colspan' => $cell->getColspan()]);
$value = $lines[$unmergedRowKey - $line] ?? '';
$unmergedRows[$unmergedRowKey][$column] = new TableCell($value, ['colspan' => $cell->getColspan(), 'style' => $cell->getStyle()]);
if ($nbLines === $unmergedRowKey - $line) {
break;
}
@@ -655,9 +683,10 @@ class Table
/**
* fill cells for a row that contains colspan > 1.
*/
private function fillCells($row)
private function fillCells(iterable $row)
{
$newRow = [];
foreach ($row as $column => $cell) {
$newRow[] = $cell;
if ($cell instanceof TableCell && $cell->getColspan() > 1) {
@@ -728,7 +757,7 @@ class Table
foreach ($row as $i => $cell) {
if ($cell instanceof TableCell) {
$textContent = Helper::removeDecoration($this->output->getFormatter(), $cell);
$textLength = Helper::strlen($textContent);
$textLength = Helper::width($textContent);
if ($textLength > 0) {
$contentColumns = str_split($textContent, ceil($textLength / $cell->getColspan()));
foreach ($contentColumns as $position => $content) {
@@ -741,13 +770,13 @@ class Table
$lengths[] = $this->getCellWidth($row, $column);
}
$this->effectiveColumnWidths[$column] = max($lengths) + Helper::strlen($this->style->getCellRowContentFormat()) - 2;
$this->effectiveColumnWidths[$column] = max($lengths) + Helper::width($this->style->getCellRowContentFormat()) - 2;
}
}
private function getColumnSeparatorWidth(): int
{
return Helper::strlen(sprintf($this->style->getBorderFormat(), $this->style->getBorderChars()[3]));
return Helper::width(sprintf($this->style->getBorderFormat(), $this->style->getBorderChars()[3]));
}
private function getCellWidth(array $row, int $column): int
@@ -756,10 +785,10 @@ class Table
if (isset($row[$column])) {
$cell = $row[$column];
$cellWidth = Helper::strlenWithoutDecoration($this->output->getFormatter(), $cell);
$cellWidth = Helper::width(Helper::removeDecoration($this->output->getFormatter(), $cell));
}
$columnWidth = isset($this->columnWidths[$column]) ? $this->columnWidths[$column] : 0;
$columnWidth = $this->columnWidths[$column] ?? 0;
$cellWidth = max($cellWidth, $columnWidth);
return isset($this->columnMaxWidths[$column]) ? min($this->columnMaxWidths[$column], $cellWidth) : $cellWidth;
+10
View File
@@ -22,6 +22,7 @@ class TableCell
private $options = [
'rowspan' => 1,
'colspan' => 1,
'style' => null,
];
public function __construct(string $value = '', array $options = [])
@@ -33,6 +34,10 @@ class TableCell
throw new InvalidArgumentException(sprintf('The TableCell does not support the following options: \'%s\'.', implode('\', \'', $diff)));
}
if (isset($options['style']) && !$options['style'] instanceof TableCellStyle) {
throw new InvalidArgumentException('The style option must be an instance of "TableCellStyle".');
}
$this->options = array_merge($this->options, $options);
}
@@ -65,4 +70,9 @@ class TableCell
{
return (int) $this->options['rowspan'];
}
public function getStyle(): ?TableCellStyle
{
return $this->options['style'];
}
}
+86
View File
@@ -0,0 +1,86 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Helper;
use Symfony\Component\Console\Exception\InvalidArgumentException;
/**
* @author Yewhen Khoptynskyi <khoptynskyi@gmail.com>
*/
class TableCellStyle
{
public const DEFAULT_ALIGN = 'left';
private $options = [
'fg' => 'default',
'bg' => 'default',
'options' => null,
'align' => self::DEFAULT_ALIGN,
'cellFormat' => null,
];
private $tagOptions = [
'fg',
'bg',
'options',
];
private $alignMap = [
'left' => \STR_PAD_RIGHT,
'center' => \STR_PAD_BOTH,
'right' => \STR_PAD_LEFT,
];
public function __construct(array $options = [])
{
if ($diff = array_diff(array_keys($options), array_keys($this->options))) {
throw new InvalidArgumentException(sprintf('The TableCellStyle does not support the following options: \'%s\'.', implode('\', \'', $diff)));
}
if (isset($options['align']) && !\array_key_exists($options['align'], $this->alignMap)) {
throw new InvalidArgumentException(sprintf('Wrong align value. Value must be following: \'%s\'.', implode('\', \'', array_keys($this->alignMap))));
}
$this->options = array_merge($this->options, $options);
}
public function getOptions(): array
{
return $this->options;
}
/**
* Gets options we need for tag for example fg, bg.
*
* @return string[]
*/
public function getTagOptions()
{
return array_filter(
$this->getOptions(),
function ($key) {
return \in_array($key, $this->tagOptions) && isset($this->options[$key]);
},
\ARRAY_FILTER_USE_KEY
);
}
public function getPadByAlign()
{
return $this->alignMap[$this->getOptions()['align']];
}
public function getCellFormat(): ?string
{
return $this->getOptions()['cellFormat'];
}
}
+2 -2
View File
@@ -46,7 +46,7 @@ class TableStyle
private $cellRowFormat = '%s';
private $cellRowContentFormat = ' %s ';
private $borderFormat = '%s';
private $padType = STR_PAD_RIGHT;
private $padType = \STR_PAD_RIGHT;
/**
* Sets padding character, used for cell padding.
@@ -319,7 +319,7 @@ class TableStyle
*/
public function setPadType(int $padType)
{
if (!\in_array($padType, [STR_PAD_LEFT, STR_PAD_RIGHT, STR_PAD_BOTH], true)) {
if (!\in_array($padType, [\STR_PAD_LEFT, \STR_PAD_RIGHT, \STR_PAD_BOTH], true)) {
throw new InvalidArgumentException('Invalid padding type. Expected one of (STR_PAD_LEFT, STR_PAD_RIGHT, STR_PAD_BOTH).');
}
+35 -13
View File
@@ -45,9 +45,7 @@ class ArgvInput extends Input
public function __construct(array $argv = null, InputDefinition $definition = null)
{
if (null === $argv) {
$argv = $_SERVER['argv'];
}
$argv = $argv ?? $_SERVER['argv'] ?? [];
// strip the application name
array_shift($argv);
@@ -74,7 +72,7 @@ class ArgvInput extends Input
$this->parseArgument($token);
} elseif ($parseOptions && '--' == $token) {
$parseOptions = false;
} elseif ($parseOptions && 0 === strpos($token, '--')) {
} elseif ($parseOptions && str_starts_with($token, '--')) {
$this->parseLongOption($token);
} elseif ($parseOptions && '-' === $token[0] && '-' !== $token) {
$this->parseShortOption($token);
@@ -167,11 +165,25 @@ class ArgvInput extends Input
// unexpected argument
} else {
$all = $this->definition->getArguments();
if (\count($all)) {
throw new RuntimeException(sprintf('Too many arguments, expected arguments "%s".', implode('" "', array_keys($all))));
$symfonyCommandName = null;
if (($inputArgument = $all[$key = array_key_first($all)] ?? null) && 'command' === $inputArgument->getName()) {
$symfonyCommandName = $this->arguments['command'] ?? null;
unset($all[$key]);
}
throw new RuntimeException(sprintf('No arguments expected, got "%s".', $token));
if (\count($all)) {
if ($symfonyCommandName) {
$message = sprintf('Too many arguments to "%s" command, expected arguments "%s".', $symfonyCommandName, implode('" "', array_keys($all)));
} else {
$message = sprintf('Too many arguments, expected arguments "%s".', implode('" "', array_keys($all)));
}
} elseif ($symfonyCommandName) {
$message = sprintf('No arguments expected for "%s" command, got "%s".', $symfonyCommandName, $token);
} else {
$message = sprintf('No arguments expected, got "%s".', $token);
}
throw new RuntimeException($message);
}
}
@@ -197,7 +209,17 @@ class ArgvInput extends Input
private function addLongOption(string $name, $value)
{
if (!$this->definition->hasOption($name)) {
throw new RuntimeException(sprintf('The "--%s" option does not exist.', $name));
if (!$this->definition->hasNegation($name)) {
throw new RuntimeException(sprintf('The "--%s" option does not exist.', $name));
}
$optionName = $this->definition->negationToName($name);
if (null !== $value) {
throw new RuntimeException(sprintf('The "--%s" option does not accept a value.', $name));
}
$this->options[$optionName] = false;
return;
}
$option = $this->definition->getOption($name);
@@ -242,7 +264,7 @@ class ArgvInput extends Input
$isOption = false;
foreach ($this->tokens as $i => $token) {
if ($token && '-' === $token[0]) {
if (false !== strpos($token, '=') || !isset($this->tokens[$i + 1])) {
if (str_contains($token, '=') || !isset($this->tokens[$i + 1])) {
continue;
}
@@ -284,8 +306,8 @@ class ArgvInput extends Input
// Options with values:
// For long options, test for '--option=' at beginning
// For short options, test for '-o' at beginning
$leading = 0 === strpos($value, '--') ? $value.'=' : $value;
if ($token === $value || '' !== $leading && 0 === strpos($token, $leading)) {
$leading = str_starts_with($value, '--') ? $value.'=' : $value;
if ($token === $value || '' !== $leading && str_starts_with($token, $leading)) {
return true;
}
}
@@ -315,8 +337,8 @@ class ArgvInput extends Input
// Options with values:
// For long options, test for '--option=' at beginning
// For short options, test for '-o' at beginning
$leading = 0 === strpos($value, '--') ? $value.'=' : $value;
if ('' !== $leading && 0 === strpos($token, $leading)) {
$leading = str_starts_with($value, '--') ? $value.'=' : $value;
if ('' !== $leading && str_starts_with($token, $leading)) {
return substr($token, \strlen($leading));
}
}
+13 -5
View File
@@ -108,12 +108,13 @@ class ArrayInput extends Input
$params = [];
foreach ($this->parameters as $param => $val) {
if ($param && \is_string($param) && '-' === $param[0]) {
$glue = ('-' === $param[1]) ? '=' : ' ';
if (\is_array($val)) {
foreach ($val as $v) {
$params[] = $param.('' != $v ? '='.$this->escapeToken($v) : '');
$params[] = $param.('' != $v ? $glue.$this->escapeToken($v) : '');
}
} else {
$params[] = $param.('' != $val ? '='.$this->escapeToken($val) : '');
$params[] = $param.('' != $val ? $glue.$this->escapeToken($val) : '');
}
} else {
$params[] = \is_array($val) ? implode(' ', array_map([$this, 'escapeToken'], $val)) : $this->escapeToken($val);
@@ -132,9 +133,9 @@ class ArrayInput extends Input
if ('--' === $key) {
return;
}
if (0 === strpos($key, '--')) {
if (str_starts_with($key, '--')) {
$this->addLongOption(substr($key, 2), $value);
} elseif (0 === strpos($key, '-')) {
} elseif (str_starts_with($key, '-')) {
$this->addShortOption(substr($key, 1), $value);
} else {
$this->addArgument($key, $value);
@@ -165,7 +166,14 @@ class ArrayInput extends Input
private function addLongOption(string $name, $value)
{
if (!$this->definition->hasOption($name)) {
throw new InvalidOptionException(sprintf('The "--%s" option does not exist.', $name));
if (!$this->definition->hasNegation($name)) {
throw new InvalidOptionException(sprintf('The "--%s" option does not exist.', $name));
}
$optionName = $this->definition->negationToName($name);
$this->options[$optionName] = false;
return;
}
$option = $this->definition->getOption($name);
+16 -4
View File
@@ -110,7 +110,7 @@ abstract class Input implements InputInterface, StreamableInputInterface
throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name));
}
return isset($this->arguments[$name]) ? $this->arguments[$name] : $this->definition->getArgument($name)->getDefault();
return $this->arguments[$name] ?? $this->definition->getArgument($name)->getDefault();
}
/**
@@ -128,7 +128,7 @@ abstract class Input implements InputInterface, StreamableInputInterface
/**
* {@inheritdoc}
*/
public function hasArgument($name)
public function hasArgument(string $name)
{
return $this->definition->hasArgument($name);
}
@@ -146,6 +146,14 @@ abstract class Input implements InputInterface, StreamableInputInterface
*/
public function getOption(string $name)
{
if ($this->definition->hasNegation($name)) {
if (null === $value = $this->getOption($this->definition->negationToName($name))) {
return $value;
}
return !$value;
}
if (!$this->definition->hasOption($name)) {
throw new InvalidArgumentException(sprintf('The "%s" option does not exist.', $name));
}
@@ -158,7 +166,11 @@ abstract class Input implements InputInterface, StreamableInputInterface
*/
public function setOption(string $name, $value)
{
if (!$this->definition->hasOption($name)) {
if ($this->definition->hasNegation($name)) {
$this->options[$this->definition->negationToName($name)] = !$value;
return;
} elseif (!$this->definition->hasOption($name)) {
throw new InvalidArgumentException(sprintf('The "%s" option does not exist.', $name));
}
@@ -170,7 +182,7 @@ abstract class Input implements InputInterface, StreamableInputInterface
*/
public function hasOption(string $name)
{
return $this->definition->hasOption($name);
return $this->definition->hasOption($name) || $this->definition->hasNegation($name);
}
/**
+9 -9
View File
@@ -21,9 +21,9 @@ use Symfony\Component\Console\Exception\LogicException;
*/
class InputArgument
{
const REQUIRED = 1;
const OPTIONAL = 2;
const IS_ARRAY = 4;
public const REQUIRED = 1;
public const OPTIONAL = 2;
public const IS_ARRAY = 4;
private $name;
private $mode;
@@ -31,10 +31,10 @@ class InputArgument
private $description;
/**
* @param string $name The argument name
* @param int|null $mode The argument mode: self::REQUIRED or self::OPTIONAL
* @param string $description A description text
* @param string|string[]|null $default The default value (for self::OPTIONAL mode only)
* @param string $name The argument name
* @param int|null $mode The argument mode: self::REQUIRED or self::OPTIONAL
* @param string $description A description text
* @param string|bool|int|float|array|null $default The default value (for self::OPTIONAL mode only)
*
* @throws InvalidArgumentException When argument mode is not valid
*/
@@ -86,7 +86,7 @@ class InputArgument
/**
* Sets the default value.
*
* @param string|string[]|null $default The default value
* @param string|bool|int|float|array|null $default
*
* @throws LogicException When incorrect default value is given
*/
@@ -110,7 +110,7 @@ class InputArgument
/**
* Returns the default value.
*
* @return string|string[]|null The default value
* @return string|bool|int|float|array|null
*/
public function getDefault()
{
+52 -18
View File
@@ -30,9 +30,10 @@ class InputDefinition
{
private $arguments;
private $requiredCount;
private $hasAnArrayArgument = false;
private $hasOptional;
private $lastArrayArgument;
private $lastOptionalArgument;
private $options;
private $negations;
private $shortcuts;
/**
@@ -71,8 +72,8 @@ class InputDefinition
{
$this->arguments = [];
$this->requiredCount = 0;
$this->hasOptional = false;
$this->hasAnArrayArgument = false;
$this->lastOptionalArgument = null;
$this->lastArrayArgument = null;
$this->addArguments($arguments);
}
@@ -99,22 +100,22 @@ class InputDefinition
throw new LogicException(sprintf('An argument with name "%s" already exists.', $argument->getName()));
}
if ($this->hasAnArrayArgument) {
throw new LogicException('Cannot add an argument after an array argument.');
if (null !== $this->lastArrayArgument) {
throw new LogicException(sprintf('Cannot add a required argument "%s" after an array argument "%s".', $argument->getName(), $this->lastArrayArgument->getName()));
}
if ($argument->isRequired() && $this->hasOptional) {
throw new LogicException('Cannot add a required argument after an optional one.');
if ($argument->isRequired() && null !== $this->lastOptionalArgument) {
throw new LogicException(sprintf('Cannot add a required argument "%s" after an optional one "%s".', $argument->getName(), $this->lastOptionalArgument->getName()));
}
if ($argument->isArray()) {
$this->hasAnArrayArgument = true;
$this->lastArrayArgument = $argument;
}
if ($argument->isRequired()) {
++$this->requiredCount;
} else {
$this->hasOptional = true;
$this->lastOptionalArgument = $argument;
}
$this->arguments[$argument->getName()] = $argument;
@@ -171,7 +172,7 @@ class InputDefinition
*/
public function getArgumentCount()
{
return $this->hasAnArrayArgument ? PHP_INT_MAX : \count($this->arguments);
return null !== $this->lastArrayArgument ? \PHP_INT_MAX : \count($this->arguments);
}
/**
@@ -185,9 +186,7 @@ class InputDefinition
}
/**
* Gets the default values.
*
* @return array An array of default values
* @return array<string|bool|int|float|array|null>
*/
public function getArgumentDefaults()
{
@@ -208,6 +207,7 @@ class InputDefinition
{
$this->options = [];
$this->shortcuts = [];
$this->negations = [];
$this->addOptions($options);
}
@@ -231,6 +231,9 @@ class InputDefinition
if (isset($this->options[$option->getName()]) && !$option->equals($this->options[$option->getName()])) {
throw new LogicException(sprintf('An option named "%s" already exists.', $option->getName()));
}
if (isset($this->negations[$option->getName()])) {
throw new LogicException(sprintf('An option named "%s" already exists.', $option->getName()));
}
if ($option->getShortcut()) {
foreach (explode('|', $option->getShortcut()) as $shortcut) {
@@ -246,6 +249,14 @@ class InputDefinition
$this->shortcuts[$shortcut] = $option->getName();
}
}
if ($option->isNegatable()) {
$negatedName = 'no-'.$option->getName();
if (isset($this->options[$negatedName])) {
throw new LogicException(sprintf('An option named "%s" already exists.', $negatedName));
}
$this->negations[$negatedName] = $option->getName();
}
}
/**
@@ -297,6 +308,14 @@ class InputDefinition
return isset($this->shortcuts[$name]);
}
/**
* Returns true if an InputOption object exists by negated name.
*/
public function hasNegation(string $name): bool
{
return isset($this->negations[$name]);
}
/**
* Gets an InputOption by shortcut.
*
@@ -308,9 +327,7 @@ class InputDefinition
}
/**
* Gets an array of default values.
*
* @return array An array of all default values
* @return array<string|bool|int|float|array|null>
*/
public function getOptionDefaults()
{
@@ -338,6 +355,22 @@ class InputDefinition
return $this->shortcuts[$shortcut];
}
/**
* Returns the InputOption name given a negation.
*
* @throws InvalidArgumentException When option given does not exist
*
* @internal
*/
public function negationToName(string $negation): string
{
if (!isset($this->negations[$negation])) {
throw new InvalidArgumentException(sprintf('The "--%s" option does not exist.', $negation));
}
return $this->negations[$negation];
}
/**
* Gets the synopsis.
*
@@ -362,7 +395,8 @@ class InputDefinition
}
$shortcut = $option->getShortcut() ? sprintf('-%s|', $option->getShortcut()) : '';
$elements[] = sprintf('[%s--%s%s]', $shortcut, $option->getName(), $value);
$negation = $option->isNegatable() ? sprintf('|--no-%s', $option->getName()) : '';
$elements[] = sprintf('[%s--%s%s%s]', $shortcut, $option->getName(), $value, $negation);
}
}
+10 -12
View File
@@ -51,9 +51,9 @@ interface InputInterface
* Does not necessarily return the correct result for short options
* when multiple flags are combined in the same option.
*
* @param string|array $values The value(s) to look for in the raw parameters (can be an array)
* @param mixed $default The default value to return if no result is found
* @param bool $onlyParams Only check real parameters, skip those following an end of options (--) signal
* @param string|array $values The value(s) to look for in the raw parameters (can be an array)
* @param string|bool|int|float|array|null $default The default value to return if no result is found
* @param bool $onlyParams Only check real parameters, skip those following an end of options (--) signal
*
* @return mixed The option value
*/
@@ -76,14 +76,14 @@ interface InputInterface
/**
* Returns all the given arguments merged with the default values.
*
* @return array
* @return array<string|bool|int|float|array|null>
*/
public function getArguments();
/**
* Returns the argument value for a given argument name.
*
* @return string|string[]|null The argument value
* @return mixed
*
* @throws InvalidArgumentException When argument given doesn't exist
*/
@@ -92,7 +92,7 @@ interface InputInterface
/**
* Sets an argument value by name.
*
* @param string|string[]|null $value The argument value
* @param mixed $value The argument value
*
* @throws InvalidArgumentException When argument given doesn't exist
*/
@@ -101,23 +101,21 @@ interface InputInterface
/**
* Returns true if an InputArgument object exists by name or position.
*
* @param string|int $name The InputArgument name or position
*
* @return bool true if the InputArgument object exists, false otherwise
*/
public function hasArgument($name);
public function hasArgument(string $name);
/**
* Returns all the given options merged with the default values.
*
* @return array
* @return array<string|bool|int|float|array|null>
*/
public function getOptions();
/**
* Returns the option value for a given option name.
*
* @return string|string[]|bool|null The option value
* @return mixed
*
* @throws InvalidArgumentException When option given doesn't exist
*/
@@ -126,7 +124,7 @@ interface InputInterface
/**
* Sets an option value by name.
*
* @param string|string[]|bool|null $value The option value
* @param mixed $value The option value
*
* @throws InvalidArgumentException When option given doesn't exist
*/
+43 -18
View File
@@ -21,10 +21,30 @@ use Symfony\Component\Console\Exception\LogicException;
*/
class InputOption
{
const VALUE_NONE = 1;
const VALUE_REQUIRED = 2;
const VALUE_OPTIONAL = 4;
const VALUE_IS_ARRAY = 8;
/**
* Do not accept input for the option (e.g. --yell). This is the default behavior of options.
*/
public const VALUE_NONE = 1;
/**
* A value must be passed when the option is used (e.g. --iterations=5 or -i5).
*/
public const VALUE_REQUIRED = 2;
/**
* The option may or may not have a value (e.g. --yell or --yell=loud).
*/
public const VALUE_OPTIONAL = 4;
/**
* The option accepts multiple values (e.g. --dir=/foo --dir=/bar).
*/
public const VALUE_IS_ARRAY = 8;
/**
* The option may have either positive or negative value (e.g. --ansi or --no-ansi).
*/
public const VALUE_NEGATABLE = 16;
private $name;
private $shortcut;
@@ -33,17 +53,17 @@ class InputOption
private $description;
/**
* @param string $name The option name
* @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
* @param int|null $mode The option mode: One of the VALUE_* constants
* @param string $description A description text
* @param string|string[]|int|bool|null $default The default value (must be null for self::VALUE_NONE)
* @param string $name The option name
* @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
* @param int|null $mode The option mode: One of the VALUE_* constants
* @param string $description A description text
* @param string|bool|int|float|array|null $default The default value (must be null for self::VALUE_NONE)
*
* @throws InvalidArgumentException If option mode is invalid or incompatible
*/
public function __construct(string $name, $shortcut = null, int $mode = null, string $description = '', $default = null)
{
if (0 === strpos($name, '--')) {
if (str_starts_with($name, '--')) {
$name = substr($name, 2);
}
@@ -70,7 +90,7 @@ class InputOption
if (null === $mode) {
$mode = self::VALUE_NONE;
} elseif ($mode > 15 || $mode < 1) {
} elseif ($mode >= (self::VALUE_NEGATABLE << 1) || $mode < 1) {
throw new InvalidArgumentException(sprintf('Option mode "%s" is not valid.', $mode));
}
@@ -82,6 +102,9 @@ class InputOption
if ($this->isArray() && !$this->acceptValue()) {
throw new InvalidArgumentException('Impossible to have an option mode VALUE_IS_ARRAY if the option does not accept a value.');
}
if ($this->isNegatable() && $this->acceptValue()) {
throw new InvalidArgumentException('Impossible to have an option mode VALUE_NEGATABLE if the option also accepts a value.');
}
$this->setDefault($default);
}
@@ -146,12 +169,13 @@ class InputOption
return self::VALUE_IS_ARRAY === (self::VALUE_IS_ARRAY & $this->mode);
}
public function isNegatable(): bool
{
return self::VALUE_NEGATABLE === (self::VALUE_NEGATABLE & $this->mode);
}
/**
* Sets the default value.
*
* @param string|string[]|int|bool|null $default The default value
*
* @throws LogicException When incorrect default value is given
* @param string|bool|int|float|array|null $default
*/
public function setDefault($default = null)
{
@@ -167,13 +191,13 @@ class InputOption
}
}
$this->default = $this->acceptValue() ? $default : false;
$this->default = $this->acceptValue() || $this->isNegatable() ? $default : false;
}
/**
* Returns the default value.
*
* @return string|string[]|int|bool|null The default value
* @return string|bool|int|float|array|null
*/
public function getDefault()
{
@@ -200,6 +224,7 @@ class InputOption
return $option->getName() === $this->getName()
&& $option->getShortcut() === $this->getShortcut()
&& $option->getDefault() === $this->getDefault()
&& $option->isNegatable() === $this->isNegatable()
&& $option->isArray() === $this->isArray()
&& $option->isValueRequired() === $this->isValueRequired()
&& $option->isValueOptional() === $this->isValueOptional()
+8 -8
View File
@@ -24,8 +24,8 @@ use Symfony\Component\Console\Exception\InvalidArgumentException;
*/
class StringInput extends ArgvInput
{
const REGEX_STRING = '([^\s]+?)(?:\s|(?<!\\\\)"|(?<!\\\\)\'|$)';
const REGEX_QUOTED_STRING = '(?:"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\')';
public const REGEX_STRING = '([^\s]+?)(?:\s|(?<!\\\\)"|(?<!\\\\)\'|$)';
public const REGEX_QUOTED_STRING = '(?:"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\')';
/**
* @param string $input A string representing the parameters from the CLI
@@ -48,12 +48,12 @@ class StringInput extends ArgvInput
$length = \strlen($input);
$cursor = 0;
while ($cursor < $length) {
if (preg_match('/\s+/A', $input, $match, null, $cursor)) {
} elseif (preg_match('/([^="\'\s]+?)(=?)('.self::REGEX_QUOTED_STRING.'+)/A', $input, $match, null, $cursor)) {
$tokens[] = $match[1].$match[2].stripcslashes(str_replace(['"\'', '\'"', '\'\'', '""'], '', substr($match[3], 1, \strlen($match[3]) - 2)));
} elseif (preg_match('/'.self::REGEX_QUOTED_STRING.'/A', $input, $match, null, $cursor)) {
$tokens[] = stripcslashes(substr($match[0], 1, \strlen($match[0]) - 2));
} elseif (preg_match('/'.self::REGEX_STRING.'/A', $input, $match, null, $cursor)) {
if (preg_match('/\s+/A', $input, $match, 0, $cursor)) {
} elseif (preg_match('/([^="\'\s]+?)(=?)('.self::REGEX_QUOTED_STRING.'+)/A', $input, $match, 0, $cursor)) {
$tokens[] = $match[1].$match[2].stripcslashes(str_replace(['"\'', '\'"', '\'\'', '""'], '', substr($match[3], 1, -1)));
} elseif (preg_match('/'.self::REGEX_QUOTED_STRING.'/A', $input, $match, 0, $cursor)) {
$tokens[] = stripcslashes(substr($match[0], 1, -1));
} elseif (preg_match('/'.self::REGEX_STRING.'/A', $input, $match, 0, $cursor)) {
$tokens[] = stripcslashes($match[1]);
} else {
// should never happen
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2004-2020 Fabien Potencier
Copyright (c) 2004-2021 Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+3 -3
View File
@@ -26,8 +26,8 @@ use Symfony\Component\Console\Output\OutputInterface;
*/
class ConsoleLogger extends AbstractLogger
{
const INFO = 'info';
const ERROR = 'error';
public const INFO = 'info';
public const ERROR = 'error';
private $output;
private $verbosityLevelMap = [
@@ -104,7 +104,7 @@ class ConsoleLogger extends AbstractLogger
*/
private function interpolate(string $message, array $context): string
{
if (false === strpos($message, '{')) {
if (!str_contains($message, '{')) {
return $message;
}
+1 -1
View File
@@ -39,7 +39,7 @@ class BufferedOutput extends Output
$this->buffer .= $message;
if ($newline) {
$this->buffer .= PHP_EOL;
$this->buffer .= \PHP_EOL;
}
}
}
+8 -1
View File
@@ -41,6 +41,13 @@ class ConsoleOutput extends StreamOutput implements ConsoleOutputInterface
{
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());
@@ -131,7 +138,7 @@ class ConsoleOutput extends StreamOutput implements ConsoleOutputInterface
$checks = [
\function_exists('php_uname') ? php_uname('s') : '',
getenv('OSTYPE'),
PHP_OS,
\PHP_OS,
];
return false !== stripos(implode(';', $checks), 'OS400');
+5 -5
View File
@@ -82,17 +82,17 @@ class ConsoleSectionOutput extends StreamOutput
*/
public function addContent(string $input)
{
foreach (explode(PHP_EOL, $input) as $lineContent) {
foreach (explode(\PHP_EOL, $input) as $lineContent) {
$this->lines += ceil($this->getDisplayLength($lineContent) / $this->terminal->getWidth()) ?: 1;
$this->content[] = $lineContent;
$this->content[] = PHP_EOL;
$this->content[] = \PHP_EOL;
}
}
/**
* {@inheritdoc}
*/
protected function doWrite($message, $newline)
protected function doWrite(string $message, bool $newline)
{
if (!$this->isDecorated()) {
parent::doWrite($message, $newline);
@@ -136,8 +136,8 @@ class ConsoleSectionOutput extends StreamOutput
return implode('', array_reverse($erasedContent));
}
private function getDisplayLength(string $text): string
private function getDisplayLength(string $text): int
{
return Helper::strlenWithoutDecoration($this->getFormatter(), str_replace("\t", ' ', $text));
return Helper::width(Helper::removeDecoration($this->getFormatter(), str_replace("\t", ' ', $text)));
}
}
+7 -2
View File
@@ -11,7 +11,7 @@
namespace Symfony\Component\Console\Output;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Formatter\NullOutputFormatter;
use Symfony\Component\Console\Formatter\OutputFormatterInterface;
/**
@@ -24,6 +24,8 @@ use Symfony\Component\Console\Formatter\OutputFormatterInterface;
*/
class NullOutput implements OutputInterface
{
private $formatter;
/**
* {@inheritdoc}
*/
@@ -37,8 +39,11 @@ class NullOutput implements OutputInterface
*/
public function getFormatter()
{
if ($this->formatter) {
return $this->formatter;
}
// to comply with the interface we must return a OutputFormatterInterface
return new OutputFormatter();
return $this->formatter = new NullOutputFormatter();
}
/**
+2 -2
View File
@@ -40,7 +40,7 @@ abstract class Output implements OutputInterface
public function __construct(?int $verbosity = self::VERBOSITY_NORMAL, bool $decorated = false, OutputFormatterInterface $formatter = null)
{
$this->verbosity = null === $verbosity ? self::VERBOSITY_NORMAL : $verbosity;
$this->formatter = $formatter ?: new OutputFormatter();
$this->formatter = $formatter ?? new OutputFormatter();
$this->formatter->setDecorated($decorated);
}
@@ -163,7 +163,7 @@ abstract class Output implements OutputInterface
break;
}
$this->doWrite($message, $newline);
$this->doWrite($message ?? '', $newline);
}
}
+8 -8
View File
@@ -20,15 +20,15 @@ use Symfony\Component\Console\Formatter\OutputFormatterInterface;
*/
interface OutputInterface
{
const VERBOSITY_QUIET = 16;
const VERBOSITY_NORMAL = 32;
const VERBOSITY_VERBOSE = 64;
const VERBOSITY_VERY_VERBOSE = 128;
const VERBOSITY_DEBUG = 256;
public const VERBOSITY_QUIET = 16;
public const VERBOSITY_NORMAL = 32;
public const VERBOSITY_VERBOSE = 64;
public const VERBOSITY_VERY_VERBOSE = 128;
public const VERBOSITY_DEBUG = 256;
const OUTPUT_NORMAL = 1;
const OUTPUT_RAW = 2;
const OUTPUT_PLAIN = 4;
public const OUTPUT_NORMAL = 1;
public const OUTPUT_RAW = 2;
public const OUTPUT_PLAIN = 4;
/**
* Writes a message to the output.
+1 -1
View File
@@ -70,7 +70,7 @@ class StreamOutput extends Output
protected function doWrite(string $message, bool $newline)
{
if ($newline) {
$message .= PHP_EOL;
$message .= \PHP_EOL;
}
@fwrite($this->stream, $message);
+62
View File
@@ -0,0 +1,62 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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é <jeremy@derusse.com>
*/
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(string $message, bool $newline)
{
$this->buffer .= $message;
if ($newline) {
$this->buffer .= \PHP_EOL;
}
$this->buffer = substr($this->buffer, 0 - $this->maxLength);
}
}
+2 -1
View File
@@ -169,7 +169,8 @@ class ChoiceQuestion extends Question
throw new InvalidArgumentException(sprintf($errorMessage, $value));
}
$multiselectChoices[] = (string) $result;
// For associative choices, consistently return the key as string:
$multiselectChoices[] = $isAssoc ? (string) $result : $result;
}
if ($multiselect) {
+31 -11
View File
@@ -30,10 +30,11 @@ class Question
private $default;
private $normalizer;
private $trimmable = true;
private $multiline = false;
/**
* @param string $question The question to ask to the user
* @param mixed $default The default answer to return if the user enters nothing
* @param string $question The question to ask to the user
* @param string|bool|int|float|null $default The default answer to return if the user enters nothing
*/
public function __construct(string $question, $default = null)
{
@@ -54,13 +55,33 @@ class Question
/**
* Returns the default answer.
*
* @return mixed
* @return string|bool|int|float|null
*/
public function getDefault()
{
return $this->default;
}
/**
* Returns whether the user response accepts newline characters.
*/
public function isMultiline(): bool
{
return $this->multiline;
}
/**
* Sets whether the user response should accept newline characters.
*
* @return $this
*/
public function setMultiline(bool $multiline): self
{
$this->multiline = $multiline;
return $this;
}
/**
* Returns whether the user response must be hidden.
*
@@ -74,13 +95,11 @@ class Question
/**
* Sets whether the user response must be hidden or not.
*
* @param bool $hidden
*
* @return $this
*
* @throws LogicException In case the autocompleter is also used
*/
public function setHidden($hidden)
public function setHidden(bool $hidden)
{
if ($this->autocompleterCallback) {
throw new LogicException('A hidden question cannot use the autocompleter.');
@@ -104,11 +123,9 @@ class Question
/**
* Sets whether to fallback on non-hidden question if the response can not be hidden.
*
* @param bool $fallback
*
* @return $this
*/
public function setHiddenFallback($fallback)
public function setHiddenFallback(bool $fallback)
{
$this->hiddenFallback = (bool) $fallback;
@@ -213,8 +230,11 @@ class Question
*/
public function setMaxAttempts(?int $attempts)
{
if (null !== $attempts && $attempts < 1) {
throw new InvalidArgumentException('Maximum number of attempts must be a positive value.');
if (null !== $attempts) {
$attempts = (int) $attempts;
if ($attempts < 1) {
throw new InvalidArgumentException('Maximum number of attempts must be a positive value.');
}
}
$this->attempts = $attempts;
+5 -5
View File
@@ -7,11 +7,11 @@ interfaces.
Resources
---------
* [Documentation](https://symfony.com/doc/current/components/console.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)
* [Documentation](https://symfony.com/doc/current/components/console.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)
Credits
-------
@@ -0,0 +1,65 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\SignalRegistry;
final class SignalRegistry
{
private $signalHandlers = [];
public function __construct()
{
if (\function_exists('pcntl_async_signals')) {
pcntl_async_signals(true);
}
}
public function register(int $signal, callable $signalHandler): void
{
if (!isset($this->signalHandlers[$signal])) {
$previousCallback = pcntl_signal_get_handler($signal);
if (\is_callable($previousCallback)) {
$this->signalHandlers[$signal][] = $previousCallback;
}
}
$this->signalHandlers[$signal][] = $signalHandler;
pcntl_signal($signal, [$this, 'handle']);
}
public static function isSupported(): bool
{
if (!\function_exists('pcntl_signal')) {
return false;
}
if (\in_array('pcntl_signal', explode(',', ini_get('disable_functions')))) {
return false;
}
return true;
}
/**
* @internal
*/
public function handle(int $signal): void
{
$count = \count($this->signalHandlers[$signal]);
foreach ($this->signalHandlers[$signal] as $i => $signalHandler) {
$hasNext = $i !== $count - 1;
$signalHandler($signal, $hasNext);
}
}
}
+67
View File
@@ -0,0 +1,67 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class SingleCommandApplication extends Command
{
private $version = 'UNKNOWN';
private $autoExit = true;
private $running = false;
public function setVersion(string $version): self
{
$this->version = $version;
return $this;
}
/**
* @final
*/
public function setAutoExit(bool $autoExit): self
{
$this->autoExit = $autoExit;
return $this;
}
public function run(InputInterface $input = null, OutputInterface $output = null): int
{
if ($this->running) {
return parent::run($input, $output);
}
// We use the command name as the application name
$application = new Application($this->getName() ?: 'UNKNOWN', $this->version);
$application->setAutoExit($this->autoExit);
// Fix the usage of the command displayed with "--help"
$this->setName($_SERVER['argv'][0]);
$application->add($this);
$application->setDefaultCommand($this->getName(), true);
$this->running = true;
try {
$ret = $application->run($input, $output);
} finally {
$this->running = false;
}
return $ret ?? 1;
}
}
+1 -1
View File
@@ -35,7 +35,7 @@ abstract class OutputStyle implements OutputInterface, StyleInterface
*/
public function newLine(int $count = 1)
{
$this->output->write(str_repeat(PHP_EOL, $count));
$this->output->write(str_repeat(\PHP_EOL, $count));
}
/**
+1 -1
View File
@@ -85,7 +85,7 @@ interface StyleInterface
*
* @return mixed
*/
public function ask(string $question, ?string $default = null, callable $validator = null);
public function ask(string $question, string $default = null, callable $validator = null);
/**
* Asks a question with the user input hidden.
+32 -18
View File
@@ -21,8 +21,8 @@ use Symfony\Component\Console\Helper\Table;
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;
@@ -35,7 +35,7 @@ use Symfony\Component\Console\Terminal;
*/
class SymfonyStyle extends OutputStyle
{
const MAX_LINE_LENGTH = 120;
public const MAX_LINE_LENGTH = 120;
private $input;
private $questionHelper;
@@ -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);
@@ -59,7 +59,7 @@ class SymfonyStyle extends OutputStyle
*
* @param string|array $messages The message to write in the block
*/
public function block($messages, ?string $type = null, ?string $style = null, string $prefix = ' ', bool $padding = false, bool $escape = true)
public function block($messages, string $type = null, string $style = null, string $prefix = ' ', bool $padding = false, bool $escape = true)
{
$messages = \is_array($messages) ? array_values($messages) : [$messages];
@@ -76,7 +76,7 @@ class SymfonyStyle extends OutputStyle
$this->autoPrependBlock();
$this->writeln([
sprintf('<comment>%s</>', OutputFormatter::escapeTrailingBackslash($message)),
sprintf('<comment>%s</>', str_repeat('=', Helper::strlenWithoutDecoration($this->getFormatter(), $message))),
sprintf('<comment>%s</>', str_repeat('=', Helper::width(Helper::removeDecoration($this->getFormatter(), $message)))),
]);
$this->newLine();
}
@@ -89,7 +89,7 @@ class SymfonyStyle extends OutputStyle
$this->autoPrependBlock();
$this->writeln([
sprintf('<comment>%s</>', OutputFormatter::escapeTrailingBackslash($message)),
sprintf('<comment>%s</>', str_repeat('-', Helper::strlenWithoutDecoration($this->getFormatter(), $message))),
sprintf('<comment>%s</>', str_repeat('-', Helper::width(Helper::removeDecoration($this->getFormatter(), $message)))),
]);
$this->newLine();
}
@@ -163,6 +163,16 @@ class SymfonyStyle extends OutputStyle
$this->block($message, 'NOTE', 'fg=yellow', ' ! ');
}
/**
* Formats an info message.
*
* @param string|array $message
*/
public function info($message)
{
$this->block($message, 'INFO', 'fg=green', ' ', true);
}
/**
* {@inheritdoc}
*/
@@ -254,7 +264,7 @@ class SymfonyStyle extends OutputStyle
/**
* {@inheritdoc}
*/
public function ask(string $question, ?string $default = null, $validator = null)
public function ask(string $question, string $default = null, callable $validator = null)
{
$question = new Question($question, $default);
$question->setValidator($validator);
@@ -265,7 +275,7 @@ class SymfonyStyle extends OutputStyle
/**
* {@inheritdoc}
*/
public function askHidden(string $question, $validator = null)
public function askHidden(string $question, callable $validator = null)
{
$question = new Question($question);
@@ -278,7 +288,7 @@ class SymfonyStyle extends OutputStyle
/**
* {@inheritdoc}
*/
public function confirm($question, $default = true)
public function confirm(string $question, bool $default = true)
{
return $this->askQuestion(new ConfirmationQuestion($question, $default));
}
@@ -290,7 +300,7 @@ class SymfonyStyle extends OutputStyle
{
if (null !== $default) {
$values = array_flip($choices);
$default = isset($values[$default]) ? $values[$default] : $default;
$default = $values[$default] ?? $default;
}
return $this->askQuestion(new ChoiceQuestion($question, $choices, $default));
@@ -422,7 +432,7 @@ class SymfonyStyle extends OutputStyle
private function autoPrependBlock(): void
{
$chars = substr(str_replace(PHP_EOL, "\n", $this->bufferedOutput->fetch()), -2);
$chars = substr(str_replace(\PHP_EOL, "\n", $this->bufferedOutput->fetch()), -2);
if (!isset($chars[0])) {
$this->newLine(); //empty history, so we should start with a new line.
@@ -437,22 +447,21 @@ class SymfonyStyle extends OutputStyle
{
$fetched = $this->bufferedOutput->fetch();
//Prepend new line if last char isn't EOL:
if ("\n" !== substr($fetched, -1)) {
if (!str_ends_with($fetched, "\n")) {
$this->newLine();
}
}
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
{
$indentLength = 0;
$prefixLength = Helper::strlenWithoutDecoration($this->getFormatter(), $prefix);
$prefixLength = Helper::width(Helper::removeDecoration($this->getFormatter(), $prefix));
$lines = [];
if (null !== $type) {
@@ -467,7 +476,12 @@ class SymfonyStyle extends OutputStyle
$message = OutputFormatter::escape($message);
}
$lines = array_merge($lines, explode(PHP_EOL, wordwrap($message, $this->lineLength - $prefixLength - $indentLength, PHP_EOL, true)));
$decorationLength = Helper::width($message) - Helper::width(Helper::removeDecoration($this->getFormatter(), $message));
$messageLineLength = min($this->lineLength - $prefixLength - $indentLength + $decorationLength, $this->lineLength);
$messageLines = explode(\PHP_EOL, wordwrap($message, $messageLineLength, \PHP_EOL, true));
foreach ($messageLines as $messageLine) {
$lines[] = $messageLine;
}
if (\count($messages) > 1 && $key < \count($messages) - 1) {
$lines[] = '';
@@ -487,7 +501,7 @@ class SymfonyStyle extends OutputStyle
}
$line = $prefix.$line;
$line .= str_repeat(' ', $this->lineLength - Helper::strlenWithoutDecoration($this->getFormatter(), $line));
$line .= str_repeat(' ', max($this->lineLength - Helper::width(Helper::removeDecoration($this->getFormatter(), $line)), 0));
if ($style) {
$line = sprintf('<%s>%s</>', $style, $line);
+5
View File
@@ -66,6 +66,11 @@ class Terminal
return self::$stty;
}
// skip check if exec function is disabled
if (!\function_exists('exec')) {
return false;
}
exec('stty 2>&1', $output, $exitcode);
return self::$stty = 0 === $exitcode;
+13 -5
View File
@@ -29,6 +29,8 @@ trait TesterTrait
/**
* Gets the display returned by the last execution of the command or application.
*
* @throws \RuntimeException If it's called before the execute method
*
* @return string The display
*/
public function getDisplay(bool $normalize = false)
@@ -42,7 +44,7 @@ trait TesterTrait
$display = stream_get_contents($this->output->getStream());
if ($normalize) {
$display = str_replace(PHP_EOL, "\n", $display);
$display = str_replace(\PHP_EOL, "\n", $display);
}
return $display;
@@ -66,7 +68,7 @@ trait TesterTrait
$display = stream_get_contents($this->output->getErrorOutput()->getStream());
if ($normalize) {
$display = str_replace(PHP_EOL, "\n", $display);
$display = str_replace(\PHP_EOL, "\n", $display);
}
return $display;
@@ -95,10 +97,16 @@ trait TesterTrait
/**
* Gets the status code returned by the last execution of the command or application.
*
* @throws \RuntimeException If it's called before the execute method
*
* @return int The status code
*/
public function getStatusCode()
{
if (null === $this->statusCode) {
throw new \RuntimeException('Status code not initialized, did you execute the command before requesting the status code?');
}
return $this->statusCode;
}
@@ -139,8 +147,8 @@ trait TesterTrait
}
} else {
$this->output = new ConsoleOutput(
isset($options['verbosity']) ? $options['verbosity'] : ConsoleOutput::VERBOSITY_NORMAL,
isset($options['decorated']) ? $options['decorated'] : null
$options['verbosity'] ?? ConsoleOutput::VERBOSITY_NORMAL,
$options['decorated'] ?? null
);
$errorOutput = new StreamOutput(fopen('php://memory', 'w', false));
@@ -168,7 +176,7 @@ trait TesterTrait
$stream = fopen('php://memory', 'r+', false);
foreach ($inputs as $input) {
fwrite($stream, $input.PHP_EOL);
fwrite($stream, $input.\PHP_EOL);
}
rewind($stream);
+12 -12
View File
@@ -1,8 +1,8 @@
{
"name": "symfony/console",
"type": "library",
"description": "Symfony Console Component",
"keywords": [],
"description": "Eases the creation of beautiful and testable command line interfaces",
"keywords": ["console", "cli", "command line", "terminal"],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
@@ -16,10 +16,13 @@
}
],
"require": {
"php": "^7.2.5",
"php": ">=7.2.5",
"symfony/deprecation-contracts": "^2.1",
"symfony/polyfill-mbstring": "~1.0",
"symfony/polyfill-php73": "^1.8",
"symfony/service-contracts": "^1.1|^2"
"symfony/polyfill-php80": "^1.16",
"symfony/service-contracts": "^1.1|^2",
"symfony/string": "^5.1"
},
"require-dev": {
"symfony/config": "^4.4|^5.0",
@@ -28,10 +31,10 @@
"symfony/lock": "^4.4|^5.0",
"symfony/process": "^4.4|^5.0",
"symfony/var-dumper": "^4.4|^5.0",
"psr/log": "~1.0"
"psr/log": "^1|^2"
},
"provide": {
"psr/log-implementation": "1.0"
"psr/log-implementation": "1.0|2.0"
},
"suggest": {
"symfony/event-dispatcher": "",
@@ -40,7 +43,9 @@
"psr/log": "For using the console logger"
},
"conflict": {
"psr/log": ">=3",
"symfony/dependency-injection": "<4.4",
"symfony/dotenv": "<5.1",
"symfony/event-dispatcher": "<4.4",
"symfony/lock": "<4.4",
"symfony/process": "<4.4"
@@ -51,10 +56,5 @@
"/Tests/"
]
},
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "5.0-dev"
}
}
"minimum-stability": "dev"
}
@@ -0,0 +1,3 @@
vendor/
composer.lock
phpunit.xml
+5
View File
@@ -0,0 +1,5 @@
CHANGELOG
=========
The changelog is maintained for all Symfony contracts at the following URL:
https://github.com/symfony/contracts/blob/main/CHANGELOG.md
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2020-2021 Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+26
View File
@@ -0,0 +1,26 @@
Symfony Deprecation Contracts
=============================
A generic function and convention to trigger deprecation notices.
This package provides a single global function named `trigger_deprecation()` that triggers silenced deprecation notices.
By using a custom PHP error handler such as the one provided by the Symfony ErrorHandler component,
the triggered deprecations can be caught and logged for later discovery, both on dev and prod environments.
The function requires at least 3 arguments:
- the name of the Composer package that is triggering the deprecation
- the version of the package that introduced the deprecation
- the message of the deprecation
- more arguments can be provided: they will be inserted in the message using `printf()` formatting
Example:
```php
trigger_deprecation('symfony/blockchain', '8.9', 'Using "%s" is deprecated, use "%s" instead.', 'bitcoin', 'fabcoin');
```
This will generate the following message:
`Since symfony/blockchain 8.9: Using "bitcoin" is deprecated, use "fabcoin" instead.`
While not necessarily recommended, the deprecation notices can be completely ignored by declaring an empty
`function trigger_deprecation() {}` in your application.
+35
View File
@@ -0,0 +1,35 @@
{
"name": "symfony/deprecation-contracts",
"type": "library",
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=7.1"
},
"autoload": {
"files": [
"function.php"
]
},
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-main": "2.4-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (!function_exists('trigger_deprecation')) {
/**
* Triggers a silenced deprecation notice.
*
* @param string $package The name of the Composer package that is triggering the deprecation
* @param string $version The version of the package that introduced the deprecation
* @param string $message The message of the deprecation
* @param mixed ...$args Values to insert in the message using printf() formatting
*
* @author Nicolas Grekas <p@tchwork.com>
*/
function trigger_deprecation(string $package, string $version, string $message, ...$args): void
{
@trigger_error(($package || $version ? "Since $package $version: " : '').($args ? vsprintf($message, $args) : $message), \E_USER_DEPRECATED);
}
}
-3
View File
@@ -1,3 +0,0 @@
/Tests export-ignore
/phpunit.xml.dist export-ignore
/.gitignore export-ignore
@@ -11,9 +11,13 @@
namespace Symfony\Component\Lock\Exception;
use Symfony\Component\Lock\Lock;
/**
* LockConflictedException is thrown when a lock is acquired by someone else.
*
* In non-blocking mode it is caught by {@see Lock::acquire()} and {@see Lock::acquireRead()}.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class LockConflictedException extends \RuntimeException implements ExceptionInterface
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2016-2020 Fabien Potencier
Copyright (c) 2016-2021 Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+14
View File
@@ -50,6 +50,19 @@ final class Lock implements LockInterface, LoggerAwareInterface
$this->logger = new NullLogger();
}
/**
* @return array
*/
public function __sleep()
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
/**
* Automatically releases the underlying lock when the object is destructed.
*/
@@ -67,6 +80,7 @@ final class Lock implements LockInterface, LoggerAwareInterface
*/
public function acquire($blocking = false): bool
{
$this->key->resetLifetime();
try {
if ($blocking) {
if (!$this->store instanceof StoreInterface && !$this->store instanceof BlockingStoreInterface) {
+8 -5
View File
@@ -1,11 +1,14 @@
Lock Component
==============
The Lock component creates and manages locks, a mechanism to provide exclusive
access to a shared resource.
Resources
---------
* [Documentation](https://symfony.com/doc/master/components/lock.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)
* [Documentation](https://symfony.com/doc/master/components/lock.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)
+11 -6
View File
@@ -29,8 +29,8 @@ use Symfony\Component\Lock\Strategy\StrategyInterface;
*/
class CombinedStore implements StoreInterface, LoggerAwareInterface
{
use LoggerAwareTrait;
use ExpiringStoreTrait;
use LoggerAwareTrait;
/** @var PersistingStoreInterface[] */
private $stores;
@@ -99,8 +99,8 @@ class CombinedStore implements StoreInterface, LoggerAwareInterface
*/
public function waitAndSave(Key $key)
{
@trigger_error(sprintf('%s() is deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', __METHOD__), E_USER_DEPRECATED);
throw new NotSupportedException(sprintf('The store "%s" does not support blocking locks.', \get_class($this)));
@trigger_error(sprintf('%s() is deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', __METHOD__), \E_USER_DEPRECATED);
throw new NotSupportedException(sprintf('The store "%s" does not support blocking locks.', static::class));
}
/**
@@ -171,9 +171,14 @@ class CombinedStore implements StoreInterface, LoggerAwareInterface
$storesCount = \count($this->stores);
foreach ($this->stores as $store) {
if ($store->exists($key)) {
++$successCount;
} else {
try {
if ($store->exists($key)) {
++$successCount;
} else {
++$failureCount;
}
} catch (\Exception $e) {
$this->logger->debug('One store failed to check the "{resource}" lock.', ['resource' => $key, 'store' => $store, 'exception' => $e]);
++$failureCount;
}
+3 -3
View File
@@ -74,7 +74,7 @@ class FlockStore implements StoreInterface, BlockingStoreInterface
$fileName = sprintf('%s/sf.%s.%s.lock',
$this->lockPath,
preg_replace('/[^a-z0-9\._-]+/i', '-', $key),
substr(preg_replace('/[^a-z0-9\._-]+/i', '-', $key), 0, 50),
strtr(substr(base64_encode(hash('sha256', $key, true)), 0, 7), '/', '_')
);
@@ -96,7 +96,7 @@ class FlockStore implements StoreInterface, BlockingStoreInterface
// On Windows, even if PHP doc says the contrary, LOCK_NB works, see
// https://bugs.php.net/54129
if (!flock($handle, LOCK_EX | ($blocking ? 0 : LOCK_NB))) {
if (!flock($handle, \LOCK_EX | ($blocking ? 0 : \LOCK_NB))) {
fclose($handle);
throw new LockConflictedException();
}
@@ -124,7 +124,7 @@ class FlockStore implements StoreInterface, BlockingStoreInterface
$handle = $key->getState(__CLASS__);
flock($handle, LOCK_UN | LOCK_NB);
flock($handle, \LOCK_UN | \LOCK_NB);
fclose($handle);
$key->removeState(__CLASS__);
+7 -7
View File
@@ -43,11 +43,11 @@ class MemcachedStore implements StoreInterface
public function __construct(\Memcached $memcached, int $initialTtl = 300)
{
if (!static::isSupported()) {
throw new InvalidArgumentException('Memcached extension is required');
throw new InvalidArgumentException('Memcached extension is required.');
}
if ($initialTtl < 1) {
throw new InvalidArgumentException(sprintf('%s() expects a strictly positive TTL. Got %d.', __METHOD__, $initialTtl));
throw new InvalidArgumentException(sprintf('"%s()" expects a strictly positive TTL. Got %d.', __METHOD__, $initialTtl));
}
$this->memcached = $memcached;
@@ -76,8 +76,8 @@ class MemcachedStore implements StoreInterface
*/
public function waitAndSave(Key $key)
{
@trigger_error(sprintf('%s() is deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', __METHOD__), E_USER_DEPRECATED);
throw new NotSupportedException(sprintf('The store "%s" does not support blocking locks.', \get_class($this)));
@trigger_error(sprintf('%s() is deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', __METHOD__), \E_USER_DEPRECATED);
throw new NotSupportedException(sprintf('The store "%s" does not support blocking locks.', static::class));
}
/**
@@ -86,7 +86,7 @@ class MemcachedStore implements StoreInterface
public function putOffExpiration(Key $key, $ttl)
{
if ($ttl < 1) {
throw new InvalidTtlException(sprintf('%s() expects a TTL greater or equals to 1 second. Got %s.', __METHOD__, $ttl));
throw new InvalidTtlException(sprintf('"%s()" expects a TTL greater or equals to 1 second. Got %s.', __METHOD__, $ttl));
}
// Interface defines a float value but Store required an integer.
@@ -94,7 +94,7 @@ class MemcachedStore implements StoreInterface
$token = $this->getUniqueToken($key);
list($value, $cas) = $this->getValueAndCas($key);
[$value, $cas] = $this->getValueAndCas($key);
$key->reduceLifetime($ttl);
// Could happens when we ask a putOff after a timeout but in luck nobody steal the lock
@@ -126,7 +126,7 @@ class MemcachedStore implements StoreInterface
{
$token = $this->getUniqueToken($key);
list($value, $cas) = $this->getValueAndCas($key);
[$value, $cas] = $this->getValueAndCas($key);
if ($value !== $token) {
// we are not the owner of the lock. Nothing to do.
+48 -23
View File
@@ -14,6 +14,7 @@ namespace Symfony\Component\Lock\Store;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\DBALException;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Schema\Schema;
use Symfony\Component\Lock\Exception\InvalidArgumentException;
use Symfony\Component\Lock\Exception\InvalidTtlException;
@@ -82,12 +83,12 @@ class PdoStore implements StoreInterface
throw new InvalidArgumentException(sprintf('"%s" requires gcProbability between 0 and 1, "%f" given.', __METHOD__, $gcProbability));
}
if ($initialTtl < 1) {
throw new InvalidTtlException(sprintf('%s() expects a strictly positive TTL, "%d" given.', __METHOD__, $initialTtl));
throw new InvalidTtlException(sprintf('"%s()" expects a strictly positive TTL, "%d" given.', __METHOD__, $initialTtl));
}
if ($connOrDsn instanceof \PDO) {
if (\PDO::ERRMODE_EXCEPTION !== $connOrDsn->getAttribute(\PDO::ATTR_ERRMODE)) {
throw new InvalidArgumentException(sprintf('"%s" requires PDO error mode attribute be set to throw Exceptions (i.e. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION))', __METHOD__));
throw new InvalidArgumentException(sprintf('"%s" requires PDO error mode attribute be set to throw Exceptions (i.e. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION)).', __METHOD__));
}
$this->conn = $connOrDsn;
@@ -126,7 +127,7 @@ class PdoStore implements StoreInterface
try {
$stmt->execute();
} catch (DBALException $e) {
} catch (DBALException | Exception $e) {
// the lock is already acquired. It could be us. Let's try to put off.
$this->putOffExpiration($key, $this->initialTtl);
} catch (\PDOException $e) {
@@ -134,7 +135,7 @@ class PdoStore implements StoreInterface
$this->putOffExpiration($key, $this->initialTtl);
}
if ($this->gcProbability > 0 && (1.0 === $this->gcProbability || (random_int(0, PHP_INT_MAX) / PHP_INT_MAX) <= $this->gcProbability)) {
if ($this->gcProbability > 0 && (1.0 === $this->gcProbability || (random_int(0, \PHP_INT_MAX) / \PHP_INT_MAX) <= $this->gcProbability)) {
$this->prune();
}
@@ -146,7 +147,7 @@ class PdoStore implements StoreInterface
*/
public function waitAndSave(Key $key)
{
@trigger_error(sprintf('%s() is deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', __METHOD__), E_USER_DEPRECATED);
@trigger_error(sprintf('%s() is deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', __METHOD__), \E_USER_DEPRECATED);
throw new NotSupportedException(sprintf('The store "%s" does not supports blocking locks.', __METHOD__));
}
@@ -156,7 +157,7 @@ class PdoStore implements StoreInterface
public function putOffExpiration(Key $key, $ttl)
{
if ($ttl < 1) {
throw new InvalidTtlException(sprintf('%s() expects a TTL greater or equals to 1 second. Got %s.', __METHOD__, $ttl));
throw new InvalidTtlException(sprintf('"%s()" expects a TTL greater or equals to 1 second. Got %s.', __METHOD__, $ttl));
}
$key->reduceLifetime($ttl);
@@ -168,10 +169,10 @@ class PdoStore implements StoreInterface
$stmt->bindValue(':id', $this->getHashedKey($key));
$stmt->bindValue(':token1', $uniqueToken);
$stmt->bindValue(':token2', $uniqueToken);
$stmt->execute();
$result = $stmt->execute();
// If this method is called twice in the same second, the row wouldn't be updated. We have to call exists to know if we are the owner
if (!$stmt->rowCount() && !$this->exists($key)) {
if (!(\is_object($result) ? $result : $stmt)->rowCount() && !$this->exists($key)) {
throw new LockConflictedException();
}
@@ -201,9 +202,9 @@ class PdoStore implements StoreInterface
$stmt->bindValue(':id', $this->getHashedKey($key));
$stmt->bindValue(':token', $this->getUniqueToken($key));
$stmt->execute();
$result = $stmt->execute();
return (bool) $stmt->fetchColumn();
return (bool) (\is_object($result) ? $result->fetchOne() : $stmt->fetchColumn());
}
/**
@@ -249,6 +250,7 @@ class PdoStore implements StoreInterface
*
* @throws \PDOException When the table already exists
* @throws DBALException When the table already exists
* @throws Exception When the table already exists
* @throws \DomainException When an unsupported PDO driver is used
*/
public function createTable(): void
@@ -266,7 +268,11 @@ class PdoStore implements StoreInterface
$table->setPrimaryKey([$this->idCol]);
foreach ($schema->toSql($conn->getDatabasePlatform()) as $sql) {
$conn->exec($sql);
if (method_exists($conn, 'executeStatement')) {
$conn->executeStatement($sql);
} else {
$conn->exec($sql);
}
}
return;
@@ -292,7 +298,11 @@ class PdoStore implements StoreInterface
throw new \DomainException(sprintf('Creating the lock table is currently not implemented for PDO driver "%s".', $driver));
}
$conn->exec($sql);
if (method_exists($conn, 'executeStatement')) {
$conn->executeStatement($sql);
} else {
$conn->exec($sql);
}
}
/**
@@ -302,7 +312,12 @@ class PdoStore implements StoreInterface
{
$sql = "DELETE FROM $this->table WHERE $this->expirationCol <= {$this->getCurrentTimestampStatement()}";
$this->getConnection()->exec($sql);
$conn = $this->getConnection();
if (method_exists($conn, 'executeStatement')) {
$conn->executeStatement($sql);
} else {
$conn->exec($sql);
}
}
private function getDriver(): string
@@ -315,25 +330,35 @@ class PdoStore implements StoreInterface
if ($con instanceof \PDO) {
$this->driver = $con->getAttribute(\PDO::ATTR_DRIVER_NAME);
} else {
switch ($this->driver = $con->getDriver()->getName()) {
case 'mysqli':
case 'pdo_mysql':
case 'drizzle_pdo_mysql':
$driver = $con->getDriver();
switch (true) {
case $driver instanceof \Doctrine\DBAL\Driver\Mysqli\Driver:
throw new \LogicException(sprintf('The adapter "%s" does not support the mysqli driver, use pdo_mysql instead.', static::class));
case $driver instanceof \Doctrine\DBAL\Driver\AbstractMySQLDriver:
$this->driver = 'mysql';
break;
case 'pdo_sqlite':
case $driver instanceof \Doctrine\DBAL\Driver\PDOSqlite\Driver:
case $driver instanceof \Doctrine\DBAL\Driver\PDO\SQLite\Driver:
$this->driver = 'sqlite';
break;
case 'pdo_pgsql':
case $driver instanceof \Doctrine\DBAL\Driver\PDOPgSql\Driver:
case $driver instanceof \Doctrine\DBAL\Driver\PDO\PgSQL\Driver:
$this->driver = 'pgsql';
break;
case 'oci8':
case 'pdo_oracle':
case $driver instanceof \Doctrine\DBAL\Driver\OCI8\Driver:
case $driver instanceof \Doctrine\DBAL\Driver\PDOOracle\Driver:
case $driver instanceof \Doctrine\DBAL\Driver\PDO\OCI\Driver:
$this->driver = 'oci';
break;
case 'pdo_sqlsrv':
case $driver instanceof \Doctrine\DBAL\Driver\SQLSrv\Driver:
case $driver instanceof \Doctrine\DBAL\Driver\PDOSqlsrv\Driver:
case $driver instanceof \Doctrine\DBAL\Driver\PDO\SQLSrv\Driver:
$this->driver = 'sqlsrv';
break;
default:
$this->driver = \get_class($driver);
break;
}
}
@@ -341,7 +366,7 @@ class PdoStore implements StoreInterface
}
/**
* Provides a SQL function to get the current timestamp regarding the current connection's driver.
* Provides an SQL function to get the current timestamp regarding the current connection's driver.
*/
private function getCurrentTimestampStatement(): string
{
+10 -10
View File
@@ -33,20 +33,20 @@ class RedisStore implements StoreInterface
private $initialTtl;
/**
* @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface $redisClient
* @param float $initialTtl the expiration delay of locks in seconds
* @param \Redis|\RedisArray|\RedisCluster|RedisProxy|RedisClusterProxy|\Predis\ClientInterface $redis
* @param float $initialTtl The expiration delay of locks in seconds
*/
public function __construct($redisClient, float $initialTtl = 300.0)
public function __construct($redis, float $initialTtl = 300.0)
{
if (!$redisClient instanceof \Redis && !$redisClient instanceof \RedisArray && !$redisClient instanceof \RedisCluster && !$redisClient instanceof \Predis\ClientInterface && !$redisClient instanceof RedisProxy) {
throw new InvalidArgumentException(sprintf('%s() expects parameter 1 to be Redis, RedisArray, RedisCluster or Predis\ClientInterface, %s given', __METHOD__, \is_object($redisClient) ? \get_class($redisClient) : \gettype($redisClient)));
if (!$redis instanceof \Redis && !$redis instanceof \RedisArray && !$redis instanceof \RedisCluster && !$redis instanceof \Predis\ClientInterface && !$redis instanceof RedisProxy && !$redis instanceof RedisClusterProxy) {
throw new InvalidArgumentException(sprintf('"%s()" expects parameter 1 to be Redis, RedisArray, RedisCluster, RedisProxy, RedisClusterProxy or Predis\ClientInterface, "%s" given.', __METHOD__, \is_object($redis) ? \get_class($redis) : \gettype($redis)));
}
if ($initialTtl <= 0) {
throw new InvalidTtlException(sprintf('%s() expects a strictly positive TTL. Got %d.', __METHOD__, $initialTtl));
throw new InvalidTtlException(sprintf('"%s()" expects a strictly positive TTL. Got %d.', __METHOD__, $initialTtl));
}
$this->redis = $redisClient;
$this->redis = $redis;
$this->initialTtl = $initialTtl;
}
@@ -80,8 +80,8 @@ class RedisStore implements StoreInterface
*/
public function waitAndSave(Key $key)
{
@trigger_error(sprintf('%s() is deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', __METHOD__), E_USER_DEPRECATED);
throw new NotSupportedException(sprintf('The store "%s" does not support blocking locks.', \get_class($this)));
@trigger_error(sprintf('%s() is deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', __METHOD__), \E_USER_DEPRECATED);
throw new NotSupportedException(sprintf('The store "%s" does not support blocking locks.', static::class));
}
/**
@@ -153,7 +153,7 @@ class RedisStore implements StoreInterface
return $this->redis->eval(...array_merge([$script, 1, $resource], $args));
}
throw new InvalidArgumentException(sprintf('%s() expects being initialized with a Redis, RedisArray, RedisCluster or Predis\ClientInterface, %s given', __METHOD__, \is_object($this->redis) ? \get_class($this->redis) : \gettype($this->redis)));
throw new InvalidArgumentException(sprintf('"%s()" expects being initialized with a Redis, RedisArray, RedisCluster or Predis\ClientInterface, "%s" given.', __METHOD__, \is_object($this->redis) ? \get_class($this->redis) : \gettype($this->redis)));
}
private function getUniqueToken(Key $key): string
+1 -1
View File
@@ -38,7 +38,7 @@ class RetryTillSaveStore implements BlockingStoreInterface, StoreInterface, Logg
* @param int $retrySleep Duration in ms between 2 retry
* @param int $retryCount Maximum amount of retry
*/
public function __construct(PersistingStoreInterface $decorated, int $retrySleep = 100, int $retryCount = PHP_INT_MAX)
public function __construct(PersistingStoreInterface $decorated, int $retrySleep = 100, int $retryCount = \PHP_INT_MAX)
{
$this->decorated = $decorated;
$this->retrySleep = $retrySleep;
+2 -2
View File
@@ -37,7 +37,7 @@ class SemaphoreStore implements StoreInterface, BlockingStoreInterface
public function __construct()
{
if (!static::isSupported()) {
throw new InvalidArgumentException('Semaphore extension (sysvsem) is required');
throw new InvalidArgumentException('Semaphore extension (sysvsem) is required.');
}
}
@@ -63,7 +63,7 @@ class SemaphoreStore implements StoreInterface, BlockingStoreInterface
return;
}
$keyId = crc32($key);
$keyId = unpack('i', md5($key, true))[1];
$resource = sem_get($keyId);
$acquired = @sem_acquire($resource, !$blocking);
+21 -21
View File
@@ -33,7 +33,7 @@ class StoreFactory
public static function createStore($connection)
{
if (!\is_string($connection) && !\is_object($connection)) {
throw new \TypeError(sprintf('Argument 1 passed to %s() must be a string or a connection object, %s given.', __METHOD__, \gettype($connection)));
throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be a string or a connection object, "%s" given.', __METHOD__, \gettype($connection)));
}
switch (true) {
@@ -56,45 +56,45 @@ class StoreFactory
return new ZookeeperStore($connection);
case !\is_string($connection):
throw new InvalidArgumentException(sprintf('Unsupported Connection: %s.', \get_class($connection)));
throw new InvalidArgumentException(sprintf('Unsupported Connection: "%s".', \get_class($connection)));
case 'flock' === $connection:
return new FlockStore();
case 0 === strpos($connection, 'flock://'):
case str_starts_with($connection, 'flock://'):
return new FlockStore(substr($connection, 8));
case 'semaphore' === $connection:
return new SemaphoreStore();
case 0 === strpos($connection, 'redis://'):
case 0 === strpos($connection, 'rediss://'):
case 0 === strpos($connection, 'memcached://'):
case str_starts_with($connection, 'redis:'):
case str_starts_with($connection, 'rediss:'):
case str_starts_with($connection, 'memcached:'):
if (!class_exists(AbstractAdapter::class)) {
throw new InvalidArgumentException(sprintf('Unsupported DSN "%s". Try running "composer require symfony/cache".', $connection));
}
$storeClass = 0 === strpos($connection, 'memcached://') ? MemcachedStore::class : RedisStore::class;
$storeClass = str_starts_with($connection, 'memcached:') ? MemcachedStore::class : RedisStore::class;
$connection = AbstractAdapter::createConnection($connection, ['lazy' => true]);
return new $storeClass($connection);
case 0 === strpos($connection, 'mssql://'):
case 0 === strpos($connection, 'mysql:'):
case 0 === strpos($connection, 'mysql2://'):
case 0 === strpos($connection, 'oci:'):
case 0 === strpos($connection, 'oci8://'):
case 0 === strpos($connection, 'pdo_oci://'):
case 0 === strpos($connection, 'pgsql:'):
case 0 === strpos($connection, 'postgres://'):
case 0 === strpos($connection, 'postgresql://'):
case 0 === strpos($connection, 'sqlsrv:'):
case 0 === strpos($connection, 'sqlite:'):
case 0 === strpos($connection, 'sqlite3://'):
case str_starts_with($connection, 'mssql://'):
case str_starts_with($connection, 'mysql:'):
case str_starts_with($connection, 'mysql2://'):
case str_starts_with($connection, 'oci:'):
case str_starts_with($connection, 'oci8://'):
case str_starts_with($connection, 'pdo_oci://'):
case str_starts_with($connection, 'pgsql:'):
case str_starts_with($connection, 'postgres://'):
case str_starts_with($connection, 'postgresql://'):
case str_starts_with($connection, 'sqlsrv:'):
case str_starts_with($connection, 'sqlite:'):
case str_starts_with($connection, 'sqlite3://'):
return new PdoStore($connection);
case 0 === strpos($connection, 'zookeeper://'):
case str_starts_with($connection, 'zookeeper://'):
return new ZookeeperStore(ZookeeperStore::createConnection($connection));
}
throw new InvalidArgumentException(sprintf('Unsupported Connection: %s.', $connection));
throw new InvalidArgumentException(sprintf('Unsupported Connection: "%s".', $connection));
}
}
+5 -5
View File
@@ -37,12 +37,12 @@ class ZookeeperStore implements StoreInterface
public static function createConnection(string $dsn): \Zookeeper
{
if (0 !== strpos($dsn, 'zookeeper:')) {
throw new InvalidArgumentException(sprintf('Unsupported DSN: %s.', $dsn));
if (!str_starts_with($dsn, 'zookeeper:')) {
throw new InvalidArgumentException(sprintf('Unsupported DSN: "%s".', $dsn));
}
if (false === $params = parse_url($dsn)) {
throw new InvalidArgumentException(sprintf('Invalid Zookeeper DSN: %s.', $dsn));
throw new InvalidArgumentException(sprintf('Invalid Zookeeper DSN: "%s".', $dsn));
}
$host = $params['host'] ?? '';
@@ -108,7 +108,7 @@ class ZookeeperStore implements StoreInterface
*/
public function waitAndSave(Key $key)
{
@trigger_error(sprintf('%s() is deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', __METHOD__), E_USER_DEPRECATED);
@trigger_error(sprintf('%s() is deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', __METHOD__), \E_USER_DEPRECATED);
throw new NotSupportedException();
}
@@ -153,7 +153,7 @@ class ZookeeperStore implements StoreInterface
// For example: foo/bar will become /foo-bar and /foo/bar will become /-foo-bar
$resource = (string) $key;
if (false !== strpos($resource, '/')) {
if (str_contains($resource, '/')) {
$resource = strtr($resource, ['/' => '-']).'-'.sha1($resource);
}
+7 -12
View File
@@ -1,7 +1,7 @@
{
"name": "symfony/lock",
"type": "library",
"description": "Symfony Lock Component",
"description": "Creates and manages locks, a mechanism to provide exclusive access to a shared resource",
"keywords": ["locking", "redlock", "mutex", "semaphore", "flock", "cas"],
"homepage": "https://symfony.com",
"license": "MIT",
@@ -16,16 +16,16 @@
}
],
"require": {
"php": "^7.1.3",
"psr/log": "~1.0"
"php": ">=7.1.3",
"psr/log": "^1|^2|^3",
"symfony/polyfill-php80": "^1.16"
},
"require-dev": {
"doctrine/dbal": "~2.5",
"mongodb/mongodb": "~1.1",
"doctrine/dbal": "^2.6|^3.0",
"predis/predis": "~1.0"
},
"conflict": {
"doctrine/dbal": "<2.5"
"doctrine/dbal": "<2.6"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Lock\\": "" },
@@ -33,10 +33,5 @@
"/Tests/"
]
},
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "4.4-dev"
}
}
"minimum-stability": "dev"
}
+24
View File
@@ -11,16 +11,40 @@
use Symfony\Polyfill\Ctype as p;
if (\PHP_VERSION_ID >= 80000) {
return require __DIR__.'/bootstrap80.php';
}
if (!function_exists('ctype_alnum')) {
function ctype_alnum($text) { return p\Ctype::ctype_alnum($text); }
}
if (!function_exists('ctype_alpha')) {
function ctype_alpha($text) { return p\Ctype::ctype_alpha($text); }
}
if (!function_exists('ctype_cntrl')) {
function ctype_cntrl($text) { return p\Ctype::ctype_cntrl($text); }
}
if (!function_exists('ctype_digit')) {
function ctype_digit($text) { return p\Ctype::ctype_digit($text); }
}
if (!function_exists('ctype_graph')) {
function ctype_graph($text) { return p\Ctype::ctype_graph($text); }
}
if (!function_exists('ctype_lower')) {
function ctype_lower($text) { return p\Ctype::ctype_lower($text); }
}
if (!function_exists('ctype_print')) {
function ctype_print($text) { return p\Ctype::ctype_print($text); }
}
if (!function_exists('ctype_punct')) {
function ctype_punct($text) { return p\Ctype::ctype_punct($text); }
}
if (!function_exists('ctype_space')) {
function ctype_space($text) { return p\Ctype::ctype_space($text); }
}
if (!function_exists('ctype_upper')) {
function ctype_upper($text) { return p\Ctype::ctype_upper($text); }
}
if (!function_exists('ctype_xdigit')) {
function ctype_xdigit($text) { return p\Ctype::ctype_xdigit($text); }
}
+46
View File
@@ -0,0 +1,46 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Symfony\Polyfill\Ctype as p;
if (!function_exists('ctype_alnum')) {
function ctype_alnum(mixed $text): bool { return p\Ctype::ctype_alnum($text); }
}
if (!function_exists('ctype_alpha')) {
function ctype_alpha(mixed $text): bool { return p\Ctype::ctype_alpha($text); }
}
if (!function_exists('ctype_cntrl')) {
function ctype_cntrl(mixed $text): bool { return p\Ctype::ctype_cntrl($text); }
}
if (!function_exists('ctype_digit')) {
function ctype_digit(mixed $text): bool { return p\Ctype::ctype_digit($text); }
}
if (!function_exists('ctype_graph')) {
function ctype_graph(mixed $text): bool { return p\Ctype::ctype_graph($text); }
}
if (!function_exists('ctype_lower')) {
function ctype_lower(mixed $text): bool { return p\Ctype::ctype_lower($text); }
}
if (!function_exists('ctype_print')) {
function ctype_print(mixed $text): bool { return p\Ctype::ctype_print($text); }
}
if (!function_exists('ctype_punct')) {
function ctype_punct(mixed $text): bool { return p\Ctype::ctype_punct($text); }
}
if (!function_exists('ctype_space')) {
function ctype_space(mixed $text): bool { return p\Ctype::ctype_space($text); }
}
if (!function_exists('ctype_upper')) {
function ctype_upper(mixed $text): bool { return p\Ctype::ctype_upper($text); }
}
if (!function_exists('ctype_xdigit')) {
function ctype_xdigit(mixed $text): bool { return p\Ctype::ctype_xdigit($text); }
}
+6 -2
View File
@@ -16,7 +16,7 @@
}
],
"require": {
"php": ">=5.3.3"
"php": ">=7.1"
},
"autoload": {
"psr-4": { "Symfony\\Polyfill\\Ctype\\": "" },
@@ -28,7 +28,11 @@
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "1.15-dev"
"dev-main": "1.23-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
}
}
File diff suppressed because one or more lines are too long
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2015-2019 Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+31
View File
@@ -0,0 +1,31 @@
Symfony Polyfill / Intl: Grapheme
=================================
This component provides a partial, native PHP implementation of the
[Grapheme functions](https://php.net/intl.grapheme) from the
[Intl](https://php.net/intl) extension.
- [`grapheme_extract`](https://php.net/grapheme_extract): Extract a sequence of grapheme
clusters from a text buffer, which must be encoded in UTF-8
- [`grapheme_stripos`](https://php.net/grapheme_stripos): Find position (in grapheme units)
of first occurrence of a case-insensitive string
- [`grapheme_stristr`](https://php.net/grapheme_stristr): Returns part of haystack string
from the first occurrence of case-insensitive needle to the end of haystack
- [`grapheme_strlen`](https://php.net/grapheme_strlen): Get string length in grapheme units
- [`grapheme_strpos`](https://php.net/grapheme_strpos): Find position (in grapheme units)
of first occurrence of a string
- [`grapheme_strripos`](https://php.net/grapheme_strripos): Find position (in grapheme units)
of last occurrence of a case-insensitive string
- [`grapheme_strrpos`](https://php.net/grapheme_strrpos): Find position (in grapheme units)
of last occurrence of a string
- [`grapheme_strstr`](https://php.net/grapheme_strstr): Returns part of haystack string from
the first occurrence of needle to the end of haystack
- [`grapheme_substr`](https://php.net/grapheme_substr): Return part of a string
More information can be found in the
[main Polyfill README](https://github.com/symfony/polyfill/blob/master/README.md).
License
=======
This library is released under the [MIT license](LICENSE).
+58
View File
@@ -0,0 +1,58 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Symfony\Polyfill\Intl\Grapheme as p;
if (extension_loaded('intl')) {
return;
}
if (\PHP_VERSION_ID >= 80000) {
return require __DIR__.'/bootstrap80.php';
}
if (!defined('GRAPHEME_EXTR_COUNT')) {
define('GRAPHEME_EXTR_COUNT', 0);
}
if (!defined('GRAPHEME_EXTR_MAXBYTES')) {
define('GRAPHEME_EXTR_MAXBYTES', 1);
}
if (!defined('GRAPHEME_EXTR_MAXCHARS')) {
define('GRAPHEME_EXTR_MAXCHARS', 2);
}
if (!function_exists('grapheme_extract')) {
function grapheme_extract($haystack, $size, $type = 0, $start = 0, &$next = 0) { return p\Grapheme::grapheme_extract($haystack, $size, $type, $start, $next); }
}
if (!function_exists('grapheme_stripos')) {
function grapheme_stripos($haystack, $needle, $offset = 0) { return p\Grapheme::grapheme_stripos($haystack, $needle, $offset); }
}
if (!function_exists('grapheme_stristr')) {
function grapheme_stristr($haystack, $needle, $beforeNeedle = false) { return p\Grapheme::grapheme_stristr($haystack, $needle, $beforeNeedle); }
}
if (!function_exists('grapheme_strlen')) {
function grapheme_strlen($input) { return p\Grapheme::grapheme_strlen($input); }
}
if (!function_exists('grapheme_strpos')) {
function grapheme_strpos($haystack, $needle, $offset = 0) { return p\Grapheme::grapheme_strpos($haystack, $needle, $offset); }
}
if (!function_exists('grapheme_strripos')) {
function grapheme_strripos($haystack, $needle, $offset = 0) { return p\Grapheme::grapheme_strripos($haystack, $needle, $offset); }
}
if (!function_exists('grapheme_strrpos')) {
function grapheme_strrpos($haystack, $needle, $offset = 0) { return p\Grapheme::grapheme_strrpos($haystack, $needle, $offset); }
}
if (!function_exists('grapheme_strstr')) {
function grapheme_strstr($haystack, $needle, $beforeNeedle = false) { return p\Grapheme::grapheme_strstr($haystack, $needle, $beforeNeedle); }
}
if (!function_exists('grapheme_substr')) {
function grapheme_substr($string, $offset, $length = null) { return p\Grapheme::grapheme_substr($string, $offset, $length); }
}
+50
View File
@@ -0,0 +1,50 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Symfony\Polyfill\Intl\Grapheme as p;
if (!defined('GRAPHEME_EXTR_COUNT')) {
define('GRAPHEME_EXTR_COUNT', 0);
}
if (!defined('GRAPHEME_EXTR_MAXBYTES')) {
define('GRAPHEME_EXTR_MAXBYTES', 1);
}
if (!defined('GRAPHEME_EXTR_MAXCHARS')) {
define('GRAPHEME_EXTR_MAXCHARS', 2);
}
if (!function_exists('grapheme_extract')) {
function grapheme_extract(?string $haystack, ?int $size, ?int $type = GRAPHEME_EXTR_COUNT, ?int $offset = 0, &$next = null): string|false { return p\Grapheme::grapheme_extract((string) $haystack, (int) $size, (int) $type, (int) $offset, $next); }
}
if (!function_exists('grapheme_stripos')) {
function grapheme_stripos(?string $haystack, ?string $needle, ?int $offset = 0): int|false { return p\Grapheme::grapheme_stripos((string) $haystack, (string) $needle, (int) $offset); }
}
if (!function_exists('grapheme_stristr')) {
function grapheme_stristr(?string $haystack, ?string $needle, ?bool $beforeNeedle = false): string|false { return p\Grapheme::grapheme_stristr((string) $haystack, (string) $needle, (bool) $beforeNeedle); }
}
if (!function_exists('grapheme_strlen')) {
function grapheme_strlen(?string $string): int|false|null { return p\Grapheme::grapheme_strlen((string) $string); }
}
if (!function_exists('grapheme_strpos')) {
function grapheme_strpos(?string $haystack, ?string $needle, ?int $offset = 0): int|false { return p\Grapheme::grapheme_strpos((string) $haystack, (string) $needle, (int) $offset); }
}
if (!function_exists('grapheme_strripos')) {
function grapheme_strripos(?string $haystack, ?string $needle, ?int $offset = 0): int|false { return p\Grapheme::grapheme_strripos((string) $haystack, (string) $needle, (int) $offset); }
}
if (!function_exists('grapheme_strrpos')) {
function grapheme_strrpos(?string $haystack, ?string $needle, ?int $offset = 0): int|false { return p\Grapheme::grapheme_strrpos((string) $haystack, (string) $needle, (int) $offset); }
}
if (!function_exists('grapheme_strstr')) {
function grapheme_strstr(?string $haystack, ?string $needle, ?bool $beforeNeedle = false): string|false { return p\Grapheme::grapheme_strstr((string) $haystack, (string) $needle, (bool) $beforeNeedle); }
}
if (!function_exists('grapheme_substr')) {
function grapheme_substr(?string $string, ?int $offset, ?int $length = null): string|false { return p\Grapheme::grapheme_substr((string) $string, (int) $offset, $length); }
}
+38
View File
@@ -0,0 +1,38 @@
{
"name": "symfony/polyfill-intl-grapheme",
"type": "library",
"description": "Symfony polyfill for intl's grapheme_* functions",
"keywords": ["polyfill", "shim", "compatibility", "portable", "intl", "grapheme"],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=7.1"
},
"autoload": {
"psr-4": { "Symfony\\Polyfill\\Intl\\Grapheme\\": "" },
"files": [ "bootstrap.php" ]
},
"suggest": {
"ext-intl": "For best performance"
},
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
}
}

Some files were not shown because too many files have changed in this diff Show More