Skip to content

Negatable options in fluent command signatures - #51708

Closed
paulredmond wants to merge 4 commits into
laravel:11.xfrom
paulredmond:negatable-options
Closed

paulredmond wants to merge 4 commits into
laravel:11.xfrom
paulredmond:negatable-options

Conversation

@paulredmond

@paulredmond paulredmond commented Jun 4, 2024 •

Copy link
Copy Markdown
Contributor

I was reaching for negatable options on an existing command and thought I'd propose a way to use them with the fluent signature:

php artisan my:command --notify
php artisan my:command --no-notify

Here's the simplest example:

use Illuminate\Console\Command;

class MyCommand extends Command
{
    protected $signature = 'my:command {--cache=!}';
}

Negatable options can have one of three states: null, false, and true. Running my:command without --cache|--no-cache, the default option value will be null. This is useful if you'd like to prompt the user of their preference if they don't pass an option:

# Prompts the user
$ php artisan my:command
> Would you like the cache the result?
...

# Skips prompt
$ php artisan my:command --cache
$ php artisan my:command --no-cache

You can also get both variants of the option:

// my:command
$this->option('cache'); // null
$this->option('no-cache'); // null

// my:command --cache
$this->option('cache'); // true
$this->option('no-cache'); // false

// my:command --no-cache
$this->option('cache'); // false
$this->option('no-cache'); // true

I like negatable options to unify toggling a boolean value on/off:
image

Using fluent signatures, you can also choose to define the default value as true or false when the user doesn't pass an option if you'd like:

protected $signature = 'my:command {--cache=!true : Enable/Disable cache (default true).}';
protected $signature = 'my:command {--cache=!false : Enable/Disable cache (default false).}';

Handling an Invalid Default

Developers could misconfigure the fluent syntax. This implementation chooses to fall back to the default value of null when filter_var() fails to filter a Boolean:

// (true) fails using `filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)`
protected $signature = 'my:command {--cache=!(true) : Enable/Disable cache.}';

// Running `my:command`
$this->option('cache'); // null

// Running `my:command --cache` 
$this->option('cache'); // true

// Running `my:command --no-cache` 
$this->option('cache'); // false

Programmatically Calling Commands

You still need to pass true or false when using negatable options programmatically. However, you can use both flags:

use Illuminate\Support\Facades\Artisan;

Artisan::call('my:command', ['--cache' => true]); // cache=true
Artisan::call('my:command', ['--cache' => false]); // cache=false

// Slightly confusing, using the `--no-*` version always sets the option to `false`:
Artisan::call('my:command', ['--no-cache' => false]); // cache=false
Artisan::call('my:command', ['--no-cache' => true]); // cache=false

Possible B/C Issues?

The syntax could be whatever is desired if this PR is merged. The syntax I chose in this PR is the ! character (I also considered ~). I did a quick Google search and found that ~ and ¬ are commonly used to represent negation. Although edge-case, my concern with ~ is a higher possibility that user-land signatures use ~ as the first character of a default value for the home folder on Unix systems:

protected $signature = 'my:command {--dir=~}';

The above would be broken by this implementation, which would convert the option to a negatable option. That being said, there's a possibility that ! breaks user-land code too, but seems edge-case and odd that someone would start a default value with !:

protected $signature = 'my:command {--message=!This is a warning : The message to send.}';

Fluent command signatures already support the * symbol for arrays, so to me, the ! doesn't feel too far a stretch for fluent negatable options:

protected $signature = 'my:command
    {--option=* : Some array option}
    {--cache=! : Enable/Disable cache.}';

Reasons to Not Merge / Current Options using Fluent Signature

Without merging this PR, a developer can use a boolean "switch" by using only one of two options depending on what the desired default should be:

protected $signature = 'my:command {--cache}'

// Prompt the user if they want to cache or not:
// my:command
$this->option('cache'); // false

// my:command --cache
// They passed the option, cache it.
$this->option('cache'); // true

You could also define both flags, but it's possible that both flags are passed and both would be true:

protected $signature = 'my:command {--cache} {--no-cache}'

I still prefer the DX of having separate flags to toggle a boolean—with the option of null by default when omitted.

Another approach outside of this PR is not using the fluent syntax and instead defining command options using configure():

protected function configure(): void
{        
    $this
        ->setName('my:command')
        // ...
        ->addOption('cache', null, InputOption::VALUE_NEGATABLE, "Cache/Don't cache result.", true);
}

@paulredmond paulredmond changed the title Negatable options Negatable options in fluent command signatures Jun 4, 2024
@taylorotwell

Copy link
Copy Markdown
Member

I dunno - I just can't wrap my head around negatable options being a useful thing 😅 Seems like if you had a value like --cache... --no-cache is available by just not passing the option. I don't think I've ever needed something like this in my work. How can I be convinced? 😊

@paulredmond

Copy link
Copy Markdown
Contributor Author

Yeah, you're probably right, thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants