Вход Регистрация
Файл: vendor/symfony/console/Helper/QuestionHelper.php
Строк: 659
<?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 SymfonyComponentConsoleHelper;

use 
SymfonyComponentConsoleCursor;
use 
SymfonyComponentConsoleExceptionMissingInputException;
use 
SymfonyComponentConsoleExceptionRuntimeException;
use 
SymfonyComponentConsoleFormatterOutputFormatter;
use 
SymfonyComponentConsoleFormatterOutputFormatterStyle;
use 
SymfonyComponentConsoleInputInputInterface;
use 
SymfonyComponentConsoleInputStreamableInputInterface;
use 
SymfonyComponentConsoleOutputConsoleOutputInterface;
use 
SymfonyComponentConsoleOutputConsoleSectionOutput;
use 
SymfonyComponentConsoleOutputOutputInterface;
use 
SymfonyComponentConsoleQuestionChoiceQuestion;
use 
SymfonyComponentConsoleQuestionQuestion;
use 
SymfonyComponentConsoleTerminal;

use function 
SymfonyComponentStrings;

/**
 * The QuestionHelper class provides helpers to interact with the user.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class QuestionHelper extends Helper
{
    
/**
     * @var resource|null
     */
    
private $inputStream;

    private static 
bool $stty true;
    private static 
bool $stdinIsInteractive;

    
/**
     * Asks a question to the user.
     *
     * @return mixed The user answer
     *
     * @throws RuntimeException If there is no data to read in the input stream
     */
    
public function ask(InputInterface $inputOutputInterface $outputQuestion $question): mixed
    
{
        if (
$output instanceof ConsoleOutputInterface) {
            
$output $output->getErrorOutput();
        }

        if (!
$input->isInteractive()) {
            return 
$this->getDefaultAnswer($question);
        }

        if (
$input instanceof StreamableInputInterface && $stream $input->getStream()) {
            
$this->inputStream $stream;
        }

        try {
            if (!
$question->getValidator()) {
                return 
$this->doAsk($output$question);
            }

            
$interviewer fn () => $this->doAsk($output$question);

            return 
$this->validateAttempts($interviewer$output$question);
        } catch (
MissingInputException $exception) {
            
$input->setInteractive(false);

            if (
null === $fallbackOutput $this->getDefaultAnswer($question)) {
                throw 
$exception;
            }

            return 
$fallbackOutput;
        }
    }

    public function 
getName(): string
    
{
        return 
'question';
    }

    
/**
     * Prevents usage of stty.
     *
     * @return void
     */
    
public static function disableStty()
    {
        
self::$stty false;
    }

    
/**
     * Asks the question to the user.
     *
     * @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden
     */
    
private function doAsk(OutputInterface $outputQuestion $question): mixed
    
{
        
$this->writePrompt($output$question);

        
$inputStream $this->inputStream ?: STDIN;
        
$autocomplete $question->getAutocompleterCallback();

        if (
null === $autocomplete || !self::$stty || !Terminal::hasSttyAvailable()) {
            
$ret false;
            if (
$question->isHidden()) {
                try {
                    
$hiddenResponse $this->getHiddenResponse($output$inputStream$question->isTrimmable());
                    
$ret $question->isTrimmable() ? trim($hiddenResponse) : $hiddenResponse;
                } catch (
RuntimeException $e) {
                    if (!
$question->isHiddenFallback()) {
                        throw 
$e;
                    }
                }
            }

            if (
false === $ret) {
                
$isBlocked stream_get_meta_data($inputStream)['blocked'] ?? true;

                if (!
$isBlocked) {
                    
stream_set_blocking($inputStreamtrue);
                }

                
$ret $this->readInput($inputStream$question);

                if (!
$isBlocked) {
                    
stream_set_blocking($inputStreamfalse);
                }

                if (
false === $ret) {
                    throw new 
MissingInputException('Aborted.');
                }
                if (
$question->isTrimmable()) {
                    
$ret trim($ret);
                }
            }
        } else {
            
$autocomplete $this->autocomplete($output$question$inputStream$autocomplete);
            
$ret $question->isTrimmable() ? trim($autocomplete) : $autocomplete;
        }

        if (
$output instanceof ConsoleSectionOutput) {
            
$output->addContent(''); // add EOL to the question
            
$output->addContent($ret);
        }

        
$ret strlen($ret) > $ret $question->getDefault();

        if (
$normalizer $question->getNormalizer()) {
            return 
$normalizer($ret);
        }

        return 
$ret;
    }

    private function 
getDefaultAnswer(Question $question): mixed
    
{
        
$default $question->getDefault();

        if (
null === $default) {
            return 
$default;
        }

        if (
$validator $question->getValidator()) {
            return 
call_user_func($validator$default);
        } elseif (
$question instanceof ChoiceQuestion) {
            
$choices $question->getChoices();

            if (!
$question->isMultiselect()) {
                return 
$choices[$default] ?? $default;
            }

            
$default explode(','$default);
            foreach (
$default as $k => $v) {
                
$v $question->isTrimmable() ? trim($v) : $v;
                
$default[$k] = $choices[$v] ?? $v;
            }
        }

        return 
$default;
    }

    
