Skip to content

[9.x] Console: Alternative Attribute Syntax for Artisan Commands for better type support and more. #41131

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 8 commits into from

Conversation

thettler
Copy link

@thettler thettler commented Feb 20, 2022

🔨 What does this PR do?

This PR aims to add an alternative syntax to the artisan command signature by using attributes. It is fully backwards compatible and does not break the existing $signature syntax.

👀 How does it look?

#[ArtisanCommand(
    name: 'basic:command',
    description: 'Basic Command description!',
    help: 'Some Help.',
)]
class BasicCommand extends Command
{
    #[Argument]
    protected string $myArgument;

    #[Option]
    protected bool $myOption;
    
    public function handle()
    {
      // ...
    }
}
php artisan basic:command myArgumentValue --myOption

➕ What are the benefits?

  • Static analysis is easier because every command input can, and should, be typed.
  • Autocompletion by the IDE because command inputs now are simple properties instead of a function call
  • Its easier to add more functionality to the command inputs because there is now a clear place where to put it. More about that in a later section "🔮 What can be done in future?".
  • (Personal Opinion) It's easier to remember and write, then the $signature because you get more help from the IDE and you don't need to remember the custom string syntax.
  • Adds negatable options
  • Adds options with required values
  • Adds command name aliases

➖ What are the drawbacks?

  • Of course it's a little bit more Code and complexity to maintain
  • It can be more to type than the $signature syntax especially for small commands, but of course you could still use the $signature syntax for those commands and the attributes for more complex ones.

🔮 What can be done in future?

In order to keep this PR as small as possible i only added the base functionality. But i already have a lot of ideas what can be done with this kind of syntax.

Casting

At the moment only Enum Types are casted by default. But it is easily possible to add a caster to an argument or option to directly cast a command input to an object, model ... or something else. This could look like this or it would check if the typed class implements the Castable interface.

Show

class BasicCommand extends Command
{
    #[Argument(
        cast: UserCast::class
    )]
    protected User $user;
    
    // ...
   
}

Validation

Also the Validator could be added like this:

Show

class BasicCommand extends Command
{
    #[Argument(
        validate: 'max:30'
    )]
    protected string $shortString;
    
    // ...
   
}

Auto Ask

It's a good practise to ask the user for a required argument if it was not provided instead of failing the command. This could be the default behaviour or can be turned on like this:

Show

class BasicCommand extends Command
{
    #[Argument(
        autoAsk: true
    )]
    protected string $message;
    
    // ...
   
}
php artisan myCommand
Please provide a message:
$ 

🔬 How does it work?

Basically there are two differed types of attributes. The ArtisanCommand and the ConsoleInput.

ArtisanCommand Attribute

This attribute is placed at the class level and defines the name, description, help, if the command is hidden and aliases for the command.

#[ArtisanCommand(
    name: 'basic:command',
    description: 'Basic Command description!',
    help: 'Some Help.',
    hidden: true,
    aliases: ['alias:command'],
)]
class BasicCommand extends Command
{
      // ...
}

Console Input

Console inputs are written als properties on the command class who get decorated with ether the Argument or the Option attribute.

    #[Argument]
    protected string $myArgument;

    #[Option]
    protected bool $myOption;

Arguments

If there is a non-nullable type the argument will be required. If you want an optional argument or an argument with an default value you ether make the type nullable or add an initial value to the property

    #[Argument]
    protected ?string $myArgument; // Optional argument

    #[Argument]
    protected string $myDefaultArgument = 'Your default'; // Optional argument with a default value

If you need an array argument simply typehint the property as array. If you want it to be optional or with a default value make it nullable or add a default.

    #[Argument]
    protected array $myArgument; // required array argument

    #[Argument]
    protected ?array $myOptionalArgument; // optional array argument

    #[Argument]
    protected array $myDefaultArgument = ['Item A', 'Item B']; // Optional array argument with a default value

Beware that an optional array Argument will always be an empty array and not null.

Options

Options are pretty similar regarding the default and optional syntax. But with one catch. There are two different kinds of options, one without a value (simple boolean flags) and ones with values. The ones with Values are pretty much the same as the arguments. Nullable -> optional, With initial value -> default Value.

    #[Option]
    protected string $requiredValue; // if the option is used the User must specify a value  
    
    #[Option]
    protected ?string $optionalValue; // The value is optional

    #[Option]
    protected string $defaultValue = 'default'; // The option has a default value

    #[Option]
    protected array $array; // an Array Option 

If you want a boolean flag option simply typehint bool and the value will be false if the option wasn't used and true if it was used:

    #[Option]
    protected bool $boolFlag;

To use shortcuts on options you can simply specify the shortcut on the Option attribute

    #[Option(
        shortcut: 'O'
    )]
    protected bool $option;

