10

How would one configure symfony/console to accept a dynamic list of options?

That said - the names for the options aren't known on development step so I need an application to accept everything and expose it using the standard $input->getOption.

Any chance it can be done easily (without hacking the component in million places)?

My attempts included extending the ArgvInput and InputDefinition classes but they failed due to various reasons (they are objective and symfony/console component implementation-specific). Briefly: the former requires parsing to be invoked multiple times; the latter - is instantiated in multiple places so I just couldn't find a proper way to inject it.

zerkms
  • 249,484
  • 69
  • 436
  • 539
  • The various reasons being... – webmaster777 Feb 18 '14 at 22:34
  • @webmaster777: it requires knowing the component internals. For the person who knows how it's implemented - they would be obvious, for the person who doesn't - it will take half an hour and references to a lot of files to explain it. I actually started trying it when created a question but then found that I'm falling into meaningless explanations. Provided a brief details though – zerkms Feb 18 '14 at 22:36
  • So I guess you've tried: but you can't create an override for `InputDefinition::getOption($name)` which doesn't throw an Exception but instead creates the option and `InputDefinition::hasOption($name)` always returning `true` or is there somewhere else being checked whether the option exists? – webmaster777 Feb 18 '14 at 22:42
  • @webmaster777: as I mentioned - I couldn't inject `InputDefinition` properly since it's being instantiated internally as well (ie: https://github.com/symfony/Console/blob/master/Command/Command.php#L59). But - yep, it was my original idea. – zerkms Feb 18 '14 at 22:46
  • So, you don't want to create a custom `Command`, in which you could overload the constructor. – webmaster777 Feb 18 '14 at 22:55
  • @webmaster777: it's just one place. I remember there were multiple. And I'm looking for something that wouldn't require extend dozens of classes (if it's possible). PS: even if I extend a command class - it would be tricky to pass the required instance there (since commands are instantiated by an `Application` class internally as well) – zerkms Feb 18 '14 at 22:56
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/47800/discussion-between-webmaster777-and-zerkms) – webmaster777 Feb 18 '14 at 23:03
  • @webmaster777: If I create a `ArgvInput` with a custom definition in a `console/app` - it immediately starts validating input - and fails because the default arguments (for a command name) aren't set yet – zerkms Feb 19 '14 at 00:53

4 Answers4

5

You can create your own ArgvInput that will allow all options.

For example you can see the slightly modified version of ArgvInput here

I have only modified lines : 178

And comment out the lines: 188-199

Then pass instance of your version ArgvInput instead of default one to

$input = new AcceptAllArgvInput();    
$kernel = new AppKernel($env, $debug);
$application = new Application($kernel);
$application->run($input);
xiidea
  • 3,344
  • 20
  • 24
  • For exemplary implementation of this solution, check [LiberalFormatArgvInput](https://github.com/apigen/apigen/commit/c1747ba88767cc2be669702730d05589f7e9fb62) – Tomas Votruba Jan 02 '15 at 05:35
4

I have accomplished this in the past using the IS_ARRAY option. Would this not work for your instance as well?

->addArgument('routeParams', InputArgument::IS_ARRAY, "Required Placeholders for route");

My use case was a custom URL generator for a special authentication system. I needed a way to generate URLs for testing. Naturally, each route has a different number of required parameters and I wanted to avoid passing the parameters as a CSV string.

Command examples: Usage: myGenerateToken user route [variables1] ... [variablesN]

 php app/console myGenerateToken 1 productHomePage
 php app/console myGenerateToken 1 getProduct 1
 php app/console myGenerateToken 1 getProductFile 1 changelog

The variables were delivered to the command in the "routeParams" as an array

 $params = $input->getArgument('routeParams');
 var_dump($params);

 array(2) {
   [0] =>
   string(1) "1"
   [1] =>
   string(9) "changelog"
 }

I noticed that there is also an "Option" version called InputOption::VALUE_IS_ARRAY, but I did not have success getting it to work. The argument version InputArgument::IS_ARRAY seems to behave as an option anyways, as it does not error if no arguments are specified.

EDIT:

The author's question is seeking "How do i define variable command line options at run time" where my answer is "How do you provide multiple values for a pre-defined option/argument"

Fodagus
  • 496
  • 5
  • 13
  • They are arguments, not options. This might work in some cases, not for this particular one. +1 though :-) – zerkms Nov 18 '14 at 20:09
  • Oh, i think i see the distinction. You're saying that you need a console command that has a variable array of acceptible options (--dry-run, --name=name, --round=4, etc), depending on runtime conditions? – Fodagus Nov 19 '14 at 17:48
  • Yep. The command works with some kind of templates (which are determined in runtime) and I wanted the values for them to be passed as options. But as I mentioned - your answer might be helpful to someone so please don't drop it :-) – zerkms Nov 19 '14 at 19:30
  • 1
    I see. Yes, i figured as much because i found your question while researching my answer, so i figured others might have the same confusion, but given that my answer was wrong, i also wanted to document my confusion so others would know why my answer didn't apply :-) – Fodagus Nov 20 '14 at 19:39
4

Here is how to implement this on PHP 7+ using symfony/console ^3.0:

abstract class CommandWithDynamicOptions extends Command {

    /** @var array The list of dynamic options passed to the command */
    protected $dynamicOptions = [];

  /**
   * @inheritdoc
   */
  protected function configure() {
    $this->setName('custom:command');

    $this->setDefinition(new class($this->getDefinition(), $this->dynamicOptions) extends InputDefinition {
      protected $dynamicOptions = [];

      public function __construct(InputDefinition $definition, array &$dynamicOptions) {
        parent::__construct();
        $this->setArguments($definition->getArguments());
        $this->setOptions($definition->getOptions());
        $this->dynamicOptions =& $dynamicOptions;
      }

      public function getOption($name) {
        if (!parent::hasOption($name)) {
          $this->addOption(new InputOption($name, null, InputOption::VALUE_OPTIONAL));
          $this->dynamicOptions[] = $name;
        }
        return parent::getOption($name);
      }

      public function hasOption($name) {
        return TRUE;
      }

    });
  }
}
Pierre Buyle
  • 4,883
  • 2
  • 32
  • 31
0

Another approach bypassing Symfony validation and reading from argv directly:

class Foo extends Command {
    protected function configure() {
        $this->ignoreValidationErrors();
    }

    protected function execute(InputInterface $input, OutputInterface $output): int {
        global $argv;

        $customOptions = array_filter($argv, static function ($value) {
            return is_string($value) && substr($value, 0, 2) === '--';
        });

        var_dump($customOptions);
    }
}
./foo --foo=bar

Result:

array(1) {
  [4]=> string(9) "--foo=bar"
}

Lucas Bustamante
  • 15,821
  • 7
  • 92
  • 86