/**
     * Outputs the question prompt.
     *
     * @return void
     */
    
protected function writePrompt(OutputInterface $outputQuestion $question)
    {
        
$message $question->getQuestion();

        if (
$question instanceof ChoiceQuestion) {
            
$output->writeln(array_merge([
                
$question->getQuestion(),
            ], 
$this->formatChoiceQuestionChoices($question'info')));

            
$message $question->getPrompt();
        }

        
$output->write($message);
    }

    
/**
     * @return string[]
     */
    
protected function formatChoiceQuestionChoices(ChoiceQuestion $questionstring $tag): array
    {
        
$messages = [];

        
$maxWidth max(array_map([__CLASS__'width'], array_keys($choices $question->getChoices())));

        foreach (
$choices as $key => $value) {
            
$padding str_repeat(' '$maxWidth self::width($key));

            
$messages[] = sprintf("  [<$tag>%s$padding</$tag>] %s"$key$value);
        }

        return 
$messages;
    }

    
/**
     * Outputs an error message.
     *
     * @return void
     */
    
protected function writeError(OutputInterface $outputException $error)
    {
        if (
null !== $this->getHelperSet() && $this->getHelperSet()->has('formatter')) {
            
$message $this->getHelperSet()->get('formatter')->formatBlock($error->getMessage(), 'error');
        } else {
            
$message '<error>'.$error->getMessage().'</error>';
        }

        
$output->writeln($message);
    }

    
/**
     * Autocompletes a question.
     *
     * @param resource $inputStream
     */
    
private function autocomplete(OutputInterface $outputQuestion $question$inputStream, callable $autocomplete): string
    
{
        
$cursor = new Cursor($output$inputStream);

        
$fullChoice '';
        
$ret '';

        
$i 0;
        
$ofs = -1;
        
$matches $autocomplete($ret);
        
$numMatches count($matches);

        
$sttyMode shell_exec('stty -g');
        
$isStdin 'php://stdin' === (stream_get_meta_data($inputStream)['uri'] ?? null);
        
$r = [$inputStream];
        
$w = [];

        
// Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead)
        
shell_exec('stty -icanon -echo');

        
// Add highlighted text style
        
$output->getFormatter()->setStyle('hl', new OutputFormatterStyle('black''white'));

        
// Read a keypress
        
while (!feof($inputStream)) {
            while (
$isStdin && === @stream_select($r$w$w0100)) {
                
// Give signal handlers a chance to run
                
$r = [$inputStream];
            }
            
$c fread($inputStream1);

            
// as opposed to fgets(), fread() returns an empty string when the stream content is empty, not false.
            
if (false === $c || ('' === $ret && '' === $c && null === $question->getDefault())) {
                
shell_exec('stty '.$sttyMode);
                throw new 
MissingInputException('Aborted.');
            } elseif (
"177" === $c) { // Backspace Character
                
if (=== $numMatches && !== $i) {
                    --
$i;
                    
$cursor->moveLeft(s($fullChoice)->slice(-1)->width(false));

                    
$fullChoice self::substr($fullChoice0$i);
                }

                if (
=== $i) {
                    
$ofs = -1;
                    
$matches $autocomplete($ret);
                    
$numMatches count($matches);
                } else {
                    
$numMatches 0;
                }

                
// Pop the last character off the end of our string
                
$ret self::substr($ret0$i);
            } elseif (
"33" === $c) {
                
// Did we read an escape sequence?
                
$c .= fread($inputStream2);

                
// A = Up Arrow. B = Down Arrow
                
if (isset($c[2]) && ('A' === $c[2] || 'B' === $c[2])) {
                    if (
'A' === $c[2] && -=== $ofs) {
                        
$ofs 0;
                    }

                    if (
=== $numMatches) {
                        continue;
                    }

                    
$ofs += ('A' === $c[2]) ? -1;
                    
$ofs = ($numMatches $ofs) % $numMatches;
                }
            } elseif (
ord($c) < 32) {
                if (
"t" === $c || "n" === $c) {
                    if (
$numMatches && -!== $ofs) {
                        
$ret = (string) $matches[$ofs];
                        
// Echo out remaining chars for current match
                        
$remainingCharacters substr($retstrlen(trim($this->mostRecentlyEnteredValue($fullChoice))));
                        
$output->write($remainingCharacters);
                        
$fullChoice .= $remainingCharacters;
                        
$i = (false === $encoding mb_detect_encoding($fullChoicenulltrue)) ? strlen($fullChoice) : mb_strlen($fullChoice$encoding);

                        
$matches array_filter(
                            
$autocomplete($ret),
                            
fn ($match) => '' === $ret || str_starts_with($match$ret)
                        );
                        
$numMatches count($matches);
                        
$ofs = -1;
                    }

                    if (
"n" === $c) {
                        
$output->write($c);
                        break;
                    }

                    
$numMatches 0;
                }

                continue;
            } else {
                if (
"x80" <= $c) {
                    
$c .= fread($inputStream, ["xC0" => 1"xD0" => 1"xE0" => 2"xF0" => 3][$c "xF0"]);
                }

                
$output->write($c);
                
$ret .= $c;
                
$fullChoice .= $c;
                ++
$i;

                
$tempRet $ret;

                if (
$question instanceof ChoiceQuestion && $question->isMultiselect()) {
                    
$tempRet $this->mostRecentlyEnteredValue($fullChoice);
                }

                
$numMatches 0;
                
$ofs 0;

                foreach (
$autocomplete($ret) as $value) {
                    
// If typed characters match the beginning chunk of value (e.g. [AcmeDe]moBundle)
                    
if (str_starts_with($value$tempRet)) {
                        
$matches[$numMatches++] = $value;
                    }
                }
            }

            
$cursor->clearLineAfter();

            if (
$numMatches && -!== $ofs) {
                
$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>');
                
$cursor->restorePosition();
            }
        }

        
// Reset stty so it behaves normally again
        
shell_exec('stty '.$sttyMode);

        return 
$fullChoice;
    }

    private function 
