You are here

class QuestionHelper in Zircon Profile 8

Same name and namespace in other branches
  1. 8.0 vendor/symfony/console/Helper/QuestionHelper.php \Symfony\Component\Console\Helper\QuestionHelper

The QuestionHelper class provides helpers to interact with the user.

@author Fabien Potencier <fabien@symfony.com>

Hierarchy

Expanded class hierarchy of QuestionHelper

2 files declare their use of QuestionHelper
Application.php in vendor/symfony/console/Application.php
QuestionHelperTest.php in vendor/symfony/console/Tests/Helper/QuestionHelperTest.php

File

vendor/symfony/console/Helper/QuestionHelper.php, line 26

Namespace

Symfony\Component\Console\Helper
View source
class QuestionHelper extends Helper {
  private $inputStream;
  private static $shell;
  private static $stty;

  /**
   * Asks a question to the user.
   *
   * @param InputInterface  $input    An InputInterface instance
   * @param OutputInterface $output   An OutputInterface instance
   * @param Question        $question The question to ask
   *
   * @return string The user answer
   *
   * @throws \RuntimeException If there is no data to read in the input stream
   */
  public function ask(InputInterface $input, OutputInterface $output, Question $question) {
    if ($output instanceof ConsoleOutputInterface) {
      $output = $output
        ->getErrorOutput();
    }
    if (!$input
      ->isInteractive()) {
      return $question
        ->getDefault();
    }
    if (!$question
      ->getValidator()) {
      return $this
        ->doAsk($output, $question);
    }
    $that = $this;
    $interviewer = function () use ($output, $question, $that) {
      return $that
        ->doAsk($output, $question);
    };
    return $this
      ->validateAttempts($interviewer, $output, $question);
  }

  /**
   * Sets the input stream to read from when interacting with the user.
   *
   * This is mainly useful for testing purpose.
   *
   * @param resource $stream The input stream
   *
   * @throws \InvalidArgumentException In case the stream is not a resource
   */
  public function setInputStream($stream) {
    if (!is_resource($stream)) {
      throw new \InvalidArgumentException('Input stream must be a valid resource.');
    }
    $this->inputStream = $stream;
  }

  /**
   * Returns the helper's input stream.
   *
   * @return resource
   */
  public function getInputStream() {
    return $this->inputStream;
  }

  /**
   * {@inheritdoc}
   */
  public function getName() {
    return 'question';
  }

  /**
   * Asks the question to the user.
   *
   * This method is public for PHP 5.3 compatibility, it should be private.
   *
   * @param OutputInterface $output
   * @param Question        $question
   *
   * @return bool|mixed|null|string
   *
   * @throws \Exception
   * @throws \RuntimeException
   */
  public function doAsk(OutputInterface $output, Question $question) {
    $this
      ->writePrompt($output, $question);
    $inputStream = $this->inputStream ?: STDIN;
    $autocomplete = $question
      ->getAutocompleterValues();
    if (null === $autocomplete || !$this
      ->hasSttyAvailable()) {
      $ret = false;
      if ($question
        ->isHidden()) {
        try {
          $ret = trim($this
            ->getHiddenResponse($output, $inputStream));
        } catch (\RuntimeException $e) {
          if (!$question
            ->isHiddenFallback()) {
            throw $e;
          }
        }
      }
      if (false === $ret) {
        $ret = fgets($inputStream, 4096);
        if (false === $ret) {
          throw new \RuntimeException('Aborted');
        }
        $ret = trim($ret);
      }
    }
    else {
      $ret = trim($this
        ->autocomplete($output, $question, $inputStream));
    }
    $ret = strlen($ret) > 0 ? $ret : $question
      ->getDefault();
    if ($normalizer = $question
      ->getNormalizer()) {
      return $normalizer($ret);
    }
    return $ret;
  }