If you want to use a negatable options you can simply specify it on the Option attribute

    #[Option(
        negatable: true
    )]
    protected bool $yell;
php artisan basic --yell
php artisan basic --no-yell

Descriptions

Arguments and options can have a description you can simply specify it on the attribute:

    #[Argument(
        description: 'My fancy argument description'
    )]
    protected string $myArgument;

    #[Option(
        description: 'My fancy option description'
    )]
    protected bool $myOption;

Alias

Arguments and options can be aliased to avoid conflicts with other parent properties or to make the api more readable for the user:

    #[Argument(
        as: 'publicArgumentName'
    )]
    protected string $internalArgumentName;

    #[Option(
        as: 'publicOptionName'
    )]
    protected bool $internalOptionName;

Enum support

Enums will be automatically be cast back and forth.

            #[Argument]
            public MyEnum $enumArgument;

            #[Argument]
            public MyEnum $enumDefaultArgument = MyEnum::Something;

❤️ Thank you

Thank you very much for taking the time and reading this. This is my first PR to Laravel so
i am open for feedback and discussion about the syntax or implementation and happily change the PR accordingly.

@X-Coder264
Copy link
Contributor

Symfony already has an attribute for defining console commands -> symfony/symfony#40234

Can't we (re)use that?

@driesvints
Copy link
Member

Definitely some good work on this but imho I believe the current mechanism to define the command signature is enough. This would add an awful lot for us to maintain. I'm personally also not a fan of attributes...

@thettler
Copy link
Author

@driesvints thank you for the feedback. Is this already a "no" for merging this? :)
Because otherwise I would try to implement the comment from @X-Coder264

@driesvints
Copy link
Member

@thettler I don't merge PR's, Taylor does. Just adding my opinion.

@thettler
Copy link
Author

@X-Coder264 Thanks for the Feedback. I didn't knew this Attribute already exist.

I had a look but came to the conclusion that we can't simply reuse it. There are two reasons for that:

  1. I want to have all attributes under the same namespace and don't want to use a Symfony class directly like this in Laravel user code. So i needed to extend the AsConsole class from Symfony , but this caused the reflection of the core Symfony command to not work anymore because it explicitly search for the AsConsole Class. So in order to get it working with our own Laravel class i would have to overwrite some methods: getDefaultName(), getDefaultDescription(). I think this only adds more complexity, then simply having our own attribute and use the setters.
  2. The AsConsole class does not support the help text and i want to be able to configure all of this stuff via the Attribute.

But i am open for a different opinion here. 😄

🔧 But i did changed some things:

  1. First i renamed CommandAttribute to ArtisanCommand. I think this is a better Name, but i am open for more suggestions.
  2. I added aliases support for commands so one command can be called with multiple names.
  3. And lastly i extracted an portion of the code, that was responsible for configure the command with the attribute, form the initCommandData() to a function in the HasAttributeSyntax trait -> initCommandDataFromAttribute()

@thettler thettler marked this pull request as ready for review February 21, 2022 12:06
@taylorotwell
Copy link
Member

Thanks for your pull request to Laravel!

Unfortunately, I'm going to delay merging this code for now. To preserve our ability to adequately maintain the framework, we need to be very careful regarding the amount of code we include.

If possible, please consider releasing your code as a package so that the community can still take advantage of your contributions!

If you feel absolutely certain that this code corrects a bug in the framework, please "@" mention me in a follow-up comment with further explanation so that GitHub will send me a notification of your response.

@Wulfheart
Copy link

@thettler please make it a package!

@mbabker
Copy link
Contributor

mbabker commented Feb 21, 2022

FWIW, Symfony 6.1 deprecates the $defaultName and $defaultDescription properties that were used in part by the lazy command loader in favor of the AsCommand attribute. The name property from the Symfony attribute is already usable Laravel commands if not using the $signature Laravel command property, but the hidden and description attribute properties would be overwritten with defaults in the Laravel command constructor without declaring those elsewhere. So, I do think it'd be beneficial if the framework fully supported the attribute, otherwise Laravel is going to lose the benefits from #34873 and other related PRs when Symfony 7 becomes a thing.

@thettler
Copy link
Author

thettler commented Mar 1, 2022

For everybody who likes the idea i have created a Package with all of the Features i mentioned in the PR:

Laravel Console Toolkit

It supports

Support Name Description
Laravel Features Supports everything laravel can do
Attribute Syntax Use PHP-Attributes to automatically define your inputs based on types
Casting Automatically cast your inputs to Enums, Models, Objects or anything you want
Validation Use the Laravel Validator to validate the inputs from the console
Auto Ask If the user provides an invalid value toolkit will ask again for a valid value without the need to run the command again
Negatable Options Options can be specified as opposites: --dry or --no-dry
Option required Value Options can have required values

Feel free to try it out and give me Feedback.

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.

6 participants