mostRecentlyEnteredValue(string $entered): string
    
{
        
// Determine the most recent value that the user entered
        
if (!str_contains($entered',')) {
            return 
$entered;
        }

        
$choices explode(','$entered);
        if (
'' !== $lastChoice trim($choices[count($choices) - 1])) {
            return 
$lastChoice;
        }

        return 
$entered;
    }

    
/**
     * Gets a hidden response from user.
     *
     * @param resource $inputStream The handler resource
     * @param bool     $trimmable   Is the answer trimmable
     *
     * @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden
     */
    
private function getHiddenResponse(OutputInterface $output$inputStreambool $trimmable true): string
    
{
        if (
'\' === DIRECTORY_SEPARATOR) {
            $exe = __DIR__.'
/../Resources/bin/hiddeninput.exe';

            // handle code running from a phar
            if (str_starts_with(__FILE__, '
phar:')) {
                $tmpExe = sys_get_temp_dir().'
/hiddeninput.exe';
                copy($exe, $tmpExe);
                $exe = $tmpExe;
            }

            $sExec = shell_exec('"'.
$exe.'"');
            $value = $trimmable ? rtrim($sExec) : $sExec;
            $output->writeln('');

            if (isset($tmpExe)) {
                unlink($tmpExe);
            }

            return $value;
        }

        if (self::$stty && Terminal::hasSttyAvailable()) {
            $sttyMode = shell_exec('
stty -g');
            shell_exec('
stty -echo');
        } elseif ($this->isInteractiveInput($inputStream)) {
            throw new RuntimeException('
Unable to hide the response.');
        }

        $value = fgets($inputStream, 4096);

        if (4095 === strlen($value)) {
            $errOutput = $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output;
            $errOutput->warning('
The value was possibly truncated by your shell or terminal emulator');
        }

        if (self::$stty && Terminal::hasSttyAvailable()) {
            shell_exec('
stty '.$sttyMode);
        }

        if (false === $value) {
            throw new MissingInputException('
Aborted.');
        }
        if ($trimmable) {
            $value = trim($value);
        }
        $output->writeln('');

        return $value;
    }

    /**
     * Validates an attempt.
     *
     * @param callable $interviewer A callable that will ask for a question and return the result
     *
     * @throws Exception In case the max number of attempts has been reached and no valid response has been given
     */
    private function validateAttempts(callable $interviewer, OutputInterface $output, Question $question): mixed
    {
        $error = null;
        $attempts = $question->getMaxAttempts();

        while (null === $attempts || $attempts--) {
            if (null !== $error) {
                $this->writeError($output, $error);
            }

            try {
                return $question->getValidator()($interviewer());
            } catch (RuntimeException $e) {
                throw $e;
            } catch (Exception $error) {
            }
        }

        throw $error;
    }

    private function isInteractiveInput($inputStream): bool
    {
        if ('
php://stdin' !== (stream_get_meta_data($inputStream)['uri'] ?? null)) {
            
return false;
        }

        if (isset(
self::$stdinIsInteractive)) {
            return 
self::$stdinIsInteractive;
        }

        return 
self::$stdinIsInteractive = @stream_isatty(fopen('php://stdin''r'));
    }

    
/**
     * 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
     */
    
private function readInput($inputStreamQuestion $question): string|false
    
{
        if (!
$question->isMultiline()) {
            
$cp $this->setIOCodepage();
            
$ret fgets($inputStream4096);

            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);
    }

    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.
     */
    
private function resetIOCodepage(int $cpstring|false $input): string|false
    
{
        if (
!== $cp) {
            
sapi_windows_cp_set($cp);

            if (
false !== $input && '' !== $input) {
                
$input sapi_windows_cp_conv(sapi_windows_cp_get('oem'), $cp$input);
            }
        }

        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;
    }
}
Онлайн: 0
Реклама