  /**
   * Outputs the question prompt.
   *
   * @param OutputInterface $output
   * @param Question        $question
   */
  protected function writePrompt(OutputInterface $output, Question $question) {
    $message = $question
      ->getQuestion();
    if ($question instanceof ChoiceQuestion) {
      $width = max(array_map('strlen', array_keys($question
        ->getChoices())));
      $messages = (array) $question
        ->getQuestion();
      foreach ($question
        ->getChoices() as $key => $value) {
        $messages[] = sprintf("  [<info>%-{$width}s</info>] %s", $key, $value);
      }
      $output
        ->writeln($messages);
      $message = $question
        ->getPrompt();
    }
    $output
      ->write($message);
  }

  /**
   * Outputs an error message.
   *
   * @param OutputInterface $output
   * @param \Exception      $error
   */
  protected function writeError(OutputInterface $output, \Exception $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 OutputInterface $output
   * @param Question        $question
   *
   * @return string
   */
  private function autocomplete(OutputInterface $output, Question $question, $inputStream) {
    $autocomplete = $question
      ->getAutocompleterValues();
    $ret = '';
    $i = 0;
    $ofs = -1;
    $matches = $autocomplete;
    $numMatches = count($matches);
    $sttyMode = shell_exec('stty -g');

    // 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)) {
      $c = fread($inputStream, 1);

      // Backspace Character
      if ("" === $c) {
        if (0 === $numMatches && 0 !== $i) {
          --$i;

          // Move cursor backwards
          $output
            ->write("\33[1D");
        }
        if ($i === 0) {
          $ofs = -1;
          $matches = $autocomplete;
          $numMatches = count($matches);
        }
        else {
          $numMatches = 0;
        }

        // Pop the last character off the end of our string
        $ret = substr($ret, 0, $i);
      }
      elseif ("\33" === $c) {

        // Did we read an escape sequence?
        $c .= fread($inputStream, 2);

        // A = Up Arrow. B = Down Arrow
        if (isset($c[2]) && ('A' === $c[2] || 'B' === $c[2])) {
          if ('A' === $c[2] && -1 === $ofs) {
            $ofs = 0;
          }
          if (0 === $numMatches) {
            continue;
          }
          $ofs += 'A' === $c[2] ? -1 : 1;
          $ofs = ($numMatches + $ofs) % $numMatches;
        }
      }
      elseif (ord($c) < 32) {
        if ("\t" === $c || "\n" === $c) {
          if ($numMatches > 0 && -1 !== $ofs) {
            $ret = $matches[$ofs];

            // Echo out remaining chars for current match
            $output
              ->write(substr($ret, $i));
            $i = strlen($ret);
          }
          if ("\n" === $c) {
            $output
              ->write($c);
            break;
          }
          $numMatches = 0;
        }
        continue;
      }
      else {
        $output
          ->write($c);
        $ret .= $c;
        ++$i;
        $numMatches = 0;
        $ofs = 0;
        foreach ($autocomplete as $value) {

          // If typed characters match the beginning chunk of value (e.g. [AcmeDe]moBundle)
          if (0 === strpos($value, $ret) && $i !== strlen($value)) {
            $matches[$numMatches++] = $value;
          }
        }
      }

      // Erase characters from cursor to end of line
      $output
        ->write("\33[K");
      if ($numMatches > 0 && -1 !== $ofs) {

        // Save cursor position
        $output
          ->write("\0337");

        // Write highlighted text
        $output
          ->write('<hl>' . substr($matches[$ofs], $i) . '</hl>');

        // Restore cursor position
        $output
          ->write("\338");
      }
    }

    // Reset stty so it behaves normally again
    shell_exec(sprintf('stty %s', $sttyMode));
    return $ret;
  }

  /**
   * Gets a hidden response from user.
   *
   * @param OutputInterface $output An Output instance
   *
   * @return string The answer
   *
   * @throws \RuntimeException In case the fallback is deactivated and the response cannot be hidden
   */
  private function getHiddenResponse(OutputInterface $output, $inputStream) {
    if ('\\' === DIRECTORY_SEPARATOR) {
      $exe = __DIR__ . '/../Resources/bin/hiddeninput.exe';

      // handle code running from a phar
      if ('phar:' === substr(__FILE__, 0, 5)) {
        $tmpExe = sys_get_temp_dir() . '/hiddeninput.exe';
        copy($exe, $tmpExe);
        $exe = $tmpExe;
      }
      $value = rtrim(shell_exec($exe));
      $output
        ->writeln('');
      if (isset($tmpExe)) {
        unlink($tmpExe);
      }
      return $value;
    }
    if ($this
      ->hasSttyAvailable()) {
      $sttyMode = shell_exec('stty -g');
      shell_exec('stty -echo');
      $value = fgets($inputStream, 4096);
      shell_exec(sprintf('stty %s', $sttyMode));
      if (false === $value) {
        throw new \RuntimeException('Aborted');
      }
      $value = trim($value);
      $output
        ->writeln('');
      return $value;
    }
    if (false !== ($shell = $this
      ->getShell())) {
      $readCmd = $shell === 'csh' ? 'set mypassword = $<' : 'read -r mypassword';
      $command = sprintf("/usr/bin/env %s -c 'stty -echo; %s; stty echo; echo \$mypassword'", $shell, $readCmd);
      $value = rtrim(shell_exec($command));
      $output
        ->writeln('');
      return $value;
    }
    throw new \RuntimeException('Unable to hide the response.');
  }

  /**
   * Validates an attempt.
   *
   * @param callable        $interviewer A callable that will ask for a question and return the result
   * @param OutputInterface $output      An Output instance
   * @param Question        $question    A Question instance
   *
   * @return string The validated response
   *
   * @throws \Exception In case the max number of attempts has been reached and no valid response has been given
   */
  private function validateAttempts($interviewer, OutputInterface $output, Question $question) {
    $error = null;
    $attempts = $question
      ->getMaxAttempts();
    while (null === $attempts || $attempts--) {
      if (null !== $error) {
        $this
          ->writeError($output, $error);
      }
      try {
        return call_user_func($question
          ->getValidator(), $interviewer());
      } catch (\Exception $error) {
      }
    }
    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() {
    if (null !== self::$shell) {
      return self::$shell;
    }
    self::$shell = false;
    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 (array(
        'bash',
        'zsh',
        'ksh',
        'csh',
      ) as $sh) {
        if ('OK' === rtrim(shell_exec(sprintf($test, $sh)))) {
          self::$shell = $sh;
          break;
        }
      }
    }
    return self::$shell;
  }

  /**
   * Returns whether Stty is available or not.
   *
   * @return bool
   */
  private function hasSttyAvailable() {
    if (null !== self::$stty) {
      return self::$stty;
    }
    exec('stty 2>&1', $output, $exitcode);
    return self::$stty = $exitcode === 0;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
Helper::$helperSet protected property
Helper::formatMemory public static function
Helper::formatTime public static function
Helper::getHelperSet public function Gets the helper set associated with this helper. Overrides HelperInterface::getHelperSet
Helper::setHelperSet public function Sets the helper set associated with this helper. Overrides HelperInterface::setHelperSet
Helper::strlen public static function Returns the length of a string, using mb_strwidth if it is available.
Helper::strlenWithoutDecoration public static function
QuestionHelper::$inputStream private property
QuestionHelper::$shell private static property
QuestionHelper::$stty private static property
QuestionHelper::ask public function Asks a question to the user. 1
QuestionHelper::autocomplete private function Autocompletes a question.
QuestionHelper::doAsk public function Asks the question to the user.
QuestionHelper::getHiddenResponse private function Gets a hidden response from user.
QuestionHelper::getInputStream public function Returns the helper's input stream.
QuestionHelper::getName public function Returns the canonical name of this helper. Overrides HelperInterface::getName
QuestionHelper::getShell private function Returns a valid unix shell.
QuestionHelper::hasSttyAvailable private function Returns whether Stty is available or not.
QuestionHelper::setInputStream public function Sets the input stream to read from when interacting with the user.
QuestionHelper::validateAttempts private function Validates an attempt.
QuestionHelper::writeError protected function Outputs an error message. 1
QuestionHelper::writePrompt protected function Outputs the question prompt